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/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml index ce5b8c5f4e..77d5287964 100644 --- a/.github/workflows/java-publish-snapshot.yml +++ b/.github/workflows/java-publish-snapshot.yml @@ -171,6 +171,80 @@ jobs: if-no-files-found: error retention-days: 1 + build-linuxmusl-arm64-classifier: + name: Build Linux musl ARM64 snapshot classifier + needs: resolve-source + runs-on: ubuntu-24.04-arm + permissions: + contents: read + outputs: + version: ${{ steps.build.outputs.version }} + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "25" + distribution: "microsoft" + cache: "maven" + + - name: Build and validate linuxmusl-arm64 classifier + id: build + run: | + set -euo pipefail + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --volume "$HOME/.m2:/root/.m2" \ + --workdir /workspace/java \ + --env HOST_GID="$(id -g)" \ + --env HOST_UID="$(id -u)" \ + "eclipse-temurin:25-jdk-alpine" \ + sh -c "apk add --no-cache git java-cacerts maven nodejs npm && + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts && + git config --global --add safe.directory /workspace && + node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64 && + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl && + chown -R \$HOST_UID:\$HOST_GID copilot-native/target" + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar" + PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" + test -f "$JAR" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linuxmusl-arm64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linuxmusl-arm64.jar + java/copilot-native/target/linuxmusl-arm64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + retention-days: 1 + build-windows-x64-classifier: name: Build Windows x64 snapshot classifier needs: resolve-source @@ -426,6 +500,7 @@ jobs: resolve-source, build-linux-arm64-classifier, build-linuxmusl-x64-classifier, + build-linuxmusl-arm64-classifier, build-windows-x64-classifier, build-windows-arm64-classifier, build-darwin-classifier, @@ -468,6 +543,11 @@ jobs: name: java-native-linuxmusl-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/java-native-linuxmusl-x64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-linuxmusl-arm64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: java-native-win32-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} @@ -569,6 +649,31 @@ jobs: echo "linuxmusl_x64_jar=$JAR" >> "$GITHUB_OUTPUT" echo "linuxmusl_x64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Linux musl ARM64 classifier + id: linuxmusl-arm64-artifact + run: | + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION="${{ steps.linux-arm64-artifact.outputs.version }}" + if [ "$VERSION" != "${{ needs.build-linuxmusl-arm64-classifier.outputs.version }}" ]; then + echo "::error::Linux musl ARM64 classifier version does not match deploy version." + exit 1 + fi + ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linuxmusl-arm64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/linuxmusl-arm64-$VERSION.sha256" + test -f "$JAR" + test -f "$MANIFEST" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "linuxmusl_arm64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "linuxmusl_arm64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Darwin classifier id: darwin-artifact run: | @@ -650,6 +755,7 @@ jobs: mvn -B deploy -DskipTests -DskipITs -Dcopilot.native.libc=glibc \ "-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ "-Dcopilot.native.external.linuxmusl.x64.classifier.path=${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}" \ + "-Dcopilot.native.external.linuxmusl.arm64.classifier.path=${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}" \ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" \ @@ -689,6 +795,7 @@ jobs: echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |" echo "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |" echo "| \`linuxmusl-x64\` | \`Alpine x64\` | \`$(basename "${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}")\` | \`${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_sha }}\` | Published |" + echo "| \`linuxmusl-arm64\` | \`Alpine ARM64\` | \`$(basename "${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}")\` | \`${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_sha }}\` | Published |" echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" echo "| \`darwin-x64\` | \`macos-15-intel\` | \`$(basename "${{ steps.darwin-x64-artifact.outputs.darwin_x64_jar }}")\` | \`${{ steps.darwin-x64-artifact.outputs.darwin_x64_sha }}\` | Published |" diff --git a/.github/workflows/java-smoke-test.yml b/.github/workflows/java-smoke-test.yml deleted file mode 100644 index 95f662beb1..0000000000 --- a/.github/workflows/java-smoke-test.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: "Java smoke test" - -on: - workflow_dispatch: - workflow_call: - secrets: - COPILOT_GITHUB_TOKEN: - required: true - -permissions: - contents: read - -jobs: - smoke-test-jdk17: - name: Build SDK and run smoke test (JDK 17) - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 17 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "17" - distribution: "microsoft" - cache: "maven" - - - uses: ./.github/actions/setup-copilot - id: setup-copilot - - - name: Build SDK and install to local repo - run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install - - - name: Create and run smoke test via Copilot CLI - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} - run: | - cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' - You are running inside the copilot-sdk monorepo, in the java/ subdirectory. - The SDK has already been built and installed into the local Maven repository. - JDK 17 and Maven are already installed and on PATH. - - Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: - - **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. - - **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. - - Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). - - If any step fails, exit with a non-zero exit code. Do not silently fix errors. - PROMPT_EOF - - node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - - - name: Run smoke test jar - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - run: | - cd smoke-test - java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar - echo "Smoke test passed (exit code 0)" - - smoke-test-java25: - name: Build SDK and run smoke test (JDK 25) - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - cache: "maven" - - - uses: ./.github/actions/setup-copilot - id: setup-copilot - - - name: Build SDK and install to local repo - run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install - - - name: Create and run smoke test via Copilot CLI - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} - run: | - cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' - You are running inside the copilot-sdk monorepo, in the java/ subdirectory. - The SDK has already been built and installed into the local Maven repository. - JDK 25 and Maven are already installed and on PATH. - - Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: - - **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. - - **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. - - **Critical override — enable Virtual Threads for JDK 25:** After creating the Java source file from the README "Quick Start" section but BEFORE building, you must modify the source file to enable virtual thread support. The Quick Start code contains inline comments that start with `// JDK 25+:` — these are instructions. Find every such comment and follow what it says (comment out lines it says to comment out, uncomment lines it says to uncomment). Add any imports required by the newly uncommented code (e.g. `java.util.concurrent.Executors`). - Also set `maven.compiler.source` and `maven.compiler.target` to `25` in the `pom.xml`. - - Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, apply the JDK 25 virtual thread modifications described above, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). - - If any step fails, exit with a non-zero exit code. Do not silently fix errors. - PROMPT_EOF - - node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - - - name: Run smoke test jar - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - run: | - cd smoke-test - java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar - echo "Smoke test passed (exit code 0)" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 6a13c57ee2..0000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,1069 +0,0 @@ -name: Publish SDK packages - -env: - HUSKY: 0 - -on: - workflow_dispatch: - inputs: - dist-tag: - description: "Tag to publish under" - type: choice - required: true - default: "prerelease" - options: - - latest - - prerelease - - unstable - - canary - version: - description: "Version override (optional, e.g., 1.0.0). If empty, auto-increments. Unstable overrides must use -unstable." - type: string - required: false - mode: - description: "Publish or validate without registry and release mutations" - type: choice - required: true - default: publish - options: - - publish - - dry-run - runtime: - description: "Runtime metadata (automation only)" - type: string - required: false - test-policy: - description: "Runtime E2E test policy" - type: choice - required: true - default: required - options: - - required - - advisory - - skipped - -permissions: - contents: read - -concurrency: - group: ${{ inputs.mode == 'dry-run' && format('publish-dry-run-{0}', github.run_id) || inputs.runtime != '' && format('publish-runtime-{0}', github.run_id) || inputs.dist-tag == 'unstable' && 'sdk-runtime-public-unstable' || 'publish' }} - cancel-in-progress: false - -jobs: - validate-dispatch: - name: Validate dispatch - runs-on: ubuntu-latest - outputs: - kind: ${{ steps.validate.outputs.kind }} - runtime_run_id: ${{ steps.validate.outputs.runtime_run_id }} - runtime_sha: ${{ steps.validate.outputs.runtime_sha }} - runtime_version: ${{ steps.validate.outputs.runtime_version }} - test_policy: ${{ steps.validate.outputs.test_policy }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Validate release dispatch - id: validate - working-directory: ./nodejs - env: - DIST_TAG: ${{ inputs.dist-tag }} - MODE: ${{ inputs.mode }} - RUNTIME_JSON: ${{ inputs.runtime }} - TEST_POLICY: ${{ inputs.test-policy }} - VERSION_OVERRIDE: ${{ inputs.version }} - run: npx tsx scripts/runtime-release-identity.ts - - # Shared job to calculate version once for all publish jobs - version: - name: Calculate Version - needs: validate-dispatch - if: needs.validate-dispatch.outputs.kind == 'direct' - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - outputs: - version: ${{ steps.unstable_version.outputs.VERSION || steps.version.outputs.VERSION }} - current: ${{ steps.version.outputs.CURRENT }} - current-prerelease: ${{ steps.version.outputs.CURRENT_PRERELEASE }} - defaults: - run: - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: ${{ inputs.dist-tag == 'unstable' && '0' || '1' }} - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - run: npm ci --ignore-scripts - - name: Plan unstable version - if: inputs.dist-tag == 'unstable' - id: unstable_version - env: - GH_TOKEN: ${{ github.token }} - SDK_CHANNEL: unstable - SDK_SHA: ${{ github.sha }} - SDK_VERSION_OVERRIDE: ${{ inputs.version }} - WORKFLOW_RUN_ID: ${{ github.run_id }} - run: | - set -euo pipefail - WORKFLOW_CREATED_AT="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq .created_at)" - gh api --paginate "/repos/$GITHUB_REPOSITORY/releases?per_page=100" | - jq -s 'add' > "$RUNNER_TEMP/sdk-releases.json" - export SDK_RELEASES_FILE="$RUNNER_TEMP/sdk-releases.json" - export WORKFLOW_CREATED_AT - VERSION="$(npx tsx scripts/unstable-version.ts)" - npm exec -- semver "$VERSION" >/dev/null - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "Planned unstable version: $VERSION" >> "$GITHUB_STEP_SUMMARY" - - name: Get version - if: inputs.dist-tag != 'unstable' - id: version - run: | - CURRENT="$(node scripts/get-version.js current)" - echo "CURRENT=$CURRENT" >> $GITHUB_OUTPUT - echo "Current latest version: $CURRENT" >> $GITHUB_STEP_SUMMARY - CURRENT_PRERELEASE="$(node scripts/get-version.js current-prerelease)" - echo "CURRENT_PRERELEASE=$CURRENT_PRERELEASE" >> $GITHUB_OUTPUT - echo "Current prerelease version: $CURRENT_PRERELEASE" >> $GITHUB_STEP_SUMMARY - if [ -n "${{ github.event.inputs.version }}" ]; then - VERSION="${{ github.event.inputs.version }}" - # Validate version format matches dist-tag - if [ "${{ github.event.inputs.dist-tag }}" = "latest" ]; then - if [[ "$VERSION" == *-* ]]; then - echo "❌ Error: Version '$VERSION' has a prerelease suffix but dist-tag is 'latest'" >> $GITHUB_STEP_SUMMARY - echo "Use a version without suffix (e.g., '1.0.0') for latest releases" - exit 1 - fi - else - if [[ "$VERSION" != *-* ]]; then - echo "❌ Error: Version '$VERSION' has no prerelease suffix but dist-tag is '${{ github.event.inputs.dist-tag }}'" >> $GITHUB_STEP_SUMMARY - echo "Use a version with suffix (e.g., '1.0.0-preview.0') for prerelease" - exit 1 - fi - fi - echo "Using manual version override: $VERSION" >> $GITHUB_STEP_SUMMARY - else - VERSION="$(node scripts/get-version.js ${{ github.event.inputs.dist-tag }})" - echo "Auto-incremented version: $VERSION" >> $GITHUB_STEP_SUMMARY - fi - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - name: Verify version is available on public npm - if: inputs.dist-tag != 'unstable' - env: - VERSION: ${{ steps.version.outputs.VERSION }} - run: | - node scripts/npm-release.js preflight \ - @github/copilot-sdk \ - "$VERSION" \ - https://registry.npmjs.org - - package-nodejs: - name: Package Node.js SDK - needs: version - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - run: npm ci --ignore-scripts - - name: Set version - run: node scripts/set-version.js - env: - VERSION: ${{ needs.version.outputs.version }} - - name: Build - run: npm run build - - name: Pack - run: | - npm run pack:release - TARBALL_COUNT="$(find . -maxdepth 1 -name 'github-copilot-sdk-*.tgz' | wc -l | tr -d ' ')" - if [ "$TARBALL_COUNT" -ne 9 ]; then - echo "::error::Expected nine Node.js package tarballs, found $TARBALL_COUNT." - exit 1 - fi - npm run verify:release-packages - - name: Create unstable package manifest - if: inputs.dist-tag == 'unstable' - env: - SDK_VERSION: ${{ needs.version.outputs.version }} - run: npm run release:manifest -- create-package-set package-set-manifest.json . - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: nodejs-package - path: | - nodejs/github-copilot-sdk-*.tgz - nodejs/package-set-manifest.json - if-no-files-found: error - - publish-nodejs: - name: Publish Node.js SDK - needs: package-nodejs - if: inputs.mode == 'publish' && (github.ref == 'refs/heads/main' || inputs.dist-tag == 'unstable') - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - id-token: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - name: Install release dependencies - if: inputs.dist-tag == 'unstable' - working-directory: ./nodejs - run: npm ci --ignore-scripts - - name: Update npm for OIDC support - run: npm i -g "npm@11.6.3" - - name: Download Node.js package - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: nodejs-package - path: ./dist - - name: Publish tarball to public npm - env: - DIST_TAG: ${{ github.event.inputs.dist-tag }} - run: | - set -euo pipefail - if [ "$DIST_TAG" = "unstable" ]; then - node nodejs/scripts/npm-release.js publish-manifest \ - dist/package-set-manifest.json dist unstable https://registry.npmjs.org public - exit 0 - fi - shopt -s nullglob - TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 9 ]; then - echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." - exit 1 - fi - MAIN_TARBALL="" - for TARBALL in "${TARBALLS[@]}"; do - PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" - if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then - MAIN_TARBALL="$TARBALL" - continue - fi - node nodejs/scripts/npm-release.js publish \ - "$TARBALL" \ - "$DIST_TAG" \ - https://registry.npmjs.org \ - public - done - if [ -z "$MAIN_TARBALL" ]; then - echo "::error::Main @github/copilot-sdk tarball not found." - exit 1 - fi - node nodejs/scripts/npm-release.js publish \ - "$MAIN_TARBALL" \ - "$DIST_TAG" \ - https://registry.npmjs.org \ - public - - publish-nodejs-internal: - name: Publish Node.js SDK to internal feed - needs: publish-nodejs - environment: cicd - runs-on: ubuntu-latest - concurrency: - group: sdk-runtime-internal-${{ inputs.dist-tag }} - cancel-in-progress: false - queue: max - permissions: - actions: read - contents: read - id-token: write - env: - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - name: Install release dependencies - if: inputs.dist-tag == 'unstable' - working-directory: ./nodejs - run: npm ci --ignore-scripts - - name: Download Node.js package - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: nodejs-package - path: ./dist - - name: Azure Login (OIDC -> id-cpd-ci) - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - name: Configure feed auth - run: | - set -euo pipefail - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - printf '%s\n' \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > "$HOME/.npmrc" - - name: Publish tarball to internal feed - env: - DIST_TAG: ${{ github.event.inputs.dist-tag }} - run: | - set -euo pipefail - if [ "$FEED_URL" != "https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/" ]; then - echo "::error::FEED_URL ('$FEED_URL') is not the expected internal feed. Refusing to publish." - exit 1 - fi - if [ "$DIST_TAG" = "unstable" ]; then - node nodejs/scripts/npm-release.js publish-manifest \ - dist/package-set-manifest.json dist unstable "$FEED_URL" azure - exit 0 - fi - shopt -s nullglob - TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 9 ]; then - echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." - exit 1 - fi - MAIN_TARBALL="" - for TARBALL in "${TARBALLS[@]}"; do - PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" - if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then - MAIN_TARBALL="$TARBALL" - continue - fi - node nodejs/scripts/npm-release.js publish \ - "$TARBALL" \ - "$DIST_TAG" \ - "$FEED_URL" \ - azure - done - if [ -z "$MAIN_TARBALL" ]; then - echo "::error::Main @github/copilot-sdk tarball not found." - exit 1 - fi - node nodejs/scripts/npm-release.js publish \ - "$MAIN_TARBALL" \ - "$DIST_TAG" \ - "$FEED_URL" \ - azure - - publish-dotnet: - name: Publish .NET SDK - if: inputs.dist-tag != 'unstable' - needs: version - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - defaults: - run: - working-directory: ./dotnet - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - dotnet-version: "10.0.x" - - name: Restore dependencies - run: dotnet restore - - name: Build and pack - run: dotnet pack src/GitHub.Copilot.SDK.csproj -c Release -p:Version=${{ needs.version.outputs.version }} -o ./artifacts - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: dotnet-package - path: | - dotnet/artifacts/*.nupkg - dotnet/artifacts/*.snupkg - - name: NuGet login (OIDC) - if: github.ref == 'refs/heads/main' - uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 - id: nuget-login - with: - # The following must be a username, not an organization name, and that user must have configured Trusted Publishing - # for this owner/repo/workflow combination in their NuGet.org account settings. We could set up a dedicated user for - # this purpose if needed, but then we'd have to manage that account separately. Other GitHub-owned packages on NuGet - # are associated with individual maintainers' accounts too. - user: stevesanderson - - name: Publish to NuGet - if: github.ref == 'refs/heads/main' - run: | - dotnet nuget push ./artifacts/*.nupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate --no-symbols - dotnet nuget push ./artifacts/*.snupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate - - publish-dotnet-internal: - name: Publish .NET SDK to internal feed - needs: publish-dotnet - if: github.ref == 'refs/heads/main' - environment: cicd - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - id-token: write - env: - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/nuget/v3/index.json - steps: - - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - dotnet-version: "10.0.x" - - name: Download .NET package - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: dotnet-package - path: ./dist - - name: Azure Login (OIDC -> id-cpd-ci) - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - name: Publish package to internal feed - run: | - set -euo pipefail - if [ "$FEED_URL" != "https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/nuget/v3/index.json" ]; then - echo "::error::FEED_URL ('$FEED_URL') is not the expected internal feed. Refusing to publish." - exit 1 - fi - shopt -s nullglob - PACKAGES=(./dist/*.nupkg) - if [ "${#PACKAGES[@]}" -ne 1 ]; then - echo "::error::Expected one .NET package, found ${#PACKAGES[@]}." - exit 1 - fi - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - dotnet nuget add source "$FEED_URL" --name CopilotInternal - # Keep the short-lived token out of NuGet.Config and command-line arguments. - export NuGetPackageSourceCredentials_CopilotInternal="Username=azure;Password=$TOKEN;ValidAuthenticationTypes=Basic" - # Azure Artifacts does not support .snupkg symbol packages. - dotnet nuget push "${PACKAGES[0]}" \ - --api-key AzureArtifacts \ - --source CopilotInternal \ - --skip-duplicate \ - --no-symbols - - publish-rust: - name: Publish Rust SDK - if: inputs.dist-tag != 'unstable' - needs: version - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./rust - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable - with: - toolchain: "1.94.0" - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - workspaces: "rust" - - name: Set version - run: sed -i -E 's/^version = ".*"$/version = "${{ needs.version.outputs.version }}"/' Cargo.toml - - name: Snapshot CLI version + hashes for build.rs - run: | - bash scripts/snapshot-bundled-cli-version.sh - bash scripts/snapshot-bundled-in-process-version.sh - - name: Verify CLI version snapshots exist - run: | - for snapshot in cli-version.txt cli-version-in-process.txt; do - if [[ ! -f "${snapshot}" ]]; then - echo "::error::${snapshot} was not generated. The Snapshot step must run before packaging." - exit 1 - fi - done - - name: Package (dry run) - run: cargo publish --dry-run --allow-dirty - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: rust-package - path: rust/target/package/*.crate - - name: Publish to crates.io - if: github.ref == 'refs/heads/main' - run: cargo publish --allow-dirty - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - - publish-python: - name: Publish Python SDK - if: inputs.dist-tag != 'unstable' - needs: version - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - defaults: - run: - working-directory: ./python - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 - with: - python-version: "3.12" - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - - name: Set version - run: sed -i "s/^version = .*/version = \"${{ needs.version.outputs.version }}\"/" pyproject.toml - - name: Inject CLI version - run: node scripts/inject-cli-version.mjs - - name: Build wheel - run: uv build --wheel --out-dir dist - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-package - path: python/dist/* - - name: Publish to PyPI - if: github.ref == 'refs/heads/main' - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - with: - packages-dir: python/dist/ - - publish-java: - name: Publish Java SDK - if: inputs.dist-tag != 'unstable' && github.ref == 'refs/heads/main' - needs: version - permissions: - contents: read - uses: ./.github/workflows/java-publish-maven.yml - with: - releaseVersion: ${{ needs.version.outputs.version }} - sourceSha: ${{ github.sha }} - prerelease: ${{ github.event.inputs.dist-tag == 'prerelease' }} - secrets: inherit - - github-release: - name: Create GitHub Release - needs: - [ - version, - publish-nodejs, - publish-dotnet, - publish-python, - publish-rust, - publish-java, - ] - if: | - always() && - github.ref == 'refs/heads/main' && - inputs.dist-tag != 'unstable' && - needs.version.result == 'success' && - needs.publish-nodejs.result == 'success' && - needs.publish-dotnet.result == 'success' && - needs.publish-python.result == 'success' && - needs.publish-rust.result == 'success' && - needs.publish-java.outputs.mavenPublished == 'true' - runs-on: ubuntu-latest - permissions: - actions: write - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Create GitHub Release - if: github.event.inputs.dist-tag == 'latest' - run: | - NOTES_FLAG="" - if git rev-parse "v${{ needs.version.outputs.current }}" >/dev/null 2>&1; then - NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current }}" - fi - gh release create "v${{ needs.version.outputs.version }}" \ - --title "v${{ needs.version.outputs.version }}" \ - --generate-notes $NOTES_FLAG \ - --target ${{ github.sha }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create GitHub Pre-Release - if: github.event.inputs.dist-tag == 'prerelease' - run: | - NOTES_FLAG="" - if git rev-parse "v${{ needs.version.outputs.current-prerelease }}" >/dev/null 2>&1; then - NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current-prerelease }}" - fi - gh release create "v${{ needs.version.outputs.version }}" \ - --prerelease \ - --title "v${{ needs.version.outputs.version }}" \ - --generate-notes $NOTES_FLAG \ - --target ${{ github.sha }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Trigger changelog generation - run: gh workflow run release-changelog.lock.yml -f tag="v${{ needs.version.outputs.version }}" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Tag Go SDK submodule - if: github.event.inputs.dist-tag == 'latest' || github.event.inputs.dist-tag == 'prerelease' - run: | - set -e - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git fetch --tags - TAG_NAME="go/v${{ needs.version.outputs.version }}" - # Try to create the tag - will fail if it already exists - if git tag "$TAG_NAME" ${{ github.sha }} 2>/dev/null; then - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git "$TAG_NAME" - echo "Created and pushed tag $TAG_NAME" - else - echo "Tag $TAG_NAME already exists, skipping" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Tag Rust SDK - # Keep a language-scoped source tag for traceability. Rust is - # included in the cross-language `vX.Y.Z` GitHub Release. - if: github.event.inputs.dist-tag == 'latest' || github.event.inputs.dist-tag == 'prerelease' - run: | - set -e - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git fetch --tags - VERSION="${{ needs.version.outputs.version }}" - TAG_NAME="rust/v${VERSION}" - if git tag "$TAG_NAME" ${{ github.sha }} 2>/dev/null; then - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git "$TAG_NAME" - echo "Created and pushed tag $TAG_NAME" - else - echo "Tag $TAG_NAME already exists, skipping tag push" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - tag-java: - name: Tag Java SDK - needs: [version, publish-java, github-release] - if: | - success() && - (github.event.inputs.dist-tag == 'latest' || - github.event.inputs.dist-tag == 'prerelease') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ needs.publish-java.outputs.sourceSha }} - fetch-depth: 0 - - - name: Tag Java SDK - # Reuse a tag only when it identifies the source that was published. - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git fetch --tags origin - TAG_NAME="java/v${VERSION}" - if git show-ref --verify --quiet "refs/tags/$TAG_NAME"; then - TAG_COMMIT=$(git rev-parse --verify "refs/tags/${TAG_NAME}^{commit}") - if [ "$TAG_COMMIT" != "$SOURCE_SHA" ]; then - echo "::error::Tag $TAG_NAME points to $TAG_COMMIT, expected $SOURCE_SHA. Refusing to overwrite it." - exit 1 - fi - echo "Tag $TAG_NAME already points to the release source, skipping tag push" - else - STATUS=$? - if [ "$STATUS" -ne 1 ]; then - echo "::error::Could not inspect tag $TAG_NAME." - exit "$STATUS" - fi - git tag "$TAG_NAME" "$SOURCE_SHA" - git push origin "refs/tags/$TAG_NAME" - echo "Created and pushed tag $TAG_NAME" - fi - env: - VERSION: ${{ needs.version.outputs.version }} - SOURCE_SHA: ${{ needs.publish-java.outputs.sourceSha }} - - deploy-java-site: - name: Deploy Java documentation site - needs: [version, tag-java] - runs-on: ubuntu-latest - permissions: {} - steps: - - name: Trigger Java documentation site deploy - # A failed dispatch can be retried without recreating the GitHub release. - run: | - set -euo pipefail - TAG="java/v${VERSION}" - PUBLISH_AS_LATEST=true - if [ "$DIST_TAG" = "prerelease" ]; then - PUBLISH_AS_LATEST=false - fi - echo "Triggering site deployment for version ${VERSION} (tag: ${TAG})" - gh workflow run deploy-site.yml \ - --repo github/copilot-sdk-java \ - -f version="${VERSION}" \ - -f publish_as_latest="${PUBLISH_AS_LATEST}" \ - -f monorepo_tag="${TAG}" - env: - VERSION: ${{ needs.version.outputs.version }} - DIST_TAG: ${{ github.event.inputs.dist-tag }} - GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_GITHUB_TOKEN }} - runtime-plan: - name: Plan runtime release - needs: validate-dispatch - if: needs.validate-dispatch.outputs.kind == 'runtime' - runs-on: ubuntu-latest - environment: cicd - permissions: - actions: read - contents: read - outputs: - artifact_name: ${{ steps.plan.outputs.artifact_name }} - sdk_version: ${{ steps.plan.outputs.sdk_version }} - workflow_created_at: ${{ steps.plan.outputs.workflow_created_at }} - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Calculate the release identity - id: plan - working-directory: ./nodejs - env: - CHANNEL: ${{ inputs.dist-tag }} - GH_TOKEN: ${{ github.token }} - SDK_CHANNEL: ${{ inputs.dist-tag }} - SDK_SHA: ${{ github.sha }} - WORKFLOW_RUN_ID: ${{ github.run_id }} - WORKFLOW_RUN_NUMBER: ${{ github.run_number }} - run: | - set -euo pipefail - WORKFLOW_CREATED_AT="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq .created_at)" - gh api --paginate "/repos/$GITHUB_REPOSITORY/releases?per_page=100" | - jq -s 'add' > "$RUNNER_TEMP/sdk-releases.json" - export SDK_RELEASES_FILE="$RUNNER_TEMP/sdk-releases.json" - export WORKFLOW_CREATED_AT - SDK_VERSION="$(npx tsx scripts/unstable-version.ts)" - npm exec -- semver "$SDK_VERSION" >/dev/null - ARTIFACT_NAME="nodejs-${CHANNEL}-${SDK_VERSION}" - echo "Runtime E2E test policy: ${{ needs.validate-dispatch.outputs.test_policy }}" >> "$GITHUB_STEP_SUMMARY" - { - echo "artifact_name=$ARTIFACT_NAME" - echo "sdk_version=$SDK_VERSION" - echo "workflow_created_at=$WORKFLOW_CREATED_AT" - } >> "$GITHUB_OUTPUT" - - runtime-acquire: - name: Acquire runtime - needs: [validate-dispatch, runtime-plan] - runs-on: ubuntu-latest - environment: cicd - permissions: - contents: read - packages: read - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - - name: Configure authentication-only GitHub Packages access - env: - NODE_AUTH_TOKEN: ${{ github.token }} - run: echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > "$HOME/.npmrc" - - name: Download and validate all runtime platforms - env: - NODE_AUTH_TOKEN: ${{ github.token }} - RUNTIME_SHA: ${{ needs.validate-dispatch.outputs.runtime_sha }} - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - run: | - npm run acquire:runtime-packages -- \ - --version "$RUNTIME_VERSION" \ - --sha "$RUNTIME_SHA" \ - --output "$RUNNER_TEMP/runtime-packages" - - name: Archive validated runtime packages - run: tar -czf "$RUNNER_TEMP/runtime-packages.tar.gz" --exclude "runtime-packages/tarballs" -C "$RUNNER_TEMP" runtime-packages - - name: Upload validated runtime packages - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }} - path: ${{ runner.temp }}/runtime-packages.tar.gz - if-no-files-found: error - retention-days: 7 - - runtime-test: - name: Test runtime (${{ matrix.os }}, ${{ matrix.transport }}) - needs: [validate-dispatch, runtime-plan, runtime-acquire] - if: needs.validate-dispatch.outputs.test_policy != 'skipped' - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - transport: ["default", "inprocess"] - runs-on: ${{ matrix.os }} - environment: cicd - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - - name: Install test harness dependencies - working-directory: ./test/harness - run: npm ci --ignore-scripts - - name: Download validated runtime packages - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }} - path: ${{ runner.temp }}/runtime-package-artifact - - name: Extract validated runtime packages - run: | - runner_temp="$RUNNER_TEMP" - if command -v cygpath >/dev/null 2>&1; then - runner_temp="$(cygpath -u "$runner_temp")" - fi - rm -rf "$runner_temp/runtime-packages" - tar -xzf "$runner_temp/runtime-package-artifact/runtime-packages.tar.gz" -C "$runner_temp" - - name: Select the acquired runtime - env: - COPILOT_SDK_RUNTIME_PACKAGE_DIR: ${{ runner.temp }}/runtime-packages - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - run: | - node scripts/set-cli-version.js "$RUNTIME_VERSION" --local-package - runtime_path="$(npm run --silent prepare:runtime -- --print-path)" - echo "COPILOT_SDK_RUNTIME_PACKAGE_DIR=$COPILOT_SDK_RUNTIME_PACKAGE_DIR" >> "$GITHUB_ENV" - echo "COPILOT_CLI_PATH=$runtime_path" >> "$GITHUB_ENV" - - run: npm run build - - name: Warm up PowerShell - if: runner.os == 'Windows' - run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - name: Select inprocess transport - if: matrix.transport == 'inprocess' - run: echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV" - - name: Run Node SDK tests - id: e2e - continue-on-error: ${{ needs.validate-dispatch.outputs.test_policy == 'advisory' }} - env: - COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - run: npm test - - name: Report advisory E2E failure - if: needs.validate-dispatch.outputs.test_policy == 'advisory' && steps.e2e.outcome == 'failure' - env: - RUNNER_OS: ${{ runner.os }} - run: | - echo "::warning::Runtime-backed Node SDK E2E tests failed on ${RUNNER_OS}; continuing because test-policy is advisory." - { - echo "### Advisory runtime E2E failure" - echo - echo "Runtime-backed Node SDK E2E tests failed on **${RUNNER_OS}**. Publication remains eligible because \`test-policy\` is \`advisory\`." - } >> "$GITHUB_STEP_SUMMARY" - - runtime-package: - name: Build SDK packages - needs: [validate-dispatch, runtime-plan, runtime-acquire, runtime-test] - if: | - always() && - !cancelled() && - needs.validate-dispatch.result == 'success' && - needs.runtime-plan.result == 'success' && - needs.runtime-acquire.result == 'success' && - ( - needs.runtime-test.result == 'success' || - (needs.validate-dispatch.outputs.test_policy == 'skipped' && needs.runtime-test.result == 'skipped') - ) - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - - name: Download validated runtime packages - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }} - path: ${{ runner.temp }}/runtime-package-artifact - - name: Extract validated runtime packages - run: | - runner_temp="$RUNNER_TEMP" - if command -v cygpath >/dev/null 2>&1; then - runner_temp="$(cygpath -u "$runner_temp")" - fi - rm -rf "$runner_temp/runtime-packages" - tar -xzf "$runner_temp/runtime-package-artifact/runtime-packages.tar.gz" -C "$runner_temp" - - name: Build and verify exact package set - env: - COPILOT_SDK_RUNTIME_PACKAGE_DIR: ${{ runner.temp }}/runtime-packages - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - SDK_VERSION: ${{ needs.runtime-plan.outputs.sdk_version }} - run: | - VERSION="$SDK_VERSION" node scripts/set-version.js - node scripts/set-cli-version.js "$RUNTIME_VERSION" --local-package - grep -F "COPILOT_CLI_USE_NPM_PACKAGE = false" src/cliVersion.ts - npm run build - npm run pack:release - npm run verify:release-packages - - name: Create immutable release manifest - env: - RELEASE_CHANNEL: ${{ inputs.dist-tag }} - RUNTIME_RUN_ID: ${{ needs.validate-dispatch.outputs.runtime_run_id }} - RUNTIME_SHA: ${{ needs.validate-dispatch.outputs.runtime_sha }} - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - SDK_REF: ${{ github.ref }} - SDK_SHA: ${{ github.sha }} - SDK_VERSION: ${{ needs.runtime-plan.outputs.sdk_version }} - TEST_POLICY: ${{ needs.validate-dispatch.outputs.test_policy }} - WORKFLOW_CREATED_AT: ${{ needs.runtime-plan.outputs.workflow_created_at }} - WORKFLOW_RUN_ID: ${{ github.run_id }} - WORKFLOW_RUN_NUMBER: ${{ github.run_number }} - run: | - npm run release:manifest -- create release-manifest.json . - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ needs.runtime-plan.outputs.artifact_name }} - path: | - nodejs/release-manifest.json - nodejs/github-copilot-sdk-*.tgz - if-no-files-found: error - retention-days: 30 - - runtime-publish-internal: - name: Publish SDK internally - if: | - always() && - !cancelled() && - inputs.mode == 'publish' && - needs.runtime-plan.result == 'success' && - needs.runtime-package.result == 'success' - needs: [validate-dispatch, runtime-plan, runtime-package] - runs-on: ubuntu-latest - concurrency: - group: sdk-runtime-internal-${{ inputs.dist-tag }} - cancel-in-progress: false - queue: max - environment: cicd - permissions: - actions: read - contents: read - id-token: write - env: - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: 22 - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Download retained release - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: ${{ needs.runtime-plan.outputs.artifact_name }} - path: ./dist - - name: Validate retained release - run: | - node nodejs/node_modules/.bin/tsx nodejs/scripts/release-manifest.ts verify dist/release-manifest.json dist - [ "$(jq -r .workflow.runId dist/release-manifest.json)" = "${{ github.run_id }}" ] || - { echo "::error::Retained release belongs to a different workflow run."; exit 1; } - [ "$(jq -r .channel dist/release-manifest.json)" = "${{ inputs.dist-tag }}" ] || - { echo "::error::Retained release channel does not match the requested channel."; exit 1; } - - name: Azure login - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - allow-no-subscriptions: true - client-id: ${{ vars.CPD_ID_CLIENT_ID }} - tenant-id: ${{ vars.CPD_ID_TENANT_ID }} - - name: Configure authentication-only Azure npm access - run: | - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - printf '%s\n' \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > "$HOME/.npmrc" - - name: Publish exact tarballs internally - run: | - node nodejs/scripts/npm-release.js publish-manifest \ - dist/release-manifest.json dist "${{ inputs.dist-tag }}" "$FEED_URL" azure - - runtime-publish-public: - name: Publish SDK publicly - if: | - always() && - !cancelled() && - inputs.dist-tag == 'unstable' && - inputs.mode == 'publish' && - needs.runtime-plan.result == 'success' && - needs.runtime-publish-internal.result == 'success' - needs: [runtime-plan, runtime-publish-internal] - runs-on: ubuntu-latest - concurrency: - group: sdk-runtime-public-unstable - cancel-in-progress: false - queue: max - permissions: - actions: read - contents: read - id-token: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: 22 - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Update npm for trusted publishing - run: npm install -g npm@11.6.3 - - name: Download retained release - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: ${{ needs.runtime-plan.outputs.artifact_name }} - path: ./dist - - name: Validate retained release - run: | - node nodejs/node_modules/.bin/tsx nodejs/scripts/release-manifest.ts verify \ - dist/release-manifest.json dist - - name: Publish the same tarballs to public npm - run: | - node nodejs/scripts/npm-release.js publish-manifest \ - dist/release-manifest.json dist unstable https://registry.npmjs.org public diff --git a/.github/workflows/release-changelog.lock.yml b/.github/workflows/release-changelog.lock.yml index c66be2f35b..35e1ada2a3 100644 --- a/.github/workflows/release-changelog.lock.yml +++ b/.github/workflows/release-changelog.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6abb56e7d33f12df48df547a0f4143936049b45de28d4f1ff92bdc8020aa57c0","body_hash":"264021ebf1d0bc74660b753fafc82d43a8ef6c34311da38d757b91b959f8481b","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6abb56e7d33f12df48df547a0f4143936049b45de28d4f1ff92bdc8020aa57c0","body_hash":"74115979906c43791724a7a021f11f23493d9eb4e4867530ecd03555a0ef29d9","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["missing_data","missing_tool","noop","update_release"]}]} # This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # diff --git a/.github/workflows/release-changelog.md b/.github/workflows/release-changelog.md index 6729eef47c..eb445e5dbc 100644 --- a/.github/workflows/release-changelog.md +++ b/.github/workflows/release-changelog.md @@ -37,6 +37,8 @@ Determine which type of release this is by inspecting the tag or fetching the re Use the GitHub API to fetch the release corresponding to `${{ github.event.inputs.tag }}` to get its name, publish date, prerelease status, and other metadata. +Use only this public SDK repository's source, history, releases, and pull requests. Do not retrieve or link private runtime source/history. If the target is not a published SDK `v` release, stop without updating anything; `runtime-*` releases contain acquisition assets, not SDK release notes. + ## Your Task ### Step 1: Identify the version range @@ -49,6 +51,7 @@ Use the GitHub API to fetch the release corresponding to `${{ github.event.input 2. The **new version** is the release tag: `${{ github.event.inputs.tag }}` 3. Fetch the release metadata to determine if this is a **stable** or **prerelease** release. 4. Determine the **previous version** to diff against: + - Consider only published, non-draft SDK `v` releases whose publication time precedes the target. Exclude `runtime-*` releases, even for prerelease comparisons. - **For stable releases**: list releases via the API and find the previous **stable** release (skip prereleases and the current release). Do not use the frozen `CHANGELOG.md` as the version baseline. Stable release notes include ALL changes since the last stable release, even if some were already mentioned in prerelease notes. - **For prerelease releases**: find the most recent release of **any kind** (stable or prerelease) that precedes this one. This way prerelease notes only cover what's new since the last release. 5. If no previous release exists at all, use the first commit in the repo as the starting point. diff --git a/.github/workflows/sdk-consistency-review.lock.yml b/.github/workflows/sdk-consistency-review.lock.yml deleted file mode 100644 index 867cca9145..0000000000 --- a/.github/workflows/sdk-consistency-review.lock.yml +++ /dev/null @@ -1,1850 +0,0 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"460e0bbc747eb7d0e062a8982fde1107719822da18b787fb1ffa16d1aac12798","body_hash":"e3a2e1a79536caf26d05fbeca8ef0c9dcf43a1279504e508823b071f5750962c","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","detection_agent_id":"copilot","detection_agent_model":"${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","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"}],"has_pull_request":true,"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":["add_comment","create_pull_request_review_comment","missing_data","missing_tool","noop"]}]} -# 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 -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# Reviews PRs to ensure features are implemented consistently across all SDK language implementations -# -# Secrets used: -# - GH_AW_DEFAULT_OTLP_HEADERS -# - GH_AW_GITHUB_MCP_SERVER_TOKEN -# - GH_AW_GITHUB_TOKEN -# - GITHUB_TOKEN -# -# Custom actions used: -# - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 -# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 -# -# Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 -# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f -# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e -# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e -# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 - -name: "SDK Consistency Review Agent" -on: - pull_request: - paths: - - nodejs/** - - python/** - - go/** - - dotnet/** - - java/** - - rust/** - - "!java/docs/**" - - "!java/*.txt" - - "!java/*.md" - types: - - opened - - synchronize - - reopened - - ready_for_review - # roles: all # Roles processed as role check in pre-activation job - workflow_dispatch: - inputs: - aw_context: - default: "" - description: "Agent caller context (used internally by Agentic Workflows)." - required: false - type: string - pr_number: - description: PR number to review - required: true - type: string - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}" - cancel-in-progress: true - -run-name: "SDK Consistency Review Agent" - -env: - OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} - OTEL_SERVICE_NAME: gh-aw.sdk-consistency-review - OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=SDK%20Consistency%20Review%20Agent,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' - OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} - GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' - GH_AW_OTLP_IF_MISSING: ignore - -jobs: - activation: - if: > - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id) && - ((github.event_name != 'pull_request' && github.event_name != 'pull_request_review') || github.event.pull_request.stack == null || - github.event.pull_request.stack.position == github.event.pull_request.stack.size) - runs-on: ubuntu-slim - permissions: - actions: read - contents: read - env: - GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - outputs: - body: ${{ steps.sanitized.outputs.body }} - comment_id: "" - comment_repo: "" - daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} - daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} - daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} - daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} - engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} - lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} - model: ${{ steps.generate_aw_info.outputs.model }} - oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} - setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} - setup-span-id: ${{ steps.setup.outputs.span-id }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} - stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} - text: ${{ steps.sanitized.outputs.text }} - title: ${{ steps.sanitized.outputs.title }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.80" - GH_AW_INFO_AWF_VERSION: "v0.28.12" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Mask OTLP telemetry headers - run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - - name: Generate agentic run info - id: generate_aw_info - env: - GH_AW_INFO_ENGINE_ID: "copilot" - GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" - GH_AW_INFO_VERSION: "1.0.80" - GH_AW_INFO_AGENT_VERSION: "1.0.80" - GH_AW_INFO_CLI_VERSION: "v0.88.2" - GH_AW_INFO_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_INFO_EXPERIMENTAL: "false" - GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" - GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' - GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.28.12" - GH_AW_INFO_AWMG_VERSION: "" - GH_AW_INFO_FIREWALL_TYPE: "squid" - GH_AW_INFO_AGENT_RUNTIME: "" - GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); - await main(core, context); - - name: Restore daily AIC usage cache - id: restore-daily-aic-cache - if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} - continue-on-error: true - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: agentic-workflow-usage-sdkconsistencyreview-${{ github.run_id }} - restore-keys: agentic-workflow-usage-sdkconsistencyreview- - path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl - - name: Restore daily AIC usage cache (artifact fallback) - id: restore-daily-aic-cache-fallback - if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} - GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); - await main(); - - name: Check daily workflow token guardrail - id: daily-effective-workflow-guardrail - if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_ID: "sdk-consistency-review" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} - GH_AW_HAS_SLASH_COMMAND: "false" - GH_AW_HAS_LABEL_COMMAND: "false" - GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); - await main(); - - name: Check for OAuth tokens - id: check-oauth-tokens - run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" - env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - - name: Checkout .github and .agents folders - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: | - .github - .agents - .claude - .codex - .gemini - .pi - sparse-checkout-cone-mode: true - fetch-depth: 1 - - name: Save agent config folders for base branch restoration - env: - GH_AW_AGENT_FOLDERS: ".agents .github" - GH_AW_AGENT_FILES: "AGENTS.md" - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - - name: Check workflow lock file - id: check-lock-file - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_WORKFLOW_FILE: "sdk-consistency-review.lock.yml" - GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); - await main(); - - name: Check compile-agentic version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_COMPILED_VERSION: "v0.88.2" - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); - await main(); - - name: Compute current body text - id: sanitized - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'compute_text.cjs')); - await main(); - - name: Log runtime features - if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} - run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - - name: Create prompt with built-in context - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions - GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl - GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" - GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_A0E5D436: ${{ github.event.pull_request.number || inputs.pr_number }} - GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - GH_AW_PROMPT_CONTENT_0000: "\n" - GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, create_pull_request_review_comment(max:10), missing_tool, missing_data, noop\n" - GH_AW_PROMPT_CONTENT_0002: "\n" - GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" - GH_AW_PROMPT_CONTENT_0004: "\n" - GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/sdk-consistency-review.md}}\n" - with: - script: | - const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); - await main(core); - - name: Interpolate variables and render templates - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt - GH_AW_ENGINE_ID: "copilot" - GH_AW_EXPR_A0E5D436: ${{ github.event.pull_request.number || inputs.pr_number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); - await main(); - - name: Substitute placeholders - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt - GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_A0E5D436: ${{ github.event.pull_request.number || inputs.pr_number }} - GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - - const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, - GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, - GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, - GH_AW_EXPR_A0E5D436: process.env.GH_AW_EXPR_A0E5D436, - GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_PR_NUMBER: process.env.GH_AW_INPUTS_PR_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - - name: Print prompt - env: - GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - - name: Stage prompt files for artifact upload - run: | - mkdir -p /tmp/gh-aw/aw-prompts - cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - - name: Upload activation artifact - if: success() || failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: activation - include-hidden-files: true - path: | - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/models.json - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw-prompts/prompt-template.txt - /tmp/gh-aw/aw-prompts/prompt-import-tree.json - /tmp/gh-aw/github_rate_limits.jsonl - /tmp/gh-aw/base - /tmp/gh-aw/.github/agents - /tmp/gh-aw/.github/skills - if-no-files-found: ignore - retention-days: 1 - - agent: - needs: activation - if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' - runs-on: ubuntu-latest - permissions: - contents: read - copilot-requests: write - issues: read - pull-requests: read - timeout-minutes: 60 - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - GH_AW_ASSETS_ALLOWED_EXTS: "" - GH_AW_ASSETS_BRANCH: "" - GH_AW_ASSETS_MAX_SIZE_KB: 0 - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_PR_HEAD_BASE_BRANCH: "" - GH_AW_PR_HEAD_BASE_PR_NUMBER: "" - GH_AW_PR_HEAD_BASE_REF: "" - GH_AW_PR_HEAD_BASE_REPO: "" - GH_AW_PR_HEAD_BASE_SHA: "" - GH_AW_PR_HEAD_REPO: "" - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - GH_AW_WORKFLOW_ID_SANITIZED: sdkconsistencyreview - outputs: - agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} - ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} - aic: ${{ steps.parse-mcp-gateway.outputs.aic }} - ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - has_patch: ${{ steps.collect_output.outputs.has_patch }} - http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} - inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} - invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} - max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} - mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} - missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} - missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} - model: ${{ needs.activation.outputs.model }} - model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} - output: ${{ steps.collect_output.outputs.output }} - output_types: ${{ steps.collect_output.outputs.output_types }} - setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} - setup-span-id: ${{ steps.setup.outputs.span-id }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} - shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} - unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.80" - GH_AW_INFO_AWF_VERSION: "v0.28.12" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Set runtime paths - id: set-runtime-paths - env: - GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} - run: | - if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then - echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" - fi - { - echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" - echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" - echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" - } >> "$GITHUB_OUTPUT" - - name: Mask OTLP telemetry headers - run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - - name: Check OTLP telemetry configuration - run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Create gh-aw temp directory - run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - - name: Configure gh CLI for GitHub Enterprise - run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" - env: - GH_TOKEN: ${{ github.token }} - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - - name: Configure Git credentials - env: - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_TOKEN: ${{ github.token }} - run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); - await main(); - - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" - env: - GH_HOST: github.com - GH_AW_COMPILED_VERSION: v0.88.2 - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 - env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); - await determineAutomaticLockdown(github, context, core); - - name: Restore agent config folders from base branch - if: steps.checkout-pr.outcome == 'success' - env: - GH_AW_AGENT_FOLDERS: ".agents .github" - GH_AW_AGENT_FILES: "AGENTS.md" - run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - - name: Restore inline sub-agents from activation artifact - env: - GH_AW_SUB_AGENT_DIR: ".github/agents" - GH_AW_SUB_AGENT_EXT: ".agent.md" - run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" - - name: Restore inline skills from activation artifact - env: - GH_AW_SKILL_DIR: ".github/skills" - run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 - - name: Prepare Safe Outputs Directories - run: | - mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" - mkdir -p /tmp/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - - name: Generate Safe Outputs Config - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" - GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'create_files.cjs')); - await main(); - - name: Generate Safe Outputs Tools - env: - GH_AW_TOOLS_META_JSON: | - { - "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Supports reply_to_id for discussion threading.", - "create_pull_request_review_comment": " CONSTRAINTS: Maximum 10 review comment(s) can be created. Comments will be on the RIGHT side of the diff." - }, - "repo_params": {}, - "dynamic_tools": [] - } - GH_AW_VALIDATION_JSON: | - { - "add_comment": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "comment_id": { - "optionalPositiveInteger": true - }, - "item_number": { - "issueOrPRNumber": true - }, - "pr": { - "issueOrPRNumber": true - }, - "pr_number": { - "issueOrPRNumber": true - }, - "reply_to_id": { - "type": "string", - "maxLength": 256 - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "target": { - "type": "string", - "enum": [ - "status" - ] - }, - "temporary_id": { - "type": "string", - "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" - } - } - }, - "create_pull_request_review_comment": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "line": { - "required": true, - "positiveInteger": true - }, - "path": { - "required": true, - "type": "string" - }, - "pull_request_number": { - "optionalPositiveInteger": true - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "side": { - "type": "string", - "enum": [ - "LEFT", - "RIGHT" - ] - }, - "start_line": { - "optionalPositiveInteger": true - } - }, - "customValidation": "startLineLessOrEqualLine" - }, - "missing_data": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "context": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "data_type": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "reason": { - "type": "string", - "sanitize": true, - "maxLength": 256 - } - } - }, - "missing_tool": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 512 - }, - "reason": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "tool": { - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "noop": { - "defaultMax": 1, - "fields": { - "message": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - } - } - }, - "report_incomplete": { - "defaultMax": 5, - "fields": { - "details": { - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "reason": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 1024 - } - } - } - } - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); - await main(); - - name: Start MCP Gateway - id: start-mcp-gateway - env: - GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} - GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then - GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" - cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" - export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" - fi - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="awmg-mcpg" - export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" - export MCP_GATEWAY_AGENT_ID - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" - export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" - export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" - export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" - export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" - export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" - export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" - export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" - export DEBUG="*" - - export GH_AW_ENGINE="copilot" - MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') - MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' - - mkdir -p "$HOME/.copilot" - GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_46604863f3d8e286_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" - { - "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.11.0", - "env": { - "GITHUB_FEATURES": "fields_param", - "GITHUB_HOST": "${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" - }, - "guard-policies": { - "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" - } - } - }, - "safeoutputs": { - "type": "stdio", - "container": "ghcr.io/github/gh-aw-node", - "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], - "args": ["-w", "\${GITHUB_WORKSPACE}"], - "entrypoint": "sh", - "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], - "env": { - "DEBUG": "*", - "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", - "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", - "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", - "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", - "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", - "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", - "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", - "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", - "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", - "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", - "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", - "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", - "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", - "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", - "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", - "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", - "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", - "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", - "GITHUB_SHA": "\${GITHUB_SHA}", - "GITHUB_TOKEN": "\${GITHUB_TOKEN}", - "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", - "RUNNER_TEMP": "\${RUNNER_TEMP}" - }, - "guard-policies": { - "write-sink": { - "accept": [ - "*" - ], - "sink-visibility": "${GH_AW_SINK_VISIBILITY}" - } - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "agentId": "${MCP_GATEWAY_AGENT_ID}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120, - "opentelemetry": { - "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", - "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", - "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" - } - } - } - GH_AW_MCP_CONFIG_46604863f3d8e286_EOF - - name: Mount MCP servers as CLIs - id: mount-mcp-clis - continue-on-error: true - env: - MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} - MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io); - const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); - await main(); - - name: Clean credentials - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" - - name: Audit pre-agent workspace - id: pre_agent_audit - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 15 - run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" - GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" - if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then - echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 - exit 127 - fi - GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" - mkdir -p "${RUNNER_TEMP}/gh-aw/bin" - if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then - cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" - fi - chmod 755 "$GH_AW_COPILOT_BIN" - - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then - GH_AW_MAX_AI_CREDITS="1000" - fi - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - GH_AW_AWF_ENGINE_NAME=copilot \ - GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ - GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ - GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ - bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' - env: - AWF_REFLECT_ENABLED: 1 - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} - GH_AW_LLM_PROVIDER: github - GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} - GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} - GH_AW_PHASE: agent - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_TIMEOUT_MINUTES: 15 - GH_AW_VERSION: v0.88.2 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_AUTHOR_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - RUNNER_TEMP: ${{ runner.temp }} - S2STOKENS: true - TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Detect agent errors - if: always() - id: detect-agent-errors - continue-on-error: true - env: - GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} - GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 15 - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); - await main(); - - name: Configure Git credentials - env: - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_TOKEN: ${{ github.token }} - run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - - name: Copy Copilot session state files to logs - if: always() - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" - - name: Stop MCP Gateway - if: always() - continue-on-error: true - env: - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} - GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); - await main(); - env: - GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Append agent step summary - if: always() - run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" - - name: Copy Safe Outputs - if: always() - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - run: | - mkdir -p /tmp/gh-aw - cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true - - name: Ingest agent output - id: collect_output - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); - await main(); - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); - await main(); - - name: Parse MCP Gateway logs for step summary - if: always() - id: parse-mcp-gateway - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - - name: Parse token usage for step summary - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); - await main(); - - name: Print AWF reflect summary - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); - await main(); - - name: Generate observability summary - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); - await main(core); - - name: Write agent output placeholder if missing - if: always() - run: | - if [ ! -f /tmp/gh-aw/agent_output.json ]; then - echo '{"items":[]}' > /tmp/gh-aw/agent_output.json - fi - # Small dedicated copy of the agent output so safe-output processing - # survives a failed or timed-out upload of the larger agent artifact - - name: Upload agent output fallback artifact - if: always() - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: agent-output-fallback - path: | - /tmp/gh-aw/agent_output.json - /tmp/gh-aw/safeoutputs.jsonl - if-no-files-found: ignore - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: agent - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/agent_usage.json - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/pre-agent-audit.txt - /tmp/gh-aw/agent/ - /tmp/gh-aw/github_rate_limits.jsonl - /tmp/gh-aw/otel.jsonl - /tmp/gh-aw/otlp-export-errors.jsonl - /tmp/gh-aw/safeoutputs.jsonl - /tmp/gh-aw/agent_output.json - /tmp/gh-aw/aw-*.patch - /tmp/gh-aw/aw-*.bundle - /tmp/gh-aw/awf-config.json - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/sandbox/firewall/audit/ - /tmp/gh-aw/sandbox/firewall/awf-reflect.json - if-no-files-found: ignore - - conclusion: - needs: - - activation - - agent - - detection - - safe_outputs - if: > - always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.daily_ai_credits_exceeded == 'true') - runs-on: ubuntu-slim - permissions: - actions: read - issues: write - pull-requests: write - concurrency: - group: "gh-aw-conclusion-sdk-consistency-review" - cancel-in-progress: false - queue: max - env: - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - outputs: - incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} - noop_message: ${{ steps.noop.outputs.noop_message }} - tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} - total_count: ${{ steps.missing_tool.outputs.total_count }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.80" - GH_AW_INFO_AWF_VERSION: "v0.28.12" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: "{agent,agent-output-fallback}" - merge-multiple: true - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - if [ -f "/tmp/gh-aw/agent_output.json" ]; then - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - fi - - name: Download detection artifact - id: download-detection-artifact - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: detection - path: /tmp/gh-aw/threat-detection/ - - name: Download Safe Outputs Items Manifest - id: download-safe-outputs-manifest - if: always() - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: safe-outputs-items - merge-multiple: true - path: /tmp/gh-aw/ - - name: Collect usage artifact files - if: always() - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - - name: Upload usage artifact - if: always() - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: usage - path: | - /tmp/gh-aw/usage/aw_info.json - /tmp/gh-aw/usage/aw-info.jsonl - /tmp/gh-aw/usage/agent_usage.json - /tmp/gh-aw/usage/agent_usage.jsonl - /tmp/gh-aw/usage/detection_usage.jsonl - /tmp/gh-aw/usage/evals.jsonl - /tmp/gh-aw/usage/graders/grader_manifest.json - /tmp/gh-aw/usage/graders/grader_results.json - /tmp/gh-aw/usage/github_rate_limits.jsonl - /tmp/gh-aw/usage/agent/token_usage.jsonl - /tmp/gh-aw/usage/detection/token_usage.jsonl - /tmp/gh-aw/usage/activity/summary.json - if-no-files-found: ignore - - name: Restore daily AIC usage cache - id: restore-daily-aic-cache-conclusion - if: always() - continue-on-error: true - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: agentic-workflow-usage-sdkconsistencyreview-${{ github.run_id }} - restore-keys: agentic-workflow-usage-sdkconsistencyreview- - path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl - - name: Write daily AIC usage cache entry - id: write-daily-aic-cache - if: always() - continue-on-error: true - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 - with: - github-token: ${{ github.token }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context); - const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); - await main(); - - name: Save daily AIC usage cache - id: save-daily-aic-cache - if: always() - continue-on-error: true - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: agentic-workflow-usage-sdkconsistencyreview-${{ github.run_id }} - path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl - - name: Upload daily AIC usage cache artifact - id: upload-daily-aic-cache - if: always() - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: aic-usage-cache - path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl - if-no-files-found: ignore - retention-days: 7 - - name: Process no-op messages - id: noop - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" - GH_AW_TRACKER_ID: "sdk-consistency-review" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "false" - GH_AW_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} - GH_AW_WORKFLOW_ID: "sdk-consistency-review" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); - await main(); - - name: Log detection run - id: detection_runs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" - GH_AW_TRACKER_ID: "sdk-consistency-review" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} - GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); - await main(); - - name: Record missing tool - id: missing_tool - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" - GH_AW_TRACKER_ID: "sdk-consistency-review" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); - await main(); - - name: Record incomplete - id: report_incomplete - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" - GH_AW_TRACKER_ID: "sdk-consistency-review" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); - await main(); - - name: Handle agent failure - id: handle_agent_failure - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" - GH_AW_TRACKER_ID: "sdk-consistency-review" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_WORKFLOW_ID: "sdk-consistency-review" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" - GH_AW_ENGINE_ID: "copilot" - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} - GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} - GH_AW_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} - GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} - GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} - GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} - GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} - GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} - GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} - GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} - GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} - GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} - GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" - GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} - GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} - GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} - GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} - GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} - GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} - GH_AW_GROUP_REPORTS: "false" - GH_AW_FAILURE_REPORT_AS_ISSUE: "true" - GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" - GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "15" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); - await main(); - - name: Report failed jobs - id: report_failed_jobs - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" - GH_AW_TRACKER_ID: "sdk-consistency-review" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_REPORT_FAILED_JOBS: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); - await main(); - - detection: - needs: - - activation - - agent - if: always() && needs.agent.result != 'skipped' - runs-on: ubuntu-latest - permissions: - contents: read - copilot-requests: write - timeout-minutes: 10 - env: - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - outputs: - aic: ${{ steps.parse_detection_token_usage.outputs.aic }} - detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} - detection_reason: ${{ steps.detection_conclusion.outputs.reason }} - detection_success: ${{ steps.detection_conclusion.outputs.success }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.80" - GH_AW_INFO_AWF_VERSION: "v0.28.12" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Download activation artifact - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: "{agent,agent-output-fallback}" - merge-multiple: true - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - if [ -f "/tmp/gh-aw/agent_output.json" ]; then - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - fi - - name: Checkout repository for patch context - if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - # --- Threat Detection --- - - name: Clean stale firewall files from agent artifact - run: | - rm -rf /tmp/gh-aw/sandbox/firewall/logs - rm -rf /tmp/gh-aw/sandbox/firewall/audit - - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - - name: Check if detection needed - id: detection_guard - if: always() - env: - OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - run: | - if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then - echo "run_detection=true" >> "$GITHUB_OUTPUT" - echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" - else - echo "run_detection=false" >> "$GITHUB_OUTPUT" - echo "Detection skipped: no agent outputs or patches to analyze" - fi - - name: Clear MCP Config for detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f "$HOME/.copilot/mcp-config.json" - rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - - name: Prepare threat detection files - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - - name: Setup threat detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - WORKFLOW_NAME: "SDK Consistency Review Agent" - WORKFLOW_DESCRIPTION: "Reviews PRs to ensure features are implemented consistently across all SDK language implementations" - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); - await main(); - - name: Ensure threat-detection directory and log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" - env: - GH_HOST: github.com - GH_AW_COMPILED_VERSION: v0.88.2 - - name: Install threat-detect binary - if: always() && steps.detection_guard.outputs.run_detection == 'true' - continue-on-error: true - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 - - name: Execute threat detection with AWF - id: detection_agentic_execution - if: always() && steps.detection_guard.outputs.run_detection == 'true' - continue-on-error: true - timeout-minutes: 10 - env: - AWF_REFLECT_ENABLED: 1 - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} - GH_AW_HARNESS_MAX_RETRIES: 0 - GH_AW_LLM_PROVIDER: github - GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} - GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} - GH_AW_PHASE: detection - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 10 - GH_AW_VERSION: v0.88.2 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_AUTHOR_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - RUNNER_TEMP: ${{ runner.temp }} - S2STOKENS: true - TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - WORKFLOW_NAME: "SDK Consistency Review Agent" - WORKFLOW_DESCRIPTION: "Reviews PRs to ensure features are implemented consistently across all SDK language implementations" - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" - if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then - echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 - exit 127 - fi - GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" - mkdir -p "${RUNNER_TEMP}/gh-aw/bin" - if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then - cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" - fi - chmod 755 "$GH_AW_COPILOT_BIN" - - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then - GH_AW_MAX_AI_CREDITS="400" - fi - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log - - name: Render detection log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); - await main(); - - name: Copy detection firewall logs - if: always() && steps.detection_guard.outputs.run_detection == 'true' - continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall - if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi - if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi - - name: Upload threat detection artifact - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: detection - path: | - /tmp/gh-aw/threat-detection/detection_result.json - /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ - /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ - if-no-files-found: ignore - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage - with: - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); - await main(); - - name: Conclude threat detection - id: detection_conclusion - if: always() - continue-on-error: true - env: - RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} - DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} - GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json - - safe_outputs: - needs: - - activation - - agent - - detection - if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' - runs-on: ubuntu-slim - permissions: - issues: write - pull-requests: write - timeout-minutes: 45 - env: - GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} - GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/sdk-consistency-review" - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} - GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} - GH_AW_ENGINE_ID: "copilot" - GH_AW_ENGINE_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" - GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_TRACKER_ID: "sdk-consistency-review" - GH_AW_WORKFLOW_ID: "sdk-consistency-review" - GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" - outputs: - code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} - code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} - comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} - comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} - process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} - process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} - process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} - process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} - process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} - process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} - process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} - process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} - process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.80" - GH_AW_INFO_AWF_VERSION: "v0.28.12" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Mask OTLP telemetry headers - run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: "{agent,agent-output-fallback}" - merge-multiple: true - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - if [ -f "/tmp/gh-aw/agent_output.json" ]; then - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - fi - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Process Safe Outputs - id: process_safe_outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); - const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); - await main(); - - name: Upload Safe Outputs Items - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: safe-outputs-items - path: | - /tmp/gh-aw/safe-output-items.jsonl - /tmp/gh-aw/temporary-id-map.json - /tmp/gh-aw/safe-output-errors.json - if-no-files-found: ignore diff --git a/.github/workflows/sdk-consistency-review.md b/.github/workflows/sdk-consistency-review.md deleted file mode 100644 index 8346bcdd63..0000000000 --- a/.github/workflows/sdk-consistency-review.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -description: Reviews PRs to ensure features are implemented consistently across all SDK language implementations -tracker-id: sdk-consistency-review -model: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} -on: - roles: all - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths: - - "nodejs/**" - - "python/**" - - "go/**" - - "dotnet/**" - - "java/**" - - "rust/**" - - "!java/docs/**" - - "!java/*.txt" - - "!java/*.md" - workflow_dispatch: - inputs: - pr_number: - description: "PR number to review" - required: true - type: string -permissions: - contents: read - pull-requests: read - issues: read - copilot-requests: write -tools: - github: - toolsets: [default] -safe-outputs: - create-pull-request-review-comment: - max: 10 - add-comment: - max: 1 - hide-older-comments: true - allowed-reasons: [outdated] - threat-detection: - engine: - id: copilot - model: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} -timeout-minutes: 15 ---- - - - -# SDK Consistency Review Agent - -You are an AI code reviewer specialized in ensuring consistency across multi-language SDK implementations. This repository contains six SDK implementations (Node.js/TypeScript, Python, Go, .NET, Java, and Rust) that should maintain feature parity and consistent API design. - -## Your Task - -When a pull request modifies any SDK client code, review it to ensure: - -1. **Cross-language consistency**: If a feature is added/modified in one SDK, check whether: - - The same feature exists in other SDK implementations - - The feature is implemented consistently across all languages - - API naming and structure are parallel (accounting for language conventions) - -2. **Feature parity**: Identify if this PR creates inconsistencies by: - - Adding a feature to only one language - - Changing behavior in one SDK that differs from others - - Introducing language-specific functionality that should be available everywhere - -3. **API design consistency**: Check that: - - Method/function names follow the same semantic pattern (e.g., `createSession` vs `create_session` vs `CreateSession`) - - Parameter names and types are equivalent - - Return types are analogous - - Error handling patterns are similar - -## Context - -- Repository: ${{ github.repository }} -- PR number: ${{ github.event.pull_request.number || inputs.pr_number }} -- Modified files: Use GitHub tools to fetch the list of changed files - -## SDK Locations - -Determine the SDK root from the repository tree: use `src/sdk` when the language implementations are nested there (the runtime repository), otherwise use the repository root (the standalone SDK repository). All paths below are relative to that SDK root. Use repository-relative paths, including `src/sdk/` when applicable, when reading files or posting review comments. - -- **Node.js/TypeScript**: `nodejs/src/` -- **Python**: `python/copilot/` -- **Go**: `go/` -- **.NET**: `dotnet/src/` -- **Java**: `java/sdk/src/main/java/` -- **Rust**: `rust/src/` - -In the runtime repository, review SDK changes under `src/sdk/`; use other runtime code only as context, not as a separate SDK implementation. - -## Review Process - -1. **Get the authoritative PR delta**: - - Call `pull_request_read` with `method: get_files` for the PR, paginating until all changed files are retrieved - - Call `pull_request_read` with `method: get_diff` for the PR - - Treat these GitHub API responses as the only authoritative source of which changes belong to the PR, including when the PR head is a merge commit - - Base every claim about what the PR adds or modifies on the API diff; use the local checkout only for surrounding context and cross-SDK comparison - - Never infer the PR base from `HEAD^`, merge-parent ordering, recent commits, or local branch refs - - If the API file list or diff cannot be retrieved, call `missing_data` and stop; do not substitute an inferred local `git diff` range -2. **Identify the changed SDK(s)**: Determine which language implementation(s) are modified in the authoritative PR delta -3. **Analyze the changes**: Understand what feature/fix is being implemented from the authoritative PR delta -4. **Cross-reference other SDKs**: Check if the equivalent functionality exists in other language implementations: - - Read the corresponding files in other SDK directories - - Compare method signatures, behavior, and documentation -5. **Report findings**: If inconsistencies are found: - - Use `create-pull-request-review-comment` to add inline comments on specific lines where changes should be made - - Use `add-comment` to provide a summary of cross-SDK consistency findings - - Be specific about which SDKs need updates and what changes would bring them into alignment - -## Guidelines - -1. **Be respectful**: This is a technical review focusing on consistency, not code quality judgments -2. **Account for language idioms**: - - TypeScript uses camelCase (e.g., `createSession`) - - Python uses snake_case (e.g., `create_session`) - - Go uses PascalCase for exported/public functions (e.g., `CreateSession`) and camelCase for unexported/private functions - - .NET uses PascalCase (e.g., `CreateSession`) - - Java uses camelCase for methods (e.g., `createSession`) and PascalCase for classes - - Rust uses snake_case for functions and methods (e.g., `create_session`) and PascalCase for types - - Focus on public API methods when comparing across languages -3. **Focus on API surface**: Prioritize public APIs over internal implementation details -4. **Distinguish between bugs and features**: - - Bug fixes in one SDK might reveal bugs in others - - New features should be considered for all SDKs -5. **Suggest, don't demand**: Frame feedback as suggestions for maintaining consistency -6. **Skip trivial changes**: Don't flag minor differences like comment styles or variable naming -7. **Only comment if there are actual consistency issues**: If the PR maintains consistency or only touches one SDK's internal implementation, acknowledge it positively in a summary comment - -## Example Scenarios - -### Good: Consistent feature addition - -If a PR adds a new `setTimeout` option to the Node.js SDK and the equivalent feature already exists or is added to Python, Go, .NET, Java, and Rust in the same PR. - -### Bad: Inconsistent feature - -If a PR adds a `withRetry` method to only the Python SDK, but this functionality doesn't exist in other SDKs and would be useful everywhere. - -### Good: Language-specific optimization - -If a PR optimizes JSON parsing in Go using native libraries specific to Go's ecosystem—this doesn't need to be mirrored exactly in other languages. - -## Output Format - -- **If consistency issues found**: Add specific review comments pointing to the gaps and suggest which other SDKs need similar changes -- **If no issues found**: Add a brief summary comment confirming the changes maintain cross-SDK consistency diff --git a/.github/workflows/sdk-dotnet.yml b/.github/workflows/sdk-dotnet.yml index 3b11e570ea..63b689f94e 100644 --- a/.github/workflows/sdk-dotnet.yml +++ b/.github/workflows/sdk-dotnet.yml @@ -1,6 +1,6 @@ # Invoked by sdk.yml to run the .NET SDK build/test matrix, documentation, -# backend shards, full subprocess CAPI coverage, and focused in-process smoke -# coverage on Linux and x64 musl. +# full subprocess CAPI coverage, and focused in-process smoke +# coverage across standard platforms and x64 musl. name: "SDK .NET" env: @@ -25,13 +25,9 @@ permissions: jobs: dotnet-linux-x64: - name: ".NET (ubuntu-latest, ${{ matrix.transport }}, CAPI, 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' || '' }} - 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,54 @@ 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-26-xlarge, default, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'darwin-arm64' - runs-on: macos-latest + runs-on: macos-26-xlarge 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 }} + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-macOS-ARM64 + artifact-name: executable-sdk-macOS-ARM64 path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -145,42 +131,48 @@ jobs: - run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test out-of-process + id: subprocess + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: ../scripts/ci/run-dotnet-tests.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" run: ../scripts/ci/run-dotnet-tests.sh - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-macos-latest-default-capi-${{ matrix.shard }}-${{ github.run_attempt }} + name: dotnet-test-diagnostics-macos-26-xlarge-default-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 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 + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Windows-X64 + artifact-name: executable-sdk-Windows-X64 path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -195,87 +187,42 @@ jobs: - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: ../scripts/ci/run-dotnet-tests.sh - - if: failure() - uses: actions/upload-artifact@v7 - with: - name: dotnet-test-diagnostics-windows-latest-default-capi-${{ 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: + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" run: ../scripts/ci/run-dotnet-tests.sh - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-ubuntu-latest-default-${{ matrix.backend }}-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 +234,6 @@ jobs: image: mcr.microsoft.com/dotnet/sdk:10.0-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }} - transport: ${{ matrix.transport }} command: | dotnet --info | grep -Eq "RID:[[:space:]]+linux-musl-x64" apk add --no-cache nodejs npm @@ -310,50 +256,33 @@ jobs: dotnet restore "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" \ -p:CopilotSkipCliDownload=true - set -- - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - set -- --filter 'FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events' - 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 cf505e29cf..e7235998f8 100644 --- a/.github/workflows/sdk-go.yml +++ b/.github/workflows/sdk-go.yml @@ -25,13 +25,12 @@ permissions: jobs: go: - name: "Go (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Go (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -42,9 +41,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -56,51 +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$' - 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: . @@ -111,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 @@ -129,38 +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$' - 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 220271c269..c4807d22cd 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -1,5 +1,5 @@ -# Invoked by sdk.yml to run full subprocess Java SDK coverage on supported JDKs, -# focused in-process smoke coverage across native classifiers, and documentation. +# Invoked by sdk.yml to run full subprocess Java SDK coverage followed by focused +# in-process smoke on standard platforms and x64 musl, plus JDK 17 and docs checks. name: "SDK Java" env: @@ -24,13 +24,19 @@ permissions: jobs: java: - name: "Java (JDK ${{ matrix.test-jdk }})" - if: inputs.platform == 'all' || inputs.platform == 'linux-x64' + name: "Java (${{ matrix.os }}, JDK ${{ matrix.test-jdk }})" + if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} test-jdk: ["25", "17"] - runs-on: ubuntu-latest + exclude: + - os: macos-26 + test-jdk: "17" + - os: windows-latest + test-jdk: "17" + runs-on: ${{ matrix.os }} defaults: run: shell: bash @@ -40,9 +46,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-gnu + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -56,16 +62,17 @@ jobs: node-version: 22 # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./scripts/test-update-documentation-versions.sh - run: ./mvnw test-compile jar:jar - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./mvnw javadoc:javadoc - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./mvnw spotless:check - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/test/harness + - if: runner.os == 'Windows' + run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - if: matrix.test-jdk == '25' + id: subprocess env: CI: "true" run: ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" @@ -78,84 +85,121 @@ jobs: env: CI: "true" run: ./mvnw -pl sdk jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test-jdk-banner surefire:test failsafe:integration-test failsafe:verify jacoco:report@build-coverage-report-from-tests -Denforcer.skip=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" + - name: Test in-process smoke + if: ${{ !cancelled() && matrix.test-jdk == '25' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + CI: "true" + run: | + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ + -Dtest=NoTestsForInProcessSmoke \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dit.test=InProcessTransportIT,AuthHostE2ETest \ + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' + run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:java - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-jdk-${{ matrix.test-jdk }} + name: java-test-results-${{ matrix.os }}-jdk-${{ matrix.test-jdk }} path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - java-inprocess: - name: "Java (inprocess, ${{ matrix.classifier }})" - if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.platform == 'linux-x64' && '[{"os":"ubuntu-latest","classifier":"linux-x64"}]' || inputs.platform == 'darwin-arm64' && '[{"os":"macos-26","classifier":"darwin-arm64"}]' || inputs.platform == 'win32-x64' && '[{"os":"windows-latest","classifier":"win32-x64"}]' || '[{"os":"ubuntu-latest","classifier":"linux-x64"},{"os":"windows-latest","classifier":"win32-x64"},{"os":"macos-26","classifier":"darwin-arm64"}]') }} - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/java + java-musl-x64: + name: "Java (JDK 25, linuxmusl-x64)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' + runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . - - uses: actions/setup-java@v5 - with: - java-version: "25" - distribution: microsoft - cache: maven - - uses: actions/setup-node@v6 + - uses: ./.github/actions/run-alpine-tests + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} with: - node-version: 22 - - env: - CI: "true" - MAVEN_ARGS: ${{ matrix.maven-args }} - run: >- - ./mvnw clean verify -Pinprocess - -Dtest=NoTestsForInProcessSmoke - -Dsurefire.failIfNoSpecifiedTests=false - -Dit.test=InProcessTransportIT - -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" - $MAVEN_ARGS + image: eclipse-temurin:25-jdk-alpine + sdk-root: /workspace/${{ inputs.sdk-home }} + workdir: /workspace/${{ inputs.sdk-home }}/java + command: | + apk add --no-cache java-cacerts maven nodejs npm + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts + export GITHUB_ENV=/tmp/copilot-sdk-runtime.env + export COPILOT_RUNTIME_TARGET=linuxmusl-x64 + export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64 + node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare + set -a + . "$GITHUB_ENV" + set +a + node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 + ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl + status=0 + ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$? + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ + -Dtest=NoTestsForInProcessSmoke \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dit.test=InProcessTransportIT,AuthHostE2ETest \ + -Dcopilot.native.libc=musl \ + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$? + exit "$status" - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-inprocess-${{ matrix.classifier }} + name: java-test-results-linuxmusl-x64 path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - - java-inprocess-musl-x64: - name: "Java (inprocess, linuxmusl-x64)" - if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' - runs-on: ubuntu-latest + java-musl-arm64: + name: "Java (JDK 25, linuxmusl-arm64)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-arm64' + runs-on: ubuntu-24.04-arm timeout-minutes: 30 steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-ARM64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . - - name: Run Java SDK tests (InProcess, musl) + - name: Run Java SDK tests (linuxmusl-arm64) env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: | @@ -166,56 +210,44 @@ jobs: --env COPILOT_HMAC_KEY \ --env GITHUB_ACTIONS=true \ --env GITHUB_WORKSPACE=/workspace \ - --env SDK_HOME="/workspace/$SDK_HOME" \ + --env COPILOT_SDK_ROOT="/workspace/$SDK_HOME" \ eclipse-temurin:25-jdk-alpine \ sh -c 'set -eux apk add --no-cache git java-cacerts maven nodejs npm - export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts git config --global --add safe.directory /workspace + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts export GITHUB_ENV=/tmp/copilot-sdk-runtime.env - export COPILOT_RUNTIME_TARGET=linuxmusl-x64 - export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64 - node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + export COPILOT_RUNTIME_TARGET=linuxmusl-arm64 + export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-arm64 + node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare set -a . "$GITHUB_ENV" set +a - node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 - ./mvnw clean verify -Pinprocess \ + node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64 + ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl + status=0 + ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$? + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ -Dtest=NoTestsForInProcessSmoke \ -Dsurefire.failIfNoSpecifiedTests=false \ - -Dit.test=InProcessTransportIT \ + -Dit.test=InProcessTransportIT,AuthHostE2ETest \ -Dcopilot.native.libc=musl \ - -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH"' + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$? + exit "$status"' - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-inprocess-linuxmusl-x64 + name: java-test-results-linuxmusl-arm64 path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - - docs-java: - name: "Docs (Java)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-java@v5 - with: - distribution: microsoft - java-version: "25" - cache: maven - - run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true - working-directory: ${{ inputs.sdk-home }}/java - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:java - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-nodejs.yml b/.github/workflows/sdk-nodejs.yml index 439d893aa3..55037d3f38 100644 --- a/.github/workflows/sdk-nodejs.yml +++ b/.github/workflows/sdk-nodejs.yml @@ -19,19 +19,23 @@ on: sdk-home: required: true type: string + outputs: + capi-result: + description: "Standard-platform CAPI job result, independent of BYOK jobs" + # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495 + value: ${{ fromJSON(toJSON(jobs.nodejs)).result }} permissions: contents: read jobs: nodejs: - name: "Node.js (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Node.js (${{ matrix.os }}, CAPI)" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -42,9 +46,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -54,16 +58,19 @@ jobs: cache: npm cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json - run: npm ci --ignore-scripts + - name: Install generator test dependencies + run: npm ci --ignore-scripts + working-directory: ${{ inputs.sdk-home }}/scripts/codegen # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run format:check - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run lint - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run typecheck - run: npm run build - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' env: COPILOT_SDK_RUNTIME_PLATFORMS: linux-x64 run: | @@ -71,43 +78,104 @@ jobs: npm run verify:release-packages - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm test working-directory: ${{ inputs.sdk-home }}/test/harness - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm ci working-directory: ${{ inputs.sdk-home }}/scripts/corrections - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm test working-directory: ${{ inputs.sdk-home }}/scripts/corrections - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} run: | - if [ "$COPILOT_SDK_DEFAULT_CONNECTION" = "inprocess" ]; then - npm test -- test/e2e/inprocess_ffi.e2e.test.ts + 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 - 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 68cb1e4de9..2abeca3d8a 100644 --- a/.github/workflows/sdk-python.yml +++ b/.github/workflows/sdk-python.yml @@ -25,14 +25,14 @@ permissions: jobs: python: - name: "Python (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Python (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -44,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: . @@ -62,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 - 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: . @@ -118,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 @@ -139,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 - 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 b67646bf19..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,26 +177,49 @@ 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/copilot-runtime.* + src/native/runtime/runtime.*.node.metadata + 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: datadog-api-key: ${{ secrets.DATADOG_API_KEY }} datadog-site: ${{ vars.DATADOG_SITE || vars.DD_SITE }} + - name: Set up private source authentication + if: >- + github.actor != 'dependabot[bot]' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && + inputs.runtime-source == 'checkout' && + steps.cache.outputs.cache-hit != 'true' + uses: ./.github/actions/setup-private-source-auth + with: + client-id: ${{ vars.RUNTIME_ACCESSOR_APP_CLIENT_ID }} + private-key: ${{ secrets.RUNTIME_ACCESSOR_APP_PRIVATE_KEY }} + - name: Install the ARM64 strip tool if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit != 'true' && runner.os == 'Linux' && endsWith(inputs.target, '-arm64') run: script/linux/apt-install.sh binutils-aarch64-linux-gnu @@ -147,15 +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/copilot-runtime.* + src/native/runtime/runtime.*.node.metadata + 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 @@ -164,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 75749ba841..abe13e28bb 100644 --- a/.github/workflows/sdk-rust.yml +++ b/.github/workflows/sdk-rust.yml @@ -25,13 +25,13 @@ permissions: jobs: rust: - name: "Rust (${{ matrix.os }}, default)" + name: "Rust (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) timeout-minutes: 60 strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26-xlarge"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26-xlarge","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -45,9 +45,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -69,82 +69,74 @@ 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. + - if: github.event_name != 'merge_group' && runner.os == 'Linux' + run: bash scripts/check-fresh-rebuild.sh --no-default-features --features test-support,derive - if: github.event_name != 'merge_group' && runner.os == 'Linux' env: RUSTDOCFLAGS: "-D warnings" run: cargo doc --no-deps --no-default-features --features local-runtime,derive + - name: External-stream-only SDK + run: cargo test --no-default-features --features test-support --test protocol_version_test - 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 }} RUST_E2E_CONCURRENCY: 4 - run: cargo test --no-default-features --features test-support,derive -- --test-threads=4 --nocapture + run: cargo test --no-default-features --features runtime,test-support,derive -- --test-threads=4 --nocapture + - name: Report Linux Rust build resources + if: failure() && runner.os == 'Linux' + run: | + echo "::group::Rust SDK build resources" + df -h . "$RUNNER_TEMP" + df -i . "$RUNNER_TEMP" + free -h + if [ -d target ]; then + du -sh target + fi + echo "::endgroup::" - rust-inprocess: - name: "Rust (${{ matrix.os }}, inprocess)" - if: contains(fromJSON('["all","linux-x64","darwin-arm64"]'), inputs.platform) - strategy: - fail-fast: false - matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || '["ubuntu-latest","macos-latest"]') }} - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/rust - env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - COPILOT_SDK_DEFAULT_CONNECTION: inprocess - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/download-artifact@v8 - with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} - path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare - working-directory: . - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: "1.94.0" - - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ inputs.sdk-home }}/rust - prefix-key: v2-sdk-rust-no-bin - cache-bin: false - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test in-process smoke + id: inprocess + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: >- cargo test --no-default-features --features test-support,local-runtime,derive --test e2e inprocess::should_start_ping_and_stop_inprocess_client -- --exact --test-threads=1 --nocapture + - 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: . @@ -159,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\"" @@ -180,13 +171,17 @@ jobs: esac export CARGO_TARGET_DIR=/tmp/copilot-sdk-rust-target + # Keep every integration test without filling the runner with build artifacts. + 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 - else - export RUST_E2E_CONCURRENCY=4 - cargo test --no-default-features --features 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 bb96fb22fd..56c165db89 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -4,6 +4,9 @@ name: "SDK build and test" env: COPILOT_BAZEL_ANG_ENABLED: ${{ vars.COPILOT_BAZEL_ANG_ENABLED }} + COPILOT_BAZEL_ANG_SCOPE: ${{ vars.COPILOT_BAZEL_ANG_SCOPE }} + COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE }} + COPILOT_BAZEL_ANG_PR_SAMPLED_MODE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLED_MODE }} HUSKY: 0 POWERSHELL_UPDATECHECK: Off SETUP_NODE_TIMEOUT_MINUTES: 10 @@ -24,8 +27,150 @@ permissions: contents: read jobs: + detect-sdk-changes: + name: "Detect SDK-impacting changes" + runs-on: ubuntu-slim + outputs: + has_sdk_changes: ${{ steps.full-coverage.outputs.has_sdk_changes || steps.classify.outputs.has_sdk_changes || steps.recover.outputs.has_sdk_changes }} + steps: + - name: Require full coverage outside pull requests + id: full-coverage + if: github.event_name != 'pull_request' + run: echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + id: checkout + if: github.event_name == 'pull_request' + continue-on-error: true + timeout-minutes: 4 + with: + # Include the synthetic merge commit's base parent without fetching + # repository history. + fetch-depth: 2 + persist-credentials: false + + - name: Detect repository layout + id: repository-layout + if: steps.checkout.outcome == 'success' + continue-on-error: true + run: | + trusted_revision="$(git rev-parse "$GITHUB_SHA^1")" + echo "base-sha=$trusted_revision" >> "$GITHUB_OUTPUT" + if git cat-file -e "${trusted_revision}:src/sdk/package.json" 2>/dev/null && + git cat-file -e "${trusted_revision}:script/sea-build.ts" 2>/dev/null; then + echo "layout=runtime" >> "$GITHUB_OUTPUT" + elif git cat-file -e "${trusted_revision}:package.json" 2>/dev/null && + git cat-file -e "${trusted_revision}:nodejs/package.json" 2>/dev/null; then + echo "layout=standalone" >> "$GITHUB_OUTPUT" + else + echo "::warning::Unable to identify the SDK repository layout; requiring full coverage." + exit 1 + fi + + - name: Find SDK-impacting paths + id: filter + if: steps.repository-layout.outcome == 'success' + continue-on-error: true + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + base_sha: ${{ steps.repository-layout.outputs.base-sha }} + sha: ${{ github.sha }} + skip_initial_fetch: true + fail_on_initial_diff_error: true + quotepath: false + files_yaml: | + runtime: + - .github/actions/** + - .github/workflows/sdk*.yml + - .github/workflows/sdk*.yaml + - .github/workflows/shared-cli-build.yml + - .github/workflows/publish.yml + - .editorconfig + - .gitattributes + - .nvmrc + - package.json + - package-lock.json + - pnpm-lock.yaml + - sdk-protocol-version.json + - assets/** + - BUILD.bazel + - src/** + - generated/** + - schema/** + - files/** + - third_party/** + - patches/** + - tools/bazel/** + - .cargo/** + - bazel/** + - script/** + - .bazelignore + - .bazelrc + - .bazelversion + - Cargo.lock + - Cargo.toml + - MODULE.bazel + - MODULE.bazel.lock + - pnpm-workspace.yaml + - rust-toolchain.toml + - esbuild.ts + - tsconfig*.json + standalone: + - .github/actions/** + - .github/workflows/sdk*.yml + - .github/workflows/sdk*.yaml + - .github/workflows/shared-cli-build.yml + - .github/workflows/publish.yml + - .editorconfig + - .gitattributes + - .nvmrc + - package.json + - package-lock.json + - pnpm-lock.yaml + - sdk-protocol-version.json + - assets/** + - BUILD.bazel + - nodejs/** + - python/** + - go/** + - dotnet/** + - java/** + - rust/** + - scripts/** + - samples/** + - test/** + - docs/** + - README.md + - vitest.config.ts + + - name: Select layout-specific result + id: classify + if: steps.filter.outcome == 'success' + env: + LAYOUT: ${{ steps.repository-layout.outputs.layout }} + RUNTIME_CHANGED: ${{ steps.filter.outputs.runtime_any_modified }} + STANDALONE_CHANGED: ${{ steps.filter.outputs.standalone_any_modified }} + run: | + if [[ "$LAYOUT" == "runtime" ]]; then + has_sdk_changes="$RUNTIME_CHANGED" + elif [[ "$LAYOUT" == "standalone" ]]; then + has_sdk_changes="$STANDALONE_CHANGED" + else + exit 1 + fi + echo "has_sdk_changes=$has_sdk_changes" >> "$GITHUB_OUTPUT" + + - name: Require full coverage after detection errors + id: recover + if: github.event_name == 'pull_request' && (steps.checkout.outcome != 'success' || steps.repository-layout.outcome != 'success' || steps.filter.outcome != 'success' || steps.classify.outcome != 'success') + run: | + echo "::warning::SDK change detection failed; requiring full coverage." + echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT" + detect-layout: name: "Detect SDK layout" + needs: detect-sdk-changes + if: ${{ !cancelled() && (needs.detect-sdk-changes.result != 'success' || needs.detect-sdk-changes.outputs.has_sdk_changes == 'true') }} runs-on: ubuntu-latest outputs: runtime-source: ${{ steps.detect.outputs.runtime-source }} @@ -49,8 +194,9 @@ jobs: fi # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain the - # full platform matrix. Merge groups rerun Linux x64 as the representative - # compatibility lane. Remove the merge_group gates below to restore it. + # full SDK matrix. Merge groups rerun only Linux x64 Node.js out-of-process + # CAPI coverage. Remove the merge_group gates here and in sdk-platform.yml + # and sdk-nodejs.yml to restore the full matrix. build-runtime-windows-x64-addons: name: "Build runtime addons (win32-x64)" needs: detect-layout @@ -70,6 +216,15 @@ 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 + + - name: Capture verified addon source identity + run: node --experimental-strip-types script/resolve-cli-build-identity.ts --github-env + - name: Restore completed Windows x64 addons id: cache uses: actions/cache/restore@v6 @@ -77,11 +232,13 @@ jobs: path: | windows-x64-addons-receipt.txt src/native/runtime/runtime.win32-x64-msvc.node + src/native/runtime/runtime.win32-x64-msvc.node.metadata src/native/runtime/copilot-runtime.win32-x64-msvc.exe 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' @@ -92,6 +249,13 @@ jobs: datadog-api-key: ${{ secrets.DATADOG_API_KEY }} datadog-site: ${{ vars.DATADOG_SITE || vars.DD_SITE }} + - name: Set up private source authentication + if: steps.cache.outputs.cache-hit != 'true' && github.actor != 'dependabot[bot]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + uses: ./.github/actions/setup-private-source-auth + with: + client-id: ${{ vars.RUNTIME_ACCESSOR_APP_CLIENT_ID }} + private-key: ${{ secrets.RUNTIME_ACCESSOR_APP_PRIVATE_KEY }} + - name: Cache xwin downloads if: steps.cache.outputs.cache-hit != 'true' uses: actions/cache@v6 @@ -109,6 +273,9 @@ jobs: - name: Cross-build Windows x64 addons if: steps.cache.outputs.cache-hit != 'true' env: + # The CI-owned Bazel override is intentionally trusted; without this + # identity the producer stamps an empty source while consumers verify the SHA. + COPILOT_NAPI_ADDONS_BUILD_IDENTITY: ${{ github.sha }} CLI_GIT_COMMIT: ${{ github.sha }} CLI_PACKAGE_NAME: "@github/copilot" CLI_TARGET: win32-x64 @@ -122,8 +289,13 @@ jobs: --bazel-config=windows-e2e-no-icf - name: Verify Windows x64 addons - if: steps.cache.outputs.cache-hit != 'true' + env: + CLI_PACKAGE_NAME: "@github/copilot" run: | + export CLI_VERSION="0.0.1-${GITHUB_SHA::7}" + node --experimental-strip-types script/assert-runtime-build-metadata.ts \ + src/native/runtime win32-x64-msvc + test -s src/native/runtime/runtime.win32-x64-msvc.node.metadata for artifact in \ src/native/runtime/runtime.win32-x64-msvc.node \ src/native/runtime/copilot-runtime.win32-x64-msvc.exe \ @@ -143,6 +315,7 @@ jobs: path: | windows-x64-addons-receipt.txt src/native/runtime/runtime.win32-x64-msvc.node + src/native/runtime/runtime.win32-x64-msvc.node.metadata src/native/runtime/copilot-runtime.win32-x64-msvc.exe src/native/runtime/index.d.ts src/native/cli/cli-native.win32-x64-msvc.node @@ -154,6 +327,7 @@ jobs: name: sdk-bazel-cross-addons-win32-x64-release path: | src/native/runtime/runtime.win32-x64-msvc.node + src/native/runtime/runtime.win32-x64-msvc.node.metadata src/native/runtime/copilot-runtime.win32-x64-msvc.exe src/native/runtime/index.d.ts src/native/cli/cli-native.win32-x64-msvc.node @@ -201,6 +375,26 @@ jobs: timeout-minutes: 60 secrets: inherit + build-runtime-linuxmusl-arm64: + name: "Build runtime (linuxmusl-arm64)" + needs: detect-layout + if: github.event_name != 'merge_group' + permissions: + actions: read + contents: read + id-token: write + packages: read + uses: ./.github/workflows/sdk-runtime-artifact.yml + with: + runtime-source: ${{ needs.detect-layout.outputs.runtime-source }} + sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} + os: ubuntu-latest-xl + target: linuxmusl-arm64 + artifact: Linux-ARM64-musl + bazel-config: e2e-no-lto + timeout-minutes: 60 + secrets: inherit + build-runtime-darwin-arm64: name: "Build runtime (darwin-arm64)" needs: detect-layout @@ -214,7 +408,7 @@ jobs: with: runtime-source: ${{ needs.detect-layout.outputs.runtime-source }} sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - os: macos-latest-xlarge + os: macos-26-xlarge target: darwin-arm64 artifact: macOS-ARM64 bazel-config: e2e-no-lto @@ -264,6 +458,16 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit + sdk-linuxmusl-arm64: + name: "Linux musl ARM64 (Java)" + needs: [detect-layout, build-runtime-linuxmusl-arm64] + if: github.event_name != 'merge_group' + uses: ./.github/workflows/sdk-java.yml + with: + platform: linuxmusl-arm64 + sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} + secrets: inherit + sdk-darwin-arm64: name: "macOS ARM64" needs: [detect-layout, build-runtime-darwin-arm64] @@ -285,15 +489,35 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit - sdk-docs: - name: "Documentation" - needs: detect-layout - if: github.event_name != 'merge_group' - uses: ./.github/workflows/sdk-platform.yml - with: - platform: docs - sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - secrets: inherit + # Only Linux CAPI gates this rollup; the full SDK aggregate still reports all coverage. + sdk-typescript: + name: sdk-typescript + if: always() + needs: [detect-sdk-changes, sdk-linux-x64] + runs-on: ubuntu-slim + steps: + - name: Check Linux CAPI Node.js SDK result + env: + DETECT_RESULT: ${{ needs.detect-sdk-changes.result }} + EVENT_NAME: ${{ github.event_name }} + HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }} + RESULTS: ${{ toJSON(needs) }} + run: | + linux_result=$(jq -r '.["sdk-linux-x64"].result' <<< "$RESULTS") + if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then + if [[ "$linux_result" != "skipped" ]]; then + echo "::error::Non-SDK pull request did not skip sdk-linux-x64: $linux_result" + exit 1 + fi + echo "No SDK-impacting changes; Linux CAPI Node.js SDK job skipped" + exit 0 + fi + + result=$(jq -r '.["sdk-linux-x64"].outputs["nodejs-capi-result"]' <<< "$RESULTS") + if [[ "$result" != "success" ]]; then + echo "::error::Linux CAPI Node.js SDK job: expected success, got $result" + exit 1 + fi check-freshness: name: "Check schema and SDK freshness" @@ -376,26 +600,45 @@ jobs: name: "SDK" if: always() needs: + - detect-sdk-changes - build-runtime-windows-x64-addons - build-runtime-linux-x64 - build-runtime-linuxmusl-x64 + - build-runtime-linuxmusl-arm64 - build-runtime-darwin-arm64 - build-runtime-win32-x64 - detect-layout - sdk-linux-x64 - sdk-linuxmusl-x64 + - sdk-linuxmusl-arm64 - sdk-darwin-arm64 - sdk-win32-x64 - - sdk-docs - check-freshness - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Check SDK results env: + DETECT_RESULT: ${{ needs.detect-sdk-changes.result }} EVENT_NAME: ${{ github.event_name }} + HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }} RESULTS: ${{ toJSON(needs) }} RUNTIME_SOURCE: ${{ needs.detect-layout.outputs.runtime-source }} run: | + if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then + unexpected=$(jq -r ' + to_entries[] + | select(.key != "detect-sdk-changes" and .value.result != "skipped") + | "\(.key): \(.value.result)" + ' <<< "$RESULTS") + if [[ -n "$unexpected" ]]; then + echo "::error::Non-SDK pull request did not skip all SDK jobs:" + echo "$unexpected" + exit 1 + fi + echo "No SDK-impacting changes; SDK build and test matrix skipped" + exit 0 + fi + # TEMPORARY MERGE QUEUE REDUCTION: keep this branch aligned with the # gated jobs above until the full merge-group matrix is restored. if [[ "$EVENT_NAME" == "merge_group" ]]; then @@ -408,12 +651,13 @@ jobs: skipped_jobs=( build-runtime-windows-x64-addons build-runtime-linuxmusl-x64 + build-runtime-linuxmusl-arm64 build-runtime-darwin-arm64 build-runtime-win32-x64 sdk-linuxmusl-x64 + sdk-linuxmusl-arm64 sdk-darwin-arm64 sdk-win32-x64 - sdk-docs ) for job in "${required_jobs[@]}"; do if [[ "$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS")" != "success" ]]; then @@ -429,7 +673,7 @@ jobs: exit 1 fi done - echo "Linux x64 SDK compatibility and generated clients passed" + echo "Linux x64 Node.js SDK compatibility and generated clients passed" exit 0 fi @@ -444,6 +688,7 @@ jobs: failures=$(jq -r --arg runtime_source "$RUNTIME_SOURCE" ' to_entries[] | select(.value.result != "success") + | select(.key != "detect-sdk-changes") | select( $runtime_source != "published" or .key != "build-runtime-windows-x64-addons" diff --git a/.gitignore b/.gitignore index 0b7600351f..c67e4c9941 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ docs/.validation/ .DS_Store +# Local runtime-host integration homes, caches, and candidate packages. +/.runtime-host-test-work/ + # Generated by `npm run auth:refresh` for local Azure Artifacts routing. /java/scripts/codegen/.npmrc 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 ba5776bc7b..93cd587a4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ are not necessarily sufficient to build the repository. | --- | --- | | Node.js and shared tooling | [Node.js](https://nodejs.org/en/download) satisfying [the SDK engines](nodejs/package.json): `^20.19.0 \|\| >=22.12.0`; SDK CI uses Node 22. Nested runtime builds require **Node >=24** and the parent repository's pinned pnpm. Use npm for SDK-local projects and their lockfiles. | | Python | [Python >=3.11](python/pyproject.toml) and [uv](https://docs.astral.sh/uv/getting-started/installation/). `uv sync` installs the project, development tools, and pinned Ruff; do not install those globally. CI tests on 3.11 and validates docs on 3.12. | -| Go | [Install Go](https://go.dev/doc/install) at the version selected by [go.mod](go/go.mod), currently 1.24, also required by [samples](go/samples/go.mod). Go includes `gofmt`. Checks additionally need [golangci-lint](https://golangci-lint.run/docs/welcome/install/); [CI](.github/workflows/sdk-go.yml) currently uses `latest`, not an exact repository pin. | +| Go | [Install Go](https://go.dev/doc/install) at the version selected by [go.mod](go/go.mod), currently 1.24, also required by [samples](go/samples/go.mod). Go includes `gofmt`; generation fails if `go fmt` cannot run rather than emitting unformatted projections. Checks additionally need [golangci-lint](https://golangci-lint.run/docs/welcome/install/); [CI](.github/workflows/sdk-go.yml) currently uses `latest`, not an exact repository pin. | | .NET | [.NET SDK](https://dotnet.microsoft.com/download/dotnet) selected by [global.json](dotnet/global.json): 10.0.100 with major roll-forward. Also install the **[.NET 8 runtime](https://dotnet.microsoft.com/download/dotnet/8.0)** for the `net8.0` [tests](dotnet/test/GitHub.Copilot.SDK.Test.csproj). SDK 10 alone does not supply that runtime. Windows additionally runs `net472` tests and needs a compatible .NET Framework runtime. | | Java | [Install a JDK](https://learn.microsoft.com/java/openjdk/download) meeting the [build requirement](java/sdk/pom.xml), currently >=25. The artifact targets Java 17; a second JDK 17 is needed only to reproduce that compatibility test. From `java/`, use `./mvnw` (`.\mvnw.cmd` on Windows): the [wrapper](java/.mvn/wrapper/maven-wrapper.properties) downloads pinned Maven. Normal builds/tests do not require a global Maven install. | | Rust | [rustup](https://rustup.rs/) with the SDK's [pinned toolchain and components](rust/rust-toolchain.toml), currently 1.94.0. Formatting/check tasks also need rustfmt from the toolchain selected in [run-tasks.mjs](scripts/run-tasks.mjs). This is separate from the parent runtime's Rust toolchain. | @@ -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 @@ -218,6 +254,33 @@ npm --prefix java/scripts/codegen ci npm run generate ``` +A standalone checkout can also consume schemas exported by a separate runtime +checkout for local development through the same facade: + +```bash +npm run generate -- --runtime-source checkout --schema-dir /absolute/path/to/runtime/generated +``` + +Generate both `api.schema.json` and `session-events.schema.json` in the runtime +checkout first, using that revision's supported commands. Keep them from the +same immutable runtime revision and record the producer commit and both file +digests when handing off an unreleased API. The equivalent generator environment +is `COPILOT_RUNTIME_SOURCE=checkout` with `COPILOT_CLI_SCHEMA_DIR` pointing to +their shared directory. Missing or invalid schemas fail rather than falling +back to a published package. + +This selects generation inputs only: it does not publish or install a runtime, +change the CLI release pin, or make a new RPC callable on an older runtime. +Use the matching runtime build for integration checks and retain capability +checks for unsupported runtimes. Do not replace installed package sources or +edit generated files to emulate an unreleased contract. + +Author SDK implementation changes in the runtime repository's `src/sdk`. +The runtime release's `sdk-release-snapshot` job exports that entire tree to +`github/copilot-sdk`, with release-derived pins. Standalone development results +must be reconciled into that source before release; an independent standalone +change can otherwise be replaced by the next snapshot. + Do not replace runtime-checkout pins with a published version to make setup work. If the shared CLI version is `0.0.0-dev`, it is a development placeholder: local work still uses same-checkout artifacts. Release snapshot export, not @@ -238,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 @@ -250,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 @@ -261,12 +332,82 @@ uv run --locked --project python npm run docs:python ``` This extracts and validates snippets for Node.js, Python, Go, .NET, or Java. -Java's current docs validator calls `mvn` directly, -so this task also needs Maven 3.9+ on PATH. There is no `docs:rust` facade; +`docs:java` installs the SDK once through its Maven wrapper, with tests, replay +harness setup, and native downloads disabled, then compiles the snippets using +the same wrapper. CI supplies that installation in its existing post-test step; +the validator does not reinstall it. A separate Maven installation is not required. +Java validation also compiles the exact [README Quick Start](java/README.md#quick-start). +The regular Maven integration suite checks a standalone consumer JAR with a manifest +classpath and runtime dependencies, without a live model, credentials, or native runtime. +These checks replace the former agent-driven Java smoke workflow. +There is no `docs:rust` facade; follow [the Rust SDK workflow](.github/workflows/sdk-rust.yml) for rustdoc. ### Recording and replaying SDK tests +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. + +All six SDKs run full subprocess coverage followed by a short in-process +smoke step on the same runner on Linux/glibc x64, macOS ARM64, Windows x64, +and Linux/musl x64. Smoke still runs if the subprocess tests fail, and either +failure fails the job. Java uses JDK 25 on all four platforms, plus a +Linux/glibc JDK 17 compatibility job using precompiled classes. +Merge groups retain the reduced Linux TypeScript CAPI subprocess coverage. + +The `sdk-typescript` required rollup checks only the Linux CAPI job, including +its build, packaging, and applicable static checks. Other platforms, BYOK +backends, and languages keep their existing scheduling and failure reporting; +they do not gate this rollup. The full `SDK` aggregate still requires all +scheduled coverage to succeed. + +The three BYOK backend sweeps run in separate Linux TypeScript jobs, alongside +the normal CAPI job; they do not repeat unit tests, packaging, or static +checks. After preparing the runtime as described above, run a sweep from the +SDK root: + +```bash +COPILOT_SDK_E2E_BACKEND=anthropic-messages GITHUB_ACTIONS=true \ + npm --prefix nodejs test -- test/e2e +``` + +Use `openai-completions` or `openai-responses` for the other sweeps; unset the +variable or use `capi` for the normal suite. BYOK uses the shared captures +through the corresponding protocol adapter and never forwards replay misses +to a live provider. Use the fixture's `createClient()` for secondary clients +that create or resume model-backed sessions. Tests that require CAPI or own +their provider/request-handler setup stay in the normal job, with exclusions +in `vitest.config.ts` or individual `it.skipIf(isByokBackend)` cases. Other SDK +languages retain their language-specific provider tests in their normal suites. + +The Node.js Vitest global setup bundles the shared replay proxy once per run +into Vitest's project-owned temporary directory (and refreshes it on watch +reruns). Each E2E context launches that bundle directly with the test runner's +Node executable, without an npm, shell, or TypeScript-loader subprocess. +Focused Vitest commands use the same setup; no separate proxy build is needed. +If startup or shutdown cleanup cannot confirm that the child exited, `CapiProxy` +retains the child handle so callers can retry cleanup with `stop()`. +Once shutdown is acknowledged, it waits for the child to finish flushing captures +and exit rather than applying a forced-termination timeout to the flush. + +Owned-stdio shutdown regressions share +`test/harness/stdio-shutdown-runtime.cjs` across all six SDKs. Launch it with +Node and arguments ` `. The fixture acknowledges +`runtime.shutdown`, but writes its cleanup marker only after stdin EOF, matching +the native wrapper's host-finalization boundary. Language-native tests exercise +graceful stop/disposal, force-stop where exposed, a child that ignores EOF, and +failed-startup cleanup. Keep those lifecycle expectations aligned when changing +an SDK transport; test watchdogs must allow all cleanup phases their separate +budgets, rather than treating the graceful-exit timeout as a total shutdown cap. + The shared harness records real inference responses under `test/snapshots`. Record new captures with `GITHUB_TOKEN` set and `GITHUB_ACTIONS` unset; never author model responses by hand. Rerun with `GITHUB_ACTIONS=true` and real @@ -281,10 +422,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 | | --- | --- | @@ -299,16 +442,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/README.md b/README.md index b2ef69d053..1ae92a18a9 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,17 @@ Your Application The SDK manages the CLI process lifecycle automatically. You can also connect to an external CLI server—see the [Getting Started Guide](./docs/getting-started.md#connecting-to-an-external-cli-server) for details on running the CLI in server mode. +### Agent Finder Skill installation APIs + +Runtimes that grant `skill-confirmed-installation` expose +`skills.planInstall`, `skills.applyInstall`, `skills.planUninstall`, +`skills.applyUninstall`, `skills.installations.list/recover/status/cancel`, +and `skills.installations.setEnabled`. Skill install reviews include the full +verified `SKILL.md` content up to 256 KiB, catalogue identity, file manifest, +and non-absolute personal target location. Summaries report `ownershipState`, +`configuredEnabled`, `sessionState`, retained catalogue identity and +`installedAt`. + ## FAQ ### Do I need a GitHub Copilot subscription to use the SDK? diff --git a/docs/developer-docs/secrets.md b/docs/developer-docs/secrets.md index 416a7b6bfb..b09ddd213c 100644 --- a/docs/developer-docs/secrets.md +++ b/docs/developer-docs/secrets.md @@ -1,29 +1,29 @@ # Secrets management -This document covers secrets management for the github/copilot-sdk repository. It lists the GitHub Actions secrets that maintainers must keep configured and not expired. +This document covers SDK build/test and automation secrets. Normal SDK package publishing runs from `github/copilot-agent-runtime` through its `publish.yml` entry workflow, using runtime-repository credentials and trusted publishers. Curated release notes and Java SNAPSHOT publishing run in the public SDK repository using its credentials. > [!WARNING] > If any of these secrets expire or are revoked, the corresponding workflows will fail silently or with opaque permission errors. Review this list periodically and rotate secrets before they expire. ## SDK test secrets -These secrets are used by the authoritative SDK build/test workflow and the publishing workflow. +These secrets are used by the authoritative SDK build/test workflow. * **`COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY`**: HMAC key used to authenticate with the Copilot Developer CLI integration endpoint during tests. Injected as `COPILOT_HMAC_KEY` in test environments. - * Workflows: `sdk.yml`, `publish.yml` + * Workflows: `sdk.yml` ## Agentic workflow secrets These secrets power the GitHub Agentic Workflows (gh-aw) used for issue triage, code generation, and release automation. * **`COPILOT_GITHUB_TOKEN`**: GitHub OAuth token consumed by the Copilot CLI for AI authentication. Required by all agentic workflows when invoking `copilot` for AI inference. - * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-smoke-test.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `sdk-consistency-review.lock.yml`, `cross-repo-issue-analysis.lock.yml` + * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` * **`GH_AW_GITHUB_TOKEN`**: Optional GitHub token override for repository operations (reading code, creating pull requests, and making GitHub API calls). If unset, workflows use the automatic `GITHUB_TOKEN`. - * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `sdk-consistency-review.lock.yml`, `cross-repo-issue-analysis.lock.yml` + * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` * **`GH_AW_GITHUB_MCP_SERVER_TOKEN`**: Optional token override for the GitHub MCP server container. If unset, workflows fall back to `GH_AW_GITHUB_TOKEN` and then the automatic `GITHUB_TOKEN`. - * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `sdk-consistency-review.lock.yml`, `cross-repo-issue-analysis.lock.yml` + * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` * **`GH_AW_CI_TRIGGER_TOKEN`**: Token used to trigger CI workflows from within agentic workflow runs. * Workflows: `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml` @@ -33,19 +33,21 @@ These secrets power the GitHub Agentic Workflows (gh-aw) used for issue triage, ## Java publishing secrets -These secrets support Java SDK Maven Central publishing, snapshot publishing, and post-release documentation deployment. +These secrets support Java SDK Maven Central publishing and post-release documentation deployment from the runtime repository. The SDK-side Java SNAPSHOT workflow uses `JAVA_MAVEN_CENTRAL_USERNAME` and `JAVA_MAVEN_CENTRAL_PASSWORD` in this repository. * **`JAVA_MAVEN_CENTRAL_USERNAME`**: Username generated by a Maven Central Portal user token. - * Workflows: `java-publish-maven.yml`, `java-publish-snapshot.yml` + * Runtime repository (`github/copilot-agent-runtime`): `publish.yml` and `sdk-publish-release.yml` + * SDK repository (`github/copilot-sdk`): `java-publish-snapshot.yml` * **`JAVA_MAVEN_CENTRAL_PASSWORD`**: Password or token for Maven Central (Sonatype OSSRH) authentication. - * Workflows: `java-publish-maven.yml`, `java-publish-snapshot.yml` + * Runtime repository (`github/copilot-agent-runtime`): `publish.yml` and `sdk-publish-release.yml` + * SDK repository (`github/copilot-sdk`): `java-publish-snapshot.yml` * **`JAVA_GPG_SECRET_KEY`**: GPG private key used to sign Java release artifacts for Maven Central. - * Workflows: `java-publish-maven.yml` + * Runtime workflow: `publish.yml` and its reusable SDK publishing jobs * **`JAVA_GPG_PASSPHRASE`**: Passphrase for the GPG signing key. - * Workflows: `java-publish-maven.yml` + * Runtime workflow: `publish.yml` and its reusable SDK publishing jobs * **`JAVA_RELEASE_GITHUB_TOKEN`**: GitHub token with **workflow dispatch** (actions:write) permission on `github/copilot-sdk-java`. Used to trigger the documentation site deployment after a release is published. * Workflows: `publish.yml` @@ -58,10 +60,11 @@ These secrets support Java SDK Maven Central publishing, snapshot publishing, an ## Secrets not managed in this repository * **`GITHUB_TOKEN`**: Automatically provided by GitHub Actions. No manual management required. - The runtime-driven Node SDK workflow grants it `packages: read` only while acquiring - private runtime packages from GitHub Packages. + SDK release packaging consumes runtime artifacts from the same workflow run + instead of acquiring private runtime packages from GitHub Packages. ## Further reading * [GitHub docs: Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) * [Repository secrets settings](https://github.com/github/copilot-sdk/settings/secrets/actions) (maintainer access required) +* [Runtime publishing configuration](https://github.com/github/copilot-agent-runtime/blob/main/docs/developer-docs/secrets.md#unified-publishing-configuration) (runtime repository access required) diff --git a/docs/features/README.md b/docs/features/README.md index 5ea070a5aa..3cf785ecb9 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -11,6 +11,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [The Agent Loop](./agent-loop.md) | How the CLI processes a prompt—the tool-use loop, turns, and completion signals | | [Hooks](./hooks.md) | Intercept and customize session behavior—control tool execution, transform results, handle errors | | [Custom Agents](./custom-agents.md) | Define specialized sub-agents with scoped tools and instructions | +| [Changing Tools](./changing-tools.md) | Replace the tools a client supplies to a live session | | [Fleet Mode](./fleet-mode.md) | Dispatch multiple sub-agents in parallel for large, independent workstreams | | [MCP Servers](./mcp.md) | Integrate Model Context Protocol servers for external tool access | | [Skills](./skills.md) | Load reusable prompt modules from directories | diff --git a/docs/features/changing-tools.md b/docs/features/changing-tools.md new file mode 100644 index 0000000000..93f0938ec9 --- /dev/null +++ b/docs/features/changing-tools.md @@ -0,0 +1,372 @@ +# Changing tools during a session + +An application can replace the tools it supplies to a live session without recreating it. For example, a web page can offer different tools as the user navigates, or a client that takes over a session can bring its own tools. Each SDK replaces the client's complete tool set and the handlers that serve it in one call. + +> **Experimental.** Tool replacement wraps the experimental `session.tools.set` RPC and requires a runtime that supports it. + +## Replace this client's tools + +Pass the complete set of tools this client should supply. It replaces the tools the client supplied when the session was created or resumed, or in an earlier replacement. Built-in, MCP, and plugin tools, and tools that other clients connected to the same session supply, are unaffected. Pass an empty list to remove all of this client's tools. + +Tools are defined exactly as they are for creating a session, and a tool without a handler behaves the same way it does there. + +
+TypeScript + + +```typescript +import { CopilotClient, approveAll, defineTool } from "@github/copilot-sdk"; +import { z } from "zod"; + +declare function findIssues(query: string): Promise; + +async function main() { + const client = new CopilotClient(); + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const searchIssues = defineTool("search_issues", { + description: "Search the issues shown on the current page", + parameters: z.object({ query: z.string().describe("Search text") }), + handler: async ({ query }) => findIssues(query), + }); + + // Supply only the tools the current page provides. + await session.setTools([searchIssues]); + + // Remove all of this client's tools. + await session.setTools([]); +} + +main(); +``` + + +```typescript +import { defineTool } from "@github/copilot-sdk"; +import { z } from "zod"; + +const searchIssues = defineTool("search_issues", { + description: "Search the issues shown on the current page", + parameters: z.object({ query: z.string().describe("Search text") }), + handler: async ({ query }) => findIssues(query), +}); + +// Supply only the tools the current page provides. +await session.setTools([searchIssues]); + +// Remove all of this client's tools. +await session.setTools([]); +``` + +
+
+Python + + +```python +from pydantic import BaseModel, Field + +from copilot import CopilotClient +from copilot.session import PermissionHandler +from copilot.tools import define_tool + + +async def find_issues(query: str) -> str: + return query + + +class SearchIssuesParams(BaseModel): + query: str = Field(description="Search text") + + +@define_tool(description="Search the issues shown on the current page") +async def search_issues(params: SearchIssuesParams) -> str: + return await find_issues(params.query) + + +async def main() -> None: + client = CopilotClient() + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + + # Supply only the tools the current page provides. + await session.set_tools([search_issues]) + + # Remove all of this client's tools. + await session.set_tools([]) +``` + + +```python +from pydantic import BaseModel, Field +from copilot.tools import define_tool + +class SearchIssuesParams(BaseModel): + query: str = Field(description="Search text") + +@define_tool(description="Search the issues shown on the current page") +async def search_issues(params: SearchIssuesParams) -> str: + return await find_issues(params.query) + +# Supply only the tools the current page provides. +await session.set_tools([search_issues]) + +# Remove all of this client's tools. +await session.set_tools([]) +``` + +
+
+Go + + +```go +package main + +import ( + "context" + "log" + + copilot "github.com/github/copilot-sdk/go" +) + +type SearchIssuesParams struct { + Query string `json:"query" jsonschema:"Search text"` +} + +func findIssues(query string) (string, error) { + return query, nil +} + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + log.Fatal(err) + } + + searchIssues := copilot.DefineTool( + "search_issues", + "Search the issues shown on the current page", + func(params SearchIssuesParams, inv copilot.ToolInvocation) (string, error) { + return findIssues(params.Query) + }, + ) + + // Supply only the tools the current page provides. + if err := session.SetTools(ctx, []copilot.Tool{searchIssues}); err != nil { + log.Fatal(err) + } + + // Remove all of this client's tools. + if err := session.SetTools(ctx, nil); err != nil { + log.Fatal(err) + } +} +``` + + +```go +type SearchIssuesParams struct { + Query string `json:"query" jsonschema:"Search text"` +} + +searchIssues := copilot.DefineTool( + "search_issues", + "Search the issues shown on the current page", + func(params SearchIssuesParams, inv copilot.ToolInvocation) (string, error) { + return findIssues(params.Query) + }, +) + +// Supply only the tools the current page provides. +if err := session.SetTools(ctx, []copilot.Tool{searchIssues}); err != nil { + log.Fatal(err) +} + +// Remove all of this client's tools. +if err := session.SetTools(ctx, nil); err != nil { + log.Fatal(err) +} +``` + +
+
+.NET + + +```csharp +#pragma warning disable GHCP001 +using System.ComponentModel; +using GitHub.Copilot; +using Microsoft.Extensions.AI; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + OnPermissionRequest = PermissionHandler.ApproveAll, +}); + +var searchIssues = CopilotTool.DefineTool( + ([Description("Search text")] string query) => FindIssues(query), + factoryOptions: new AIFunctionFactoryOptions + { + Name = "search_issues", + Description = "Search the issues shown on the current page", + }); + +// Supply only the tools the current page provides. +await session.SetToolsAsync([searchIssues]); + +// Remove all of this client's tools. +await session.SetToolsAsync([]); + +static string FindIssues(string query) => query; +#pragma warning restore GHCP001 +``` + + +```csharp +var searchIssues = CopilotTool.DefineTool( + ([Description("Search text")] string query) => FindIssues(query), + factoryOptions: new AIFunctionFactoryOptions + { + Name = "search_issues", + Description = "Search the issues shown on the current page", + }); + +// Supply only the tools the current page provides. +await session.SetToolsAsync([searchIssues]); + +// Remove all of this client's tools. +await session.SetToolsAsync([]); +``` + +`SetToolsAsync` raises the `GHCP001` experimental diagnostic, which you suppress with `#pragma warning disable GHCP001` or a project-level `GHCP001`. + +
+
+Java + + +```java +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotClient; +import com.github.copilot.CopilotSession; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.ToolDefinition; + +@AllowCopilotExperimental +public class ChangingToolsExample { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + var searchIssues = ToolDefinition.create( + "search_issues", + "Search the issues shown on the current page", + Map.of( + "type", "object", + "properties", Map.of("query", Map.of("type", "string", "description", "Search text")), + "required", List.of("query")), + invocation -> CompletableFuture.completedFuture( + findIssues((String) invocation.getArguments().get("query")))); + + // Supply only the tools the current page provides. + session.setTools(List.of(searchIssues)).get(); + + // Remove all of this client's tools. + session.setTools(List.of()).get(); + } + } + + private static String findIssues(String query) { + return query; + } +} +``` + + +```java +var searchIssues = ToolDefinition.create( + "search_issues", + "Search the issues shown on the current page", + Map.of( + "type", "object", + "properties", Map.of("query", Map.of("type", "string", "description", "Search text")), + "required", List.of("query")), + invocation -> CompletableFuture.completedFuture( + findIssues((String) invocation.getArguments().get("query")))); + +// Supply only the tools the current page provides. +session.setTools(List.of(searchIssues)).get(); + +// Remove all of this client's tools. +session.setTools(List.of()).get(); +``` + +`setTools` is `@CopilotExperimental`, so the consuming class or method must opt in with `@AllowCopilotExperimental`. See [Using experimental APIs](../../java/README.md#using-experimental-apis). + +
+
+Rust + + + +```rust +#[derive(Deserialize, JsonSchema)] +struct SearchIssuesParams { + /// Search text + query: String, +} + +let search_issues = define_tool( + "search_issues", + "Search the issues shown on the current page", + |_inv, params: SearchIssuesParams| async move { + Ok(ToolResult::Text(find_issues(¶ms.query).await)) + }, +); + +// Supply only the tools the current page provides. +session.set_tools(vec![search_issues]).await?; + +// Remove all of this client's tools. +session.set_tools(Vec::new()).await?; +``` + +
+ +## When the new tools take effect + +- **Model requests.** The runtime offers the new tools from the agent's next model request. That request can be part of a turn that is already in progress. +- **Handlers.** As soon as the runtime accepts the replacement, every tool call the session dispatches uses the new handlers. Calls that are already running finish on the handlers that started them. +- **Rejection.** If the runtime rejects the replacement, the call fails and the previous tools and handlers stay in place. The runtime rejects tool names with invalid characters and names that another connected client already supplies. +- **Ordering.** Concurrent replacements on the same session are applied one at a time, in the order they're made. +- **Cancellation.** In SDKs that let you cancel the call, cancelling it while an earlier replacement is still in flight sends nothing. Once the request is sent, cancelling only stops the wait: if the runtime accepts the replacement, the new handlers still take effect. + +## Replacing tools during a turn + +A model request that is already in flight was made with the previous tools, so the agent can still call a tool you just removed. The session doesn't answer that call, because another connected client might supply a tool with the same name, so the call can stay pending until the turn is aborted. + +If a running turn might still call a tool you remove, replace tools while the session is idle, for example after the `session.idle` event, or abort the turn first. Replacing a tool's handler under the same name is safe during a turn: calls dispatched after the runtime accepts the replacement use the new handler. + +## SDK reference + +| SDK | Method | +|---|---| +| TypeScript | `session.setTools(tools)` | +| Python | `await session.set_tools(tools)` | +| Go | `session.SetTools(ctx, tools)` | +| .NET | `await session.SetToolsAsync(tools, cancellationToken)` | +| Java | `session.setTools(tools)`, which returns a `CompletableFuture` | +| Rust | `session.set_tools(tools).await` | diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md index 891a8ef04f..9a9547c8b9 100644 --- a/docs/features/fleet-mode.md +++ b/docs/features/fleet-mode.md @@ -243,7 +243,9 @@ This pattern gives every worker a clear owner and lets the parent session reason Fleet mode invokes sub-agents through the runtime's task mechanism. The runtime emits hook activity for sub-agent tool calls: the runtime 1.0.52 changelog notes that `preToolUse`, `postToolUse`, `subagentStart`, and `subagentStop` fire correctly for sub-agent tool calls. -A dedicated SDK hook callback for `subagentStart` or `subagentStop` was not found in the public SDK surface on this branch. SDK consumers can observe sub-agent activity through the generic session event stream, which includes events such as `subagent.started`, `subagent.completed`, `subagent.failed`, `subagent.selected`, and `subagent.deselected`. +All six SDKs expose typed sub-agent lifecycle hooks: Node.js uses `onSubagentStart` and `onSubagentStop`, Python and Rust use `on_subagent_start` and `on_subagent_stop`, Go and .NET use `OnSubagentStart` and `OnSubagentStop`, and Java uses `setOnSubagentStart` and `setOnSubagentStop`. Register them when creating or resuming a session. The start hook can prepend `additionalContext` to the child's first prompt; the stop hook can request another child turn with a block reason or replace the response reported to the parent. + +For passive status updates, subscribe to the generic session event stream. It includes `subagent.started`, `subagent.completed`, `subagent.failed`, `subagent.selected`, and `subagent.deselected`:
Node.js / TypeScript @@ -338,7 +340,7 @@ Keep plugin-provided sub-agent types narrow and descriptive so the orchestrator * Fleet mode is exposed through generated session RPC bindings and is marked experimental in several SDKs. * The SQL todos pattern is the canonical coordination model in the runtime guidance, but whether it is a stable extensibility contract for SDK consumers is still an open question. -* `subagentStart` and `subagentStop` are runtime hook names; this branch exposes sub-agent lifecycle to SDK consumers through the generic session event stream, not dedicated hook callbacks. +* Sub-agent lifecycle hook inputs identify the parent session but do not yet carry a shared tool-call ID for correlating with `subagent.*` events. * Plugin sub-agent registration is configured at the runtime layer through `--plugin-dir`; no SDK-level plugin registration helper was verified on this branch. * Java native typed bindings for `session.fleet.start` were not found in the Java SDK source on this branch. * Fleet mode does not remove the need for parent-agent review. Parallel workers can produce inconsistent assumptions that the orchestrator must reconcile. diff --git a/docs/features/mcp.md b/docs/features/mcp.md index 19f12e285b..cf3003ce8c 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -183,6 +183,57 @@ On session creation and a **cold** resume, disabled servers are not started and the runtime does not initiate their authentication. A resident resume cannot undo a server that the runtime has already spawned. Names are matched exactly. +## Listing and retrieving prompts + +The experimental generated MCP namespace exposes the wire JSON-RPC methods +`session.mcp.prompts.list` and `session.mcp.prompts.get`. Use the language-specific +SDK accessors below to call them. These methods target one connected server in the +current session; they do not combine results from multiple servers. + +| SDK | List prompts | Get a prompt | +| --- | --- | --- | +| Node.js | `session.rpc.mcp.prompts.list(...)` | `session.rpc.mcp.prompts.get(...)` | +| Python | `session.rpc.mcp.prompts.list(...)` | `session.rpc.mcp.prompts.get(...)` | +| Go | `session.RPC.MCP.Prompts().List(...)` | `session.RPC.MCP.Prompts().Get(...)` | +| .NET | `session.Rpc.Mcp.Prompts.ListAsync(...)` | `session.Rpc.Mcp.Prompts.GetAsync(...)` | +| Java | `session.getRpc().mcp.prompts.list(...)` | `session.getRpc().mcp.prompts.get(...)` | +| Rust | `session.rpc().mcp().prompts().list(...)` | `session.rpc().mcp().prompts().get(...)` | + +Listing requires `serverName` and accepts an optional opaque `cursor`. Each +call returns one page of typed prompt definitions and an optional `nextCursor`. +Pass that cursor in another list call to request the next page. Definitions +include names, optional titles and descriptions, and argument definitions. +An omitted argument `required` flag remains distinct from `false`. + +Getting a prompt requires `serverName` and `promptName`, with an optional +`arguments` dictionary whose values are strings. Omitting `arguments` leaves the +MCP request's arguments absent; passing `{}` sends an explicitly empty dictionary. +The runtime preserves this distinction for the server. The result contains an optional +description and ordered messages with typed roles and opaque JSON `content`. +Inspect each content block's `type` before interpreting it; content is not +flattened into text. Nested resources, annotations, metadata, unfamiliar content +types, and additional content fields retain their JSON structure. Server +extensions on typed descriptors, messages, and result envelopes are available under +`additionalProperties`. A server extension itself named `additionalProperties` +is preserved as an entry inside that map, not merged into it. +The protocol's top-level `resultType: "complete"` discriminator is consumed by +the runtime; it is not returned as an SDK field or a server extension. +Requests fail explicitly if the runtime cannot preserve the raw response, +including stdio response frames exceeding the 1 MiB capture limit. + +The returned prompt messages are not automatically sent to the model or used to +execute tools or fetch referenced resources. Your application decides how to +use the result. +While generating that result, an MCP server can request sampling or elicitation +through multi round-trip continuations. These requests use the same configured +host responders as other MCP operations; prompt retrieval does not grant +additional permission or bypass the host's decision. A missing responder or +responder error fails the request, and an elicitation decline is returned to the +server unchanged. State-only continuations do not invoke either responder. + +After an existing `mcp.prompts.list_changed` session event, list the named +server's prompts again to refresh your application's view. + ## Tool configuration You can control which tools are available to an MCP server using the `tools` field. @@ -295,6 +346,53 @@ directories for different applications. | `tools` | `string[]` | No | Tools to enable | | `timeout` | `number` | No | Timeout in milliseconds | +## OAuth ownership for remote servers + +Choose one OAuth ownership model for each authentication attempt: + +1. **Runtime-managed loopback**: Call `session.mcp.oauth.login` without a + redirect URI. The runtime opens a local callback listener and owns discovery, + PKCE, state validation, token exchange, persistence, refresh, and reconnect. +1. **Runtime-managed hosted callback**: Call `session.mcp.oauth.login` with a + trusted public HTTPS redirect URI. Your host receives the callback, but the + runtime continues to own the OAuth protocol and credentials. +1. **Host-managed OAuth**: Use the SDK's MCP authentication request handler and + return a host-acquired access token. Your host owns authorization and refresh. + +Do not register a host-managed OAuth handler for an authentication attempt that +uses a runtime-managed hosted callback. Wait until the remote server enters the +`needs-auth` state, then call the generated `session.rpc.mcp.oauth.login` method +directly with the server name and callback URI: + +```jsonc +{ + "serverName": "remote-mcp", + "redirectUri": "https://agent.example.com/oauth/callback" +} +``` + +The selected static, CIMD, or dynamically registered OAuth client must advertise +that exact URI. The URI must use HTTPS and must not contain a query or fragment. +When browser interaction is required, the result contains `authorizationUrl` +and `authorizationId`. Open `authorizationUrl` in the user's browser and retain +`authorizationId` with the target session. + +After the authorization server redirects to your endpoint, call the generated +`session.rpc.mcp.oauth.complete` method: + +```jsonc +{ + "authorizationId": "", + "callbackUrl": "https://agent.example.com/oauth/callback?code=...&state=..." +} +``` + +Construct `callbackUrl` from the configured public redirect origin and path plus +the callback's original query. Do not pass an internal service URL or trust +client-supplied `Forwarded` or `X-Forwarded-*` headers. The runtime validates the +origin, port, path, state, and OAuth response before accepting delivery. Token +exchange, persistence, and MCP reconnect then continue asynchronously. + ## Session-scoped MCP diagnostics Set `diagnostics: { sources: { mcp: { level: "debug" } } }` when you create or diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 69fda41fba..302844b347 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -131,6 +131,30 @@ await session.SendAndWaitAsync(new MessageOptions { Prompt = "Analyze my codebas Later—minutes, hours, or even days—you can resume the session from where you left off. +### Transcript recovery + +The resume option `allowTranscriptRecovery` controls recovery when the runtime loads a +transcript from storage. It defaults to `true` in all modes. With `false`, a load that +would discard damaged records or move `session.start` fails with JSON-RPC error `-32075` +without rewriting the transcript. An intact final record without a newline is accepted. + +Records skipped for forward compatibility still count when checking persisted order. +If such records precede `session.start`, recovery must move the start record ahead of +them. A transcript containing only skipped records is not empty and cannot be recovered +without a valid `session.start`. + +When recovery is allowed, the returned session exposes `transcriptRecovery`, including +the invalid line numbers, whether `session.start` moved, and a planned byte-exact backup +path. Loading alone does not write that backup; the next durable append performs the +repair and backup. + +> [!NOTE] +> This option applies to a new transcript load, not to the lifetime of a session. +> Reconnecting to a session already resident in the runtime reuses its live history +> without revalidating storage or rejecting recovery authorized by an earlier resume. +> The recovery report describes the load that performed recovery, not every subsequent +> reconnect. + ```mermaid flowchart LR subgraph Day1["Day 1"] diff --git a/docs/features/skills.md b/docs/features/skills.md index d76196429e..df8d319928 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -336,6 +336,333 @@ The frontmatter fields: The markdown body contains the instructions that are injected into the session context when the skill is loaded. +## Skill providers (experimental) + +> [!NOTE] +> Skill providers are experimental. The API can change in future SDK releases. + +A skill provider serves skills from your application's own storage, such as a database in a multi-tenant service, instead of `SKILL.md` files on disk. Provider skills join the session's skill catalog alongside file-based skills. The model loads them on demand through the `skill` tool, and users can invoke them like any other skill. + +A provider implements two operations: + +* **List skills**: returns catalog metadata for every skill: a `name`, a `description`, and optionally `userInvocable`, `disableModelInvocation`, and `argumentHint`. The runtime calls it when it loads the session's skills. +* **Read skill**: returns the markdown for one skill, or a not-found result if the skill no longer exists. The runtime calls it only when the skill is loaded. + +Pass the provider when you create or resume a session: + +
+Node.js / TypeScript + +```typescript +import { approveAll, CopilotClient, type SkillProvider } from "@github/copilot-sdk"; + +const releaseSkills = new Map([ + [ + "release-notes", + { + description: "Writes release notes in the team's format.", + markdown: "Group changes by feature area and link each pull request.", + }, + ], +]); + +const skillProvider: SkillProvider = { + listSkills: () => + [...releaseSkills].map(([name, skill]) => ({ name, description: skill.description })), + readSkill: (name) => releaseSkills.get(name)?.markdown ?? null, +}; + +const client = new CopilotClient(); +const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider, +}); +``` + +
+
+Python + +```python +from copilot import CopilotClient, SkillProviderDescriptor +from copilot.session import PermissionHandler + +RELEASE_SKILLS = { + "release-notes": ( + "Writes release notes in the team's format.", + "Group changes by feature area and link each pull request.", + ), +} + + +class ReleaseSkills: + async def list_skills(self) -> list[SkillProviderDescriptor]: + return [ + SkillProviderDescriptor(name=name, description=description) + for name, (description, _) in RELEASE_SKILLS.items() + ] + + async def read_skill(self, name: str) -> str | None: + skill = RELEASE_SKILLS.get(name) + return skill[1] if skill else None + + +async def main(): + client = CopilotClient() + await client.start() + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=ReleaseSkills(), + ) +``` + +
+
+Go + +```go +package main + +import ( + "context" + "fmt" + "log" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + +type releaseSkill struct { + description string + markdown string +} + +type releaseSkills map[string]releaseSkill + +func (s releaseSkills) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + descriptors := make([]rpc.SkillProviderDescriptor, 0, len(s)) + for name, skill := range s { + descriptors = append(descriptors, rpc.SkillProviderDescriptor{Name: name, Description: skill.description}) + } + return descriptors, nil +} + +func (s releaseSkills) ReadSkill(ctx context.Context, name string) (string, error) { + skill, ok := s[name] + if !ok { + return "", fmt.Errorf("%w: %s", copilot.ErrSkillNotFound, name) + } + return skill.markdown, nil +} + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: releaseSkills{ + "release-notes": { + description: "Writes release notes in the team's format.", + markdown: "Group changes by feature area and link each pull request.", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + _ = session +} +``` + +
+
+.NET + +```csharp +#pragma warning disable GHCP001 // Skill providers are experimental. +using GitHub.Copilot; +using GitHub.Copilot.Rpc; + +public sealed class ReleaseSkills : ISkillProvider +{ + private readonly Dictionary _skills = new() + { + ["release-notes"] = ( + "Writes release notes in the team's format.", + "Group changes by feature area and link each pull request."), + }; + + public Task> ListSkillsAsync(CancellationToken cancellationToken) => + Task.FromResult>( + _skills.Select(skill => new SkillProviderDescriptor + { + Name = skill.Key, + Description = skill.Value.Description, + }).ToList()); + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(_skills.TryGetValue(name, out var skill) ? skill.Markdown : null); +} + +public static class SkillProviderExample +{ + public static async Task RunAsync(CopilotClient client) + { + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SkillProvider = new ReleaseSkills(), + }); + } +} +``` + +The .NET skill provider types raise the `GHCP001` experimental diagnostic. Suppress it with `#pragma warning disable GHCP001` or a project-level `GHCP001`. + +
+
+Java + +```java +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotClient; +import com.github.copilot.CopilotSession; +import com.github.copilot.SkillProvider; +import com.github.copilot.SkillProviderDescriptor; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@AllowCopilotExperimental +class ReleaseSkills implements SkillProvider { + private final Map markdown = Map.of( + "release-notes", "Group changes by feature area and link each pull request."); + + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of(new SkillProviderDescriptor( + "release-notes", "Writes release notes in the team's format.", null, null, null))); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(markdown.get(name)); + } +} + +@AllowCopilotExperimental +class SkillProviderExample { + static CopilotSession createSession(CopilotClient client) throws Exception { + return client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillProvider(new ReleaseSkills())) + .get(); + } +} +``` + +The Java skill provider API is `@CopilotExperimental`, so the consuming class or method must opt in with `@AllowCopilotExperimental` (or compile with `-Acopilot.experimental.allowed=true`). See [Using experimental APIs](../../java/README.md#using-experimental-apis). + +
+
+Rust + + +```rust +use std::sync::Arc; + +use async_trait::async_trait; +use github_copilot_sdk::{Error, SessionConfig, SkillProvider, SkillProviderDescriptor}; + +struct ReleaseSkills; + +#[async_trait] +impl SkillProvider for ReleaseSkills { + async fn list_skills(&self) -> Result, Error> { + Ok(vec![SkillProviderDescriptor { + name: "release-notes".into(), + description: "Writes release notes in the team's format.".into(), + ..Default::default() + }]) + } + + async fn read_skill(&self, name: &str) -> Result, Error> { + Ok((name == "release-notes") + .then(|| "Group changes by feature area and link each pull request.".to_string())) + } +} + +let session = client + .create_session( + SessionConfig::default() + .approve_all_permissions() + .with_skill_provider(Arc::new(ReleaseSkills)), + ) + .await?; +``` + +
+ +### Provider skill content + +Read skill returns the skill's `SKILL.md` text. YAML frontmatter is optional: + +* Without frontmatter, the whole text is the skill body, and all metadata comes from the listed skill. +* With frontmatter, an omitted field inherits the listed value. A field that you include must match the listed value, or the skill fails to load. +* `allowed-tools` is read only from frontmatter. +* If the first line of the text is `---`, the runtime parses it as frontmatter, so don't start a body-only skill with a Markdown thematic break. + +Provider skills are text-only. They have no base directory, so they can't reference bundled scripts, templates, or other files. `skills.list` reports them with the source `sdk` and an empty `path`. + +### Serving existing SKILL.md files + +To serve `SKILL.md` files that you already have, such as files stored in a database, parse each file's YAML frontmatter once to build its catalog entry, then return the file unchanged from read skill: + +* Map `name` and `description` to the listed skill's `name` and `description`. Both are required in the catalog; the runtime doesn't fall back to a folder name or the skill body. +* Map `user-invocable`, `disable-model-invocation`, and `argument-hint` to the matching optional fields when the frontmatter sets them. +* Return the original text, frontmatter included, from read skill. Because the listed values came from the same frontmatter, they match, and the runtime still reads `allowed-tools` from it. + +### Trusting provider content + +Treat provider skills like skill directories: their content is trusted input. The model follows a skill's instructions, and the frontmatter controls how the skill is invoked. `allowed-tools` doesn't grant permissions in SDK sessions. The runtime reports it in the `allowedTools` field of the `skill.invoked` event, and your permission handler still decides every tool request. + +Don't serve text that end users or other tenants can edit unless you would let them author a skill file. If you build skills from user input, generate the frontmatter yourself instead of passing user-supplied frontmatter through. + +### Provider limits and errors + +The runtime validates the provider's catalog and content: + +* Names must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` and be unique, ignoring case. +* A catalog can contain at most 1,024 skills and 1 MiB of metadata. Each description and argument hint can be at most 1,024 characters. +* Each skill's markdown can be at most 1 MiB of UTF-8. +* Each call must complete within 30 seconds. When the runtime stops waiting for a call, because it timed out, the session ended, or a resume replaced the provider, it cancels the request and ignores any later result. Each SDK passes that cancellation to your provider in its usual way: + + | SDK | Cancellation signal | + |---|---| + | Node.js | The `signal` (`AbortSignal`) in the options passed to `listSkills` and `readSkill` is aborted. | + | Python | The provider's `asyncio` task is cancelled with `asyncio.CancelledError`. Synchronous providers run to completion. | + | Go | The call's `context.Context` is cancelled. | + | .NET | The call's `CancellationToken` is cancelled. | + | Java | The returned `CompletableFuture` is cancelled with `cancel(true)`, which doesn't interrupt running work. | + | Rust | The provider future is dropped. | + + These signals cover calls that the runtime cancels. When the connection closes or the client is force-stopped, the .NET, Java, and Rust SDKs also cancel calls that are still running. The Node.js, Python, and Go SDKs don't: a running call continues until it returns, and the SDK discards its result. + +If listing fails or returns an invalid catalog, the runtime reports the problem in the `errors` list returned by `skills.reload`. A failed reload keeps the provider skills from the last successful list. If reading a skill fails, the `skill` tool reports a generic load failure to the model. The SDK never forwards the text of errors your provider throws or returns, so that text can't leak into the conversation. + +### Provider lifecycle + +A provider is bound to one session: + +* A provider enables skills unless you set `enableSkills` to `false`, which keeps the provider bound but never called. In `mode: "empty"`, skills stay disabled until you set `enableSkills` to `true`. +* The provider is never persisted. Pass it again when you resume a session. Resuming without a provider removes it from the session. Extensions that join the session don't change it. +* A provider skill with the same name as a file-based skill replaces the file-based skill, and `skills.reload` reports a warning. +* Sub-agents use their parent session's provider. +* The runtime can call the provider concurrently, for example from the `skill` tool, user invocations, and custom agents that preload skills, so both operations must be safe for concurrent use. +* Cloud sessions don't support skill providers. The SDK rejects the configuration before it creates the session. + ## Configuration options ### SessionConfig skill fields @@ -344,12 +671,18 @@ The markdown body contains the instructions that are injected into the session c |----------|-------|------|-------------| | Node.js | `skillDirectories` | `string[]` | Directories to load skills from | | Node.js | `disabledSkills` | `string[]` | Skills to disable | +| Node.js | `skillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | Python | `skill_directories` | `list[str]` | Directories to load skills from | | Python | `disabled_skills` | `list[str]` | Skills to disable | +| Python | `skill_provider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | Go | `SkillDirectories` | `[]string` | Directories to load skills from | | Go | `DisabledSkills` | `[]string` | Skills to disable | +| Go | `SkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | .NET | `SkillDirectories` | `List` | Directories to load skills from | | .NET | `DisabledSkills` | `List` | Skills to disable | +| .NET | `SkillProvider` | `ISkillProvider` | Experimental provider for SDK-supplied skills | +| Java | `setSkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | +| Rust | `with_skill_provider` | `Arc` | Experimental provider for SDK-supplied skills | ### Built-in skills and `mode: "empty"` @@ -367,6 +700,22 @@ casing) to their names. You can also opt into your **own** custom skills under remain fully usable, including a custom skill that shares a name with a built-in. Under `mode: "copilot-cli"` the field is omitted unless you set the option. +Custom skill directories are scanned when skills first load, including directories +that are missing or empty at that point. Filesystem changes in those directories +do not automatically refresh the catalog on later turns. After adding, editing, +or removing skills, call your session's `skills.reload` RPC: + +| SDK | Reload call | +|-----|-------------| +| Node.js | `await session.rpc.skills.reload()` | +| Python | `await session.rpc.skills.reload()` | +| Go | `session.RPC.Skills.Reload(ctx)` | +| .NET | `await session.Rpc.Skills.ReloadAsync()` | +| Java | `session.getRpc().skills.reload().join()` | +| Rust | `session.rpc().skills().reload().await?` | + +Changing the configured skill directories also refreshes the catalog. + ## Best practices 1. **Organize by domain** - Group related skills together (e.g., `skills/security/`, `skills/testing/`) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 0292f98e00..dbb6111897 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -532,12 +532,31 @@ Emitted when a tool begins executing. ### `tool.execution_partial_result` -Ephemeral. Incremental output from a running tool (e.g., streaming bash output). +Deprecated. Use `tool.shell_output` for live terminal output. This ephemeral event remains emitted alongside the replacement during migration; subscribe to only one representation to avoid displaying the same output twice. | Data Field | Type | Required | Description | |------------|------|----------|-------------| | `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` | -| `partialOutput` | `string` | ✅ | Incremental output chunk | +| `partialOutput` | `string` | ✅ | Bounded, cumulative replacement snapshot with stdout and stderr merged; not an append-only chunk | + +### `tool.shell_output` + +Ephemeral. Append-only output from an attached terminal command or a user-requested terminal command. Each event contains newly observed, secret-filtered text, without the separators or markers used to format final tool results for the model. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Identifies the originating tool call | +| `text` | `string` | ✅ | New text to append, not a replacement snapshot | +| `stream` | `"stdout" \| "stderr" \| "terminal"` | | Output source; omitted means `"stdout"`. `"terminal"` identifies inherently merged PTY output, whose stdout/stderr origin cannot be recovered | +| `sequence` | `number` | ✅ | Starts at zero and increases across all streams for this tool call, in observed publication order | + +Accumulate `text` separately by `toolCallId` and `stream`, or join the chunks in sequence order for a combined display. A sequence number identifies duplicates and missing chunks; it does not establish exact ordering between independent operating-system writes. A late subscriber can receive a first sequence greater than zero. + +These events are live-only: they are not persisted or replayed after resume. Final `tool.execution_complete` results, model-facing formatting, and detached command capture are unchanged. Text can contain terminal control sequences; renderers must apply their normal terminal-text handling. + +An attached terminal command can keep running after `tool.execution_complete` when it runs asynchronously or continues beyond `initial_wait`. Output received while it continues uses the original `toolCallId` and continues the same `sequence`, rather than restarting at zero. Consumers that display only active calls must ignore chunks received after completion instead of recreating completed-call state. + +Secret filtering operates on each decoded chunk. The runtime does not artificially split lines, but operating-system reads can divide a line or secret across chunks; cross-chunk secret reconstruction is not provided. ### `tool.execution_progress` @@ -837,7 +856,7 @@ A skill was activated for the current conversation. | `name` | `string` | ✅ | Skill name | | `path` | `string` | ✅ | File path to the SKILL.md definition | | `content` | `string` | ✅ | Full skill content injected into the conversation | -| `allowedTools` | `string[]` | | Tools auto-approved while this skill is active | +| `allowedTools` | `string[]` | | Tools listed in the skill's `allowed-tools` frontmatter. The SDK doesn't approve them automatically; your permission handler still decides. The Copilot CLI auto-approves them while the skill is active. | | `pluginName` | `string` | | Plugin the skill originated from | | `pluginVersion` | `string` | | Plugin version | @@ -970,7 +989,7 @@ assistant.turn_start → Turn begins │ ├── permission.requested → Needs user approval │ ├── permission.completed → Approval result │ ├── tool.execution_start → Tool begins -│ ├── tool.execution_partial_result → Streaming tool output (ephemeral, repeated) +│ ├── tool.shell_output → Stream-tagged terminal output (ephemeral, repeated) │ ├── tool.execution_progress → Progress updates (ephemeral, repeated) │ ├── tool.execution_complete → Tool finished │ │ @@ -998,6 +1017,7 @@ This table lists key `data` payload fields. Common envelope fields are documente | `tool.user_requested` | | Tool | `toolCallId`, `toolName`, `arguments?` | | `tool.execution_start` | | Tool | `toolCallId`, `toolName`, `arguments?`, `mcpServerName?` | | `tool.execution_partial_result` | ✅ | Tool | `toolCallId`, `partialOutput` | +| `tool.shell_output` | ✅ | Tool | `toolCallId`, `text`, `stream?`, `sequence` | | `tool.execution_progress` | ✅ | Tool | `toolCallId`, `progressMessage` | | `tool.execution_complete` | | Tool | `toolCallId`, `success`, `result?`, `error?` | | `session.idle` | ✅ | Session | `aborted?` | diff --git a/docs/getting-started.md b/docs/getting-started.md index 5512da8531..b6172d6fc8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1843,6 +1843,14 @@ Copilot decides when to call your tool based on the user's question. When it doe 1. The result is sent back to Copilot 1. Copilot incorporates the result into its response +### Updating external tools in a running session + +The existing low-level `session.tools.set` request replaces the definitions owned by that SDK connection. Send the complete desired list (or an empty list to remove them), rather than a patch. The updated definitions apply to the next model request that reads them. During a tool round, a change processed before the post-tool refresh is visible on the next model call; a concurrent update processed after that refresh takes effect on a later request. It does **not** update the high-level SDK tool-handler map: register and manage handlers separately, or handle `external_tool.requested` and complete it with `session.tools.handlePendingToolCall`. + +On direct Anthropic API connections to `https://api.anthropic.com` using a model marked for Anthropic tool updates in the runtime's model catalog (currently Claude Fable 5 or 5.1, or Claude Opus 4.8, 5, or 5.5), the runtime can keep the original tool prefix and represent eligible between-turn SDK changes in provider-native conversation items to preserve a cacheable prefix. On the CAPI WebSocket Responses route for a model marked for Responses tool updates in that catalog (currently GPT-5.4 and 5.4 Mini, GPT-5.5, GPT-5.6 Luna, Sol, Terra, and Sol-fast, GPT-6 Astra, Sol, and Luna, and GPT-6.1 Sol), eligible append-only additions can use positioned `additional_tools` items. Eligible removals retain historical definitions and restrict callable tools with `tool_choice: { type: "allowed_tools", ... }`, or `"none"` when no tools remain. This removal optimization requires every callable survivor to be present in the original top-level tool list: CAPI rejects allowlist references to inline-added definitions when full history is resent. Removing an inline-added tool can preserve its historical position, but retaining a callable inline-added tool while removing another tool falls back to the complete current list. Schema replacements and unsupported tool-choice or provider-native tool configurations also use that fallback. + +Model support is an explicit catalog opt-in, independent of support for reasoning configuration updates, and other models should be enabled only after their route is verified. If historical changes no longer fit the prompt budget, or an optimized request receives a recognized pre-output provider rejection, the runtime returns to the current complete tool list. Anthropic through CAPI, unannotated OpenAI Responses models, other provider endpoints, mid-turn changes, and history rewrites use the current complete tool list. Removing every callable tool selects the runtime's existing single-shot HTTP path, which also uses the complete current list rather than this optimization. Successful compaction starts a new prefix with the tools currently provided; resuming in a new process does not replay previous tool changes. A stable request shape does not guarantee cached tokens or lower cost. + ## What's next? Now that you've got the basics, here are more powerful features to explore: diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index b3932ce66a..2078b0cfe5 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -112,6 +112,53 @@ let client = Client::start(ClientOptions::new() The OTLP protocol field configures the CLI's `"otlp-http"` exporter for all signals. Leave it unset to use the CLI default, or set it to `"http/protobuf"` to export protobuf over HTTP. +### Skill and command telemetry + +The runtime follows the [GenAI skill semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/commit/771e3210f7518694666834f7243573b8018c7a4f) +by refining existing `execute_tool` spans, not adding a separate skill +operation. A loader is named `execute_tool skill {skill}`. An unambiguously +identified resource or script is named +`execute_tool {tool} {skill} {resource}`, with a skill-relative resource name. +Other command spans append the actual known launcher name. + +* `gen_ai.skill.name` and `gen_ai.skill.resource.name`: Available independently of content capture +* `gen_ai.skill.description` and `gen_ai.skill.source.uri`: Require content capture through `TelemetryConfig` +* `process.executable.name`: The directly launched executable, such as the shell launcher, not a guessed command token +* `process.executable.path`: An absolute known path; requires content capture +* `process.exit.code`: An integer, including zero, when a command-executing tool reports an exit; unavailable exits and pre-spawn failures are omitted + +Poll tools do not inherit process attributes from the command they read. +Sandboxed executable identities are omitted when the sandbox backend does +not expose them. A nonzero exit does not imply that the tool itself failed. +Skill definitions and directory listings are not named skill resources. +Preloaded skills produce invocation events without a fabricated tool span. +Repeated preparation of the same selected-agent skill context emits one receipt; +changed content or provenance emits another. Child sessions report their own +preloads independently. Detached command spans identify the directly spawned +Unix `sh` supervisor or selected Windows PowerShell executable. + +Custom tools that override built-in names do not inherit native skill or process +attributes. Visible, enabled skills still own resources when model invocation +is disabled. Script attribution uses the post-hook command and the selected +shell's directory; it is omitted when startup scripts or profiles make the +execution context uncertain. + +While managed telemetry settings are unresolved, pending tracking buffers only +capture-independent tool facts. Descriptions, source URIs, and executable paths +observed before the policy resolves are omitted, even if capture is later enabled. +The final policy governs replay; disabled or cancelled tracking discards the buffer. + +These are breaking telemetry-name changes: use `gen_ai.skill.name` instead +of `github.copilot.tool.parameters.skill_name` or +`github.copilot.skill.name`, and use `gen_ai.skill.source.uri` instead of +`github.copilot.skill.path`. Local source values are file URIs, not bare paths. +The `github.copilot.skill.invoked` event and its content, source-category, +trigger, and plugin fields remain Copilot-specific. Skill content retains its +existing capture gate. + +The skill attributes and span refinements have Development stability; the +process attributes have Release Candidate stability. + ### Trace context propagation > **Most users don't need this.** The `TelemetryConfig` above is all you need to collect traces from the CLI. The trace context propagation described in this section is an **advanced feature** for applications that create their own OpenTelemetry spans and want them to appear in the **same distributed trace** as the CLI's spans. diff --git a/docs/runtime-supervised-host.md b/docs/runtime-supervised-host.md new file mode 100644 index 0000000000..427fe39daf --- /dev/null +++ b/docs/runtime-supervised-host.md @@ -0,0 +1,599 @@ +# Runtime-supervised AHP host + +This workstream exposes copilotd's existing, complete AHP server through a +runtime-supervised, in-process hosting task. All six SDKs control its lifetime +through generated SDK JSON-RPC operations. It does not implement an AHP server, +launch a companion process or second runtime, or relay host traffic through the application. + +## Source layout + +The implementation spans `github/copilot-agent-runtime` and `github/copilot-host`. +Build from the runtime checkout; Cargo obtains the pinned `copilotd-hosting` +library from the host repository. +The SDK lives in the runtime repository at `src/sdk`; its source revision is +the runtime commit, not a separate `github/copilot-sdk` commit. Run SDK commands +from `src/sdk` unless a command names another working directory. + +Both runtime and SDK pin SDK protocol version 3. The standard AHP client is +pinned to `@microsoft/agent-host-protocol@0.9.0`, matching the host's +`rust/v0.9.0` source pin (`60706330f2f351b09f150d9a9c3c0eaedfc8e8b9`). +The host prototype `d62d982` +and SDK sample prototype `8fd2498` were inspected as references, not cherry-picked +as production implementations. Their application-side TCP relay is not the +target architecture; application callbacks use ordinary SDK RPC. + +## Ownership and transport + +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 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 +connection has no implicit claim on an old host. + +The optional Node `onExit` / Rust `on_exit` callback reports exit at most once. +On owner connection loss, already-received exits take precedence; remaining +callbacks receive `ownerDisconnected` (`OwnerDisconnected` in Rust), without an +exit code and with an explanation that cleanup cannot be acknowledged. That +report cannot prove cleanup through a transport that has already closed. +`host.pid` is absent for in-process listeners (`undefined` in Node, `None` in Rust). +The optional field preserves separate host process IDs from legacy runtimes, +never the hosting runtime PID. +`reason: "exited"` reports hosting-task failure, not runtime process death. +`exitCode` is absent (Rust `exit_code: None`). Use `dispose()` to stop a listener. +Hosting no longer provides process isolation: a runtime crash also ends its host. +End-to-end coverage independently observes listener closure and runtime survival, +except when the owning runtime itself shuts down. +The small `AhpHost` handle forwards each `dispose()` call to the runtime, including +concurrent and repeated calls; the runtime owns idempotence and teardown outcomes. +A successful disposal means the listener is closed, session participation is +detached, and the hosting task has stopped, not merely that shutdown was requested. + +```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. +// Hosting lasts for this SDK client's connection; dispose early only if needed. +await client.stop(); +``` + +`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 +the actual bound address, including IPv6 brackets where needed. The host always +uses the runtime's configured working directory, with no per-host override. + +Connection-token authentication defaults on: supply a nonempty token or let the +listener generate one. `requireConnectionToken: false` disables only that +connection gate and returns `token: undefined`; supplying any token alongside +`false` is invalid. An empty token is always invalid. AHP resource authentication +and authorization remain in effect independently. Explicit public binding or +disabling the connection gate is the application's choice; the default remains +loopback with a generated token. Tokens travel over framed SDK RPC, not argv. + +Node.js, Rust, Python, Go, .NET, and Java expose experimental thin handles over the generated host RPCs. +`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 +session creation in your application. The callback receives host-selected +session configuration. Preserve its identity, workspace, and settings, add +application tools, hooks, or prompts, and return a normal session created on the +owning SDK client. Rust supplies a request-scoped `Client` for that purpose. +The host then attaches to the same runtime session through its existing separate +connection. Tool and hook functions stay in your application. + +
+TypeScript + +```ts +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({ + ...config, + onPermissionRequest: approveAll, + }); + }, + onSessionReleased: async (originalSession) => { + await originalSession.disconnect(); + }, + }); +} +``` + +
+
+Rust + +```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?)) + }, + ) + .with_on_session_released(|original| { + tokio::spawn(async move { + if let Err(error) = original.disconnect().await { + eprintln!("session cleanup failed: {error}"); + } + }); + }), + ) + .await +} +``` + +
+ +
+Python + +```python +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): + return await client.create_session( + **{**request.config, "on_permission_request": PermissionHandler.approve_all} + ) + + async def release(session: CopilotSession): + await session.disconnect() + + return await client.start_ahp_host( + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + on_session_released=release, + ) + ) +``` + +
+
+Go + +```go +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) + }, + OnSessionReleased: func(session *copilot.Session) error { + return session.Disconnect() + }, + }) +} + +func main() {} +``` + +
+
+.NET + +```csharp +#pragma warning disable GHCP001 +using System.Threading.Tasks; +using GitHub.Copilot; + +static class HostingExample +{ + public static Task StartApplicationHostAsync(CopilotClient client) => + client.StartAhpHostAsync(new AhpHostOptions + { + LocalServer = new(), + CreateSession = request => + { + request.CancellationToken.ThrowIfCancellationRequested(); + request.Config.OnPermissionRequest = PermissionHandler.ApproveAll; + return client.CreateSessionAsync(request.Config, request.CancellationToken); + }, + OnSessionReleased = session => session.DisposeAsync().AsTask() + }); +} +``` + +
+
+Java + +```java +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; + +@AllowCopilotExperimental +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 -> { + session.close(); + return CompletableFuture.completedFuture(null); + })); + } +} +``` + +
+ +The examples choose to disconnect the application session on release. Omit the +release callback to retain it instead. No SDK automatically disconnects or +destroys the returned session. + +The SDK retains the original object (the same `Arc` allocation in Rust) +and invokes Node `onSessionReleased` / Rust `with_on_session_released` at most +once per handoff after participation ends or the handoff fails. This includes a +callback that returns its session after cancellation. Node exposes an +`AbortSignal`; Rust exposes `AhpSessionRequest::cancellation_token`. Cancellation +signals that participation ended without transferring session cleanup ownership +to the SDK. + +Python exposes an `asyncio.Event` as `cancellation_event`; Go supplies a cancellable +factory `context.Context`; .NET supplies a `CancellationToken`; Java supplies a +read-only `CompletionStage` that completes normally on cancellation. +Cancellation ends the handoff RPC even if the factory has not finished. A late +session result still receives its release callback. Lifecycle callback failures +are logged rather than treated as successful cleanup. + +Rust's release closure is synchronous. Its example spawns asynchronous cleanup; +that task belongs to the application and host disposal does not await it. Retain +and join cleanup tasks when application shutdown must wait for them. +The other SDKs also dispatch release and exit callbacks independently of listener +disposal. Await application-owned cleanup separately when it must finish before +the application exits. + +Omitting the creation factory keeps normal host-owned session creation. There is +no custom session-listing callback. + +### Restoring application-owned sessions + +Node's optional `resumeSession` callback receives an `AhpSessionResumeRequest` +with `sessionId`, host-selected `config`, and an abort `signal`. Return the +object from `client.resumeSession(sessionId, { ...config, onPermissionRequest, ... })`, +adding the application's tools, hooks, and handlers needed after a runtime restart. +These functions are application code, not serialized catalog data. +If the application retained its original session on the owning client, the +callback can return that object instead of creating another resumed wrapper. +It must still match the requested session identity and workspace. + +Rust exposes `with_resume_session` and the `AhpSessionResumeFactory` trait. +Its `AhpSessionResumeRequest` carries a `ResumeSessionConfig` and a cooperative +`cancellation_token`. Use the supplied request-scoped `Client` to call +`resume_session(request.config)` with your application handlers, and return the +result in an `Arc`. +Returning a retained original `Arc` is also supported under the same +identity and ownership checks. + +The other SDKs support the same create, resume, release, and publication contract: + +| SDK | Start | Resume callback | Resume the requested session | Publish | +| --- | --- | --- | --- | --- | +| Python | `client.start_ahp_host(options)` | `AhpHostOptions.resume_session` | `await client.resume_session(request.session_id, **request.config)` | `await host.publish_session(session.session_id)` | +| Go | `client.StartAhpHost(ctx, options)` | `AhpHostOptions.ResumeSession` | `client.ResumeSession(ctx, request.SessionID, request.Config)` | `host.PublishSession(ctx, session.SessionID)` | +| .NET | `client.StartAhpHostAsync(options)` | `AhpHostOptions.ResumeSession` | `client.ResumeSessionAsync(request.SessionId, request.Config, request.CancellationToken)` | `host.PublishSessionAsync(session.SessionId)` | +| Java | `client.startAhpHost(options)` | `AhpHostOptions.setResumeSession(callback)` | `client.resumeSession(request.sessionId(), request.config())` | `host.publishSession(session.getSessionId())` | + +Install application tools, hooks, prompts, and permission handlers in the resume +configuration before calling these methods, just as in the creation examples. +Python and .NET handles support asynchronous context management; Java handles +support try-with-resources. Go callers explicitly invoke `Dispose(ctx)`. + +Only durable catalog entries marked as application-owned invoke the resume +factory. If its resume callback is missing, restoration fails rather than silently +falling back to host-owned creation. Published resident sessions attach directly and do not invoke either +factory. This callback does not provide arbitrary adoption or reconfiguration +of an already-resident session. + +Resumed handoffs use the same ownership rules as fresh handoffs: the SDK retains +the exact object returned by this callback and releases it at most once through +`onSessionReleased` / `with_on_session_released`, including cancellation and late +completion. It does not disconnect or destroy that object automatically. + +### Publishing an existing resident session + +`await host.publishSession(session.sessionId)` (Rust: +`host.publish_session(session.id().to_string()).await`) publishes an existing local session +already attached to the host's owning SDK connection. It returns `sessionId` +and `sessionUri`. Publication does not call the factory, copy history, replace +the native session, or transfer ownership. Its metadata and workspace come +from the resident session, not from an application-supplied configuration. + +Published sessions are discoverable only for this listener's lifetime. They +are not imported into its durable catalog. Stopping the listener detaches its +participation without deleting the original session or transcript. A missing +or replaced resident session cannot be silently restored from disk by the +listener. + +### CLI hosting commands + +With `AHP_CLIENT` enabled, `/ahp start` starts a runtime-supervised +in-process listener alongside the current CLI session. `/remote share` starts or reuses +that same listener and publishes the foreground local session, preserving its +ID. Neither command starts another runtime. Ordinary new AHP sessions remain +available; the application session factory is not enabled. + +Both commands show the loopback endpoint, in-process host ID, and generated connection token. +Sharing also shows the session URI. Use an AHP 0.9 client with the connection +token and ordinary GitHub resource authentication; the connection token alone +does not bypass resource authorization. + +`/ahp status` shows connection information. `/ahp stop` stops the **entire +listener and all its shares**. `/remote unshare` removes the foreground session. +The pinned host has no per-session unregister operation, so unsharing drains +and restarts the listener at the same endpoint with the same connection token +before republishing other shares. Other clients must reconnect; local sessions +and their identities remain unchanged. Exiting the owning CLI stops the +listener as well. There is one listener per CLI/effective catalog, not one +listener per shared session. + +`--ahp-host [--listen host:port] [--workspace directory]` serves this same backend +in the CLI process, using normal SDK session construction, managed policy, +hooks, authentication and telemetry. The old promotion backend and duplicate +commands have been removed. Local-owner `/remote on|off|show` retains its independent +Mission Control path; the old attached-AHP-client export toggle is intentionally unavailable. +`COPILOT_AHP_SHARE_BIND` can select a wider listener for sharing; +the CLI's managed remote-control policy still gates off-machine publication. +The CLI `--ahp` attachment client negotiates AHP 0.9 for these listeners, with +0.7 retained for older external hosts. Bare `--ahp` can start an in-process +listener when no local host exists; that listener ends with its owning CLI. +Outbound relay, host-picker and explicitly configured external-daemon controls remain. + +## Durable catalog and single host owner + +The runtime passes its actual resolved data directory to the host library; the single AHP +catalog lives at `/ahp/sessions`. It follows the same +default `~/.copilot`, `COPILOT_HOME`, and SDK `baseDirectory` resolution as that +runtime. The catalog contains sessions previously created through AHP, not all +SDK/CLI sessions. Listener disposal, owner disconnect, and restart retain it. +A replacement listener can list and resume these sessions after authenticating. + +Only one AHP server may own a catalog at a time. A second start for the +same location fails, including from another runtime. Ordinary runtimes, SDK +clients, and sessions do not acquire this lock and remain usable. The lock is +kernel-managed, non-blocking, held until shutdown writes finish, and released +even after forced process termination. Different effective homes have separate +catalogs. This does not change standalone `copilotd` defaults or concurrency +behavior, and does not add standalone/in-process shared-writer support. + +## Current GHES shell limitation + +In-process hosting does not yet provide traditional copilotd's propagation of an AHP session's +GHES credential to shell `gh` commands. Ordinary per-session authentication is +unchanged. General support belongs in the runtime's existing per-session shell +credential capability and is tracked in +[runtime #22077](https://github.com/github/copilot-agent-runtime/issues/22077). +The experimental credential workaround and new command-target policy have been +removed. Traditional copilotd retains its existing Enterprise-token subprocess +seeding and is unaffected. + +## Local-development requirements + +Use the runtime checkout, including its Rust SDK in +`copilot-agent-runtime/src/sdk/rust`. +The runtime provider links the `copilotd-hosting` library at the immutable +`copilot-host` Git revision recorded in the runtime's `Cargo.lock`. Its own SDK +dependency is runtime-free; do not patch that dependency to the runtime-bearing +SDK build or stage a separate host executable. + +Build both the runtime launcher and its native provider from the runtime +checkout. Point the SDK's `RuntimeConnection.forStdio({ path })` at that local +launcher. A local JavaScript launcher loading a released native provider is +not a local runtime build. + +Set `COPILOT_RUNTIME_PROVIDER_LIB` to the source-built provider for development-path +integration. The launcher and provider must both be built from the current source. +The host library is linked into the provider. +Candidate package coverage exercises the bundled launcher and adjacent provider; +it does not require a companion executable. + +Only model inference is eligible for record/replay. AHP connections, session +creation, streaming events, participant ownership, and listener cleanup must run +against the real local runtime and host. + +After building the local runtime provider, run the focused CLI suite +from the runtime repository root (with root dependencies installed): + +```sh +corepack pnpm run build +unset COPILOT_RUNTIME_E2E_OOP COPILOT_RUNTIME_OOP +COPILOT_CLI_PATH="$PWD/dist-cli/index.js" \ +COPILOT_RUNTIME_PROVIDER_LIB="/absolute/local/runtime/runtime.node" \ +COPILOT_RUNTIME_HOST_E2E=1 STRICT_CAPTURES=true \ +corepack pnpm run test:cli-e2e test/cli/e2e/supervised-ahp.test.ts +``` + +This exercises the default embedded CLI runtime. Stage the source-built provider as +`dist-cli/prebuilds/linux-x64/runtime.node` first. The fixture verifies that +this addon matches the source provider, that no host PID is reported, and that the +actual spawned Node process owns the listener socket. +It also rejects companion hosts and additional provider-loaded runtimes. + +The existing `COPILOT_RUNTIME_E2E_OOP=1` harness option expects a historical +Rust-parent launcher accepting `node ` arguments. The current +source-built `copilot-runtime` is an SDK-only stdio/TCP server, not that launcher, +and rejects those arguments. Do not enable this option with the current +prototype artifacts or substitute a released launcher to make it pass. + +This exercises existing-session sharing with local tool approval and exactly-once +execution, ordinary AHP session creation, unshare, CLI exit, and listener restart +in the same runtime. The Node and Rust SDK suites additionally cover occupied-port +startup failure and recovery. These are not process-crash isolation tests; no +task-failure injection is available. Without the opt-in environment variable, these cross-repository tests +are skipped. + +### Cross-language SDK end-to-end tests + +The Python, Go, .NET, and Java suites start a listener through their own SDK APIs +and launch the standard TypeScript AHP 0.9 client as a **test-only subprocess**. +`nodejs/test/e2e/harness/ahpTestDriver.ts` provides its JSON-lines test interface. +This process is an AHP client, not a host or additional runtime. It connects +directly to the real listener. The existing CAPI replay proxy handles only +inference traffic and has no AHP orchestration routes or state. + +Install the existing Node SDK and replay-harness test dependencies before these +suites (`npm --prefix nodejs ci --ignore-scripts` and +`npm --prefix test/harness ci --ignore-scripts`, from `src/sdk`). +Use a source-built runtime or integrated candidate, not the released runtime pin. +As with the Node and Rust suites, set `COPILOT_RUNTIME_HOST_E2E=1` to enable them. +Set `COPILOT_CLI_PATH` to that runtime's launcher and `GITHUB_ACTIONS=true` to +keep the shared snapshots read-only. Then run the relevant focused command: + +```sh +# From src/sdk/python +uv run pytest test_host.py e2e/test_runtime_host_e2e.py + +# From src/sdk/go +go test -race ./... -run 'Ahp|RuntimeHost' + +# From src/sdk/dotnet +dotnet test test/GitHub.Copilot.SDK.Test.csproj -p:CopilotSkipCliDownload=true \ + --filter 'FullyQualifiedName~ClientSessionLifetimeTests.Ahp_|FullyQualifiedName~RuntimeHostE2ETests' + +# From src/sdk/java, using JDK 25+ +./mvnw -pl sdk verify -Dtest=AhpHostTest -Dit.test=RuntimeHostIT \ + -Dcopilot.cli.path="$COPILOT_CLI_PATH" +``` + +Each language reuses the unchanged +`multi_client/both_clients_see_tool_request_and_completion_events.yaml` and +`runtime_host/app_resume_callback_composes_tools_after_history.yaml` snapshots. +The scenarios cover fresh application factories, exact resident publication, +durable application resume after a complete runtime restart, restored history, +composed application/AHP-client tools, preserved prompts and hooks, original-object +release, listener closure, and continued application-session use after disposal. + +## Companion release order + +To bootstrap the accepted cross-repository dependency, the host pins the +runtime-free Rust SDK implementation to an immutable companion commit recorded +in its Cargo manifest and lockfile. This is a source dependency, not +a claim that a corresponding SDK or runtime release has been published. + +The host library must be built into the runtime provider before a runtime +release can publish the integrated artifacts and checksums. The SDK can then pin +that runtime release through its existing runtime-distribution mechanism. +Unreleased local candidates must be staged explicitly; substituting a released +runtime or host does not validate these changes. + +The SDK's existing released runtime pin must be advanced only after the +companion runtime is published. Until then, `startAhpHost()` requires the local +source-built runtime or an assembled candidate; the currently released runtime +is not claimed to implement the new host operations. Generated bindings in this +branch come from the companion runtime's local schema, so release-based code +generation must use that same companion release when its pin is advanced. + +The canonical `.github/workflows/sdk-rust.yml` runs SDK feature checks in active +monorepo CI and is mirrored to `src/sdk/.github/workflows/sdk-rust.yml` for export. +Its runtime-free feature checks do not activate the opt-in AHP E2Es. + +The coordinated follow-up is: publish the integrated runtime artifacts, provision private +release-read credentials, update the runtime acquisition pin, +advance the SDK runtime pin, **then enable the opt-in AHP E2Es in CI**. +CI activation is not a prerequisite for these implementation drafts; local +source and assembled-candidate runs provide current integration evidence. +Actual platform ABI/signing/notarization release jobs still need to execute. +Unsigned debug candidates do not establish signed-release behavior. + +Arbitrary existing-session adoption, application-owned AHP transport, +projection relocation, general multi-harness composition, and exhaustive AHP +compatibility coverage are separate workstreams. 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/docs/troubleshooting/debugging.md b/docs/troubleshooting/debugging.md index 588049f0d7..abfdbe9c4e 100644 --- a/docs/troubleshooting/debugging.md +++ b/docs/troubleshooting/debugging.md @@ -108,7 +108,14 @@ var client = new CopilotClient(new CopilotClientOptions() ### Log directory -The CLI writes logs to a directory. You can specify a custom location: +The SDK-managed out-of-process runtime writes redacted +`process--.log` files under the Copilot home's `logs` directory +by default in `copilot-cli` mode. SDKs in `empty` mode do not set the logging +opt-in, but an inherited setting can still enable it; session events remain +available to `collectLogs`. An explicitly selected older CLI executable may +ignore this setting. You can specify a custom log location with `--log-dir`; +`collectLogs` searches the configured directory. In-process connections do +not start a separate logging process.
Node.js / TypeScript @@ -125,13 +132,12 @@ const client = new CopilotClient({ Python ```python -# The Python SDK does not currently support passing extra CLI arguments. -# Logs are written to the default location or can be configured via -# the CLI when running in server mode. -``` +from copilot import CopilotClient, RuntimeConnection -> [!NOTE] -> Python SDK logging configuration is limited. For advanced logging, run the CLI manually with `--log-dir` and connect via `RuntimeConnection.for_uri(...)`. +client = CopilotClient( + connection=RuntimeConnection.for_stdio(args=["--log-dir", "/path/to/logs"]) +) +```
@@ -182,9 +188,11 @@ var client = new CopilotClient(new CopilotClientOptions ```java -// The Java SDK does not currently support passing extra CLI arguments. -// For custom log directories, run the CLI manually with --log-dir -// and connect via cliUrl. +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.CopilotClientOptions; + +var client = new CopilotClient(new CopilotClientOptions() + .setCliArgs(new String[] {"--log-dir", "/path/to/logs"})); ```
diff --git a/dotnet/README.md b/dotnet/README.md index 946210437b..1269c6c057 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -37,6 +37,17 @@ dotnet run --file dotnet/samples/ManualToolResume.cs ## Quick Start +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. + ```csharp using GitHub.Copilot; @@ -99,6 +110,8 @@ 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 @@ -113,6 +126,49 @@ adjacent `runtime.node` by default. An explicit connection path or `COPILOT_CLI_PATH` overrides the bundled runtime. Managed launch fails if the bundled wrapper pair is unavailable. +#### Installation confirmation (experimental) + +Set `CopilotClientOptions.InstallationConfirmationHandler` to receive the +runtime's `installations.confirm` callback. The handler receives the generated +`GitHub.Copilot.Rpc.InstallationsConfirmRequest` and an +`InstallationConfirmationContext`, and returns only an explicit +`GitHub.Copilot.Rpc.InstallationDecision.Confirm`, `.Decline` or `.Cancel`. The +SDK echoes the original challenge and review fingerprint; it never infers approval. + +Match `OperationId` and `PolicySessionId` against the original action on this +exact connection before presenting the complete review. Missing legacy session +metadata does not select a default session. Refuse unknown operations or +incomplete reviews. Concurrent reviews are independent and do not block other +connection callbacks. + +`context.CancellationToken` is cancelled when the runtime sends numeric +`$/cancelRequest`, including runtime-enforced expiry, or when the original +connection closes. Observe it to close pending UI. Late handler results cannot +approve a retired request, and this incoming signal does not cancel outbound +installation or OAuth RPCs. + +Call `client.Rpc.Mcp.PrepareInstallAsync(...)` before +`ApplyInstallAsync(...)`. Register its inert runtime-issued `OperationId`, +original expiry and captured session on this client before applying. Removal +uses `PlanUninstallAsync(...)` then `ApplyUninstallAsync(...)`; its +`OperationId` identifies the operation, while `PlanHandle` is the one-use +removal input. Never interchange them. The `Installations` namespace exposes +`ListAsync`, `RecoverAsync`, `StatusAsync` and `CancelAsync`. Control uncertain +work using its original connection and operation ID, without selecting a +replacement session or replaying apply. + +Owned OAuth uses `session.Rpc.Mcp.Oauth.PrepareLoginAsync(...)` to return +`LoginId` before browser, network or cached-reconnect work. Keep that ID with +the original session and `ExpectedInstallationId` for `LoginAsync(...)` and +`CancelLoginAsync(...)`. Preparation freezes reauthentication and display +options. Dropping the login future is not a substitute for `CancelLoginAsync(...)`. +Manual MCP OAuth retains its direct `LoginAsync(...)` path. + +A matching runtime contract and available owned-lifecycle support are required. +Capability negotiation does not promise availability; preserve typed refusals +instead of falling back to raw configuration writes. Generated presence and +transport tests do not establish live OAuth, activation or cross-process recovery. + #### Methods ##### `StartAsync(): Task` @@ -122,6 +178,10 @@ Start the CLI server and establish connection. ##### `StopAsync(): Task` Stop the server and close all sessions. Throws if errors are encountered during cleanup. +For an owned stdio runtime, graceful shutdown closes stdin and waits up to 10 seconds +for host cleanup, including telemetry export. This cleanup is best-effort: if the wait +times out, the process is terminated and that timeout alone is not reported as a cleanup +error. A successful return does not guarantee that all telemetry was exported. ##### `ForceStopAsync(): Task` @@ -147,10 +207,16 @@ Create a new conversation session. - `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory. - `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled. - `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`. +- `SkillProvider` - Experimental session-scoped skill provider. See [Skill providers (experimental)](#skill-providers-experimental). - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`. - `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. +- `CanvasHandler` - Handles canvas open, close, and action callbacks. The SDK awaits + asynchronous callbacks before replying, including callbacks without a result, unless + the runtime cancels the request first. A cancellation response can be sent while the + callback is still running. Their cancellation token is canceled by a per-request + `$/cancelRequest`, when the runtime connection closes, or when the client is disposed. ##### `ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null): Task` @@ -160,7 +226,18 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. +- `SkillProvider` - Re-supplies the session-scoped skill provider when resuming. - `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. +- `AllowTranscriptRecovery` - Repairs a damaged transcript when true. The default + is true in all modes; set false to reject recovery. `session.TranscriptRecovery` contains + `PlannedBackupPath`, `InvalidLineNumbers` (including torn-tail loss), and + `SessionStartMoved` when repair is reported; otherwise it is null. On rejection, + `ResumeSessionAsync` throws an `IOException` whose `InnerException` is a + `RemoteRpcException`. Read `ErrorCode` (`-32075`) and `ErrorData` from that inner + exception for the server's typed error and its `invalidLineNumbers` / + `sessionStartMoved` fields. The outer message retains the existing communication-error + prefix. Disabling recovery still permits adding a missing newline after an intact + final record; it rejects torn tails. ```csharp await using var session = await client.CreateSessionAsync(new SessionConfig @@ -179,6 +256,62 @@ await using var session = await client.CreateSessionAsync(new SessionConfig Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +##### Skill providers (experimental) + +Set `SessionConfig.SkillProvider` or `ResumeSessionConfig.SkillProvider` to +serve session-scoped skills from your application. The provider is not persisted: +pass it again on every resume, because resuming without one unbinds it. Skill +providers are only supported for local sessions; `CreateSessionAsync` throws if +`Cloud` and `SkillProvider` are both set. The skill provider types are +experimental and raise the `GHCP001` diagnostic; suppress it with +`#pragma warning disable GHCP001` or `GHCP001`. + +```csharp +#pragma warning disable GHCP001 // Skill providers are experimental. + +public sealed class MySkillProvider : ISkillProvider +{ + public Task> ListSkillsAsync(CancellationToken cancellationToken) => + Task.FromResult>( + [ + new() + { + Name = "project-facts", + Description = "Important facts about this host application", + ArgumentHint = "" + } + ]); + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(name == "project-facts" + ? """ + # Project facts + + Use the application's project index before answering. + """ + : null); +} + +await using var client = new CopilotClient(new CopilotClientOptions +{ + Mode = CopilotClientMode.Empty +}); + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + AvailableTools = [], // Empty mode requires an explicit tool allow-list. + SkillProvider = new MySkillProvider(), + EnableSkills = true // Required in empty mode; otherwise skills default off. +}); +``` + +The runtime may call `ListSkillsAsync` and `ReadSkillAsync` concurrently. Honor +the supplied cancellation token; it is canceled when the runtime abandons the +request (for example, on timeout, session disposal, or a resume that replaces the +provider) or the connection closes. Return `null` from `ReadSkillAsync` when a +skill name is not found. Optional `SkillProviderDescriptor` properties are +omitted from JSON when unset. + ##### `PingAsync(string? message = null): Task` Ping the server to check connectivity. @@ -283,6 +416,19 @@ await session.SendAndWaitAsync(new MessageOptions Agent sources serialize as `agent-`. Pass the agent ID without adding a prefix. The SDK preserves its case and whitespace and rejects null IDs. +##### `SetToolsAsync(ICollection tools, CancellationToken cancellationToken = default): Task` (experimental) + +Replace the complete set of externally implemented tools supplied by this client +on a live session. Pass the same tool declarations used in `SessionConfig.Tools` +or `ResumeSessionConfig.Tools`; an empty collection removes this client's tools. +Built-in, MCP/plugin, extension, subagent, and other clients' tools are unchanged. + +Handlers switch after the runtime accepts the replacement. Running tool calls +finish on the handlers that started them, rejected replacements leave the +previous handlers installed, and concurrent replacements are applied in order. +See [Changing tools](../docs/features/changing-tools.md) for shared behavior and +active-turn limitations. + ##### Structured outputs (experimental) Use `SendAndWaitAsync` to infer a JSON Schema from a .NET type and @@ -431,6 +577,13 @@ Abort the currently processing message in this session. Get all events/messages from this session. +Live notifications and history deserialization select source-generated metadata for the +received event type rather than initializing every event type on the first event. +History parsing uses a bounded stack or pooled UTF-8 buffer to avoid an extra +reader-scoping pass; common event-type selection does not allocate a discriminator string. +Unknown or missing event types retain the base `SessionEvent` fallback; serialization +and malformed-event validation are unchanged. + ##### `DisposeAsync(): ValueTask` Close the session and release in-memory resources. Session data on disk is preserved — the conversation can be resumed later via `ResumeSessionAsync()`. To permanently delete session data, use `client.DeleteSessionAsync()`. @@ -723,6 +876,16 @@ var session = await client.CreateSessionAsync(new SessionConfig }); ``` +An explicit `apply_patch` override may advertise a root string schema instead of +an object schema. The runtime delivers the patch as a string in +`ToolInvocation.Arguments`; the SDK binds that scalar to the named `input` +argument required by `AIFunction`. A schema wrapper around a typed handler should +therefore use a parameter named `input`. Object-schema tools retain their +declared parameter names. + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `CopilotToolOptions.SkipPermission` to allow a tool to execute without triggering a permission prompt: @@ -901,6 +1064,8 @@ var session = await client.CreateSessionAsync(new SessionConfig Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Preamble`, `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`. `Identity` and `ToolInstructions` are section groups that target a collection of related sub-sections as a unit; use `Preamble` to target just the identity preamble. +`SystemMessageSection.LastInstructions` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a `SectionOverride.Transform` callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SystemMessageSection.RuntimeInstructions` is a separate section: removing it does not remove `SystemMessageSection.LastInstructions`. + Each section override supports five actions: `Replace`, `Remove`, `Append`, `Prepend`, and `Preserve` (a no-op that opts an individually-addressable section out of a group-level `Remove`). Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored. #### Replace Mode @@ -1189,6 +1354,32 @@ var session = await client.CreateSessionAsync(new SessionConfig - `OnSessionStart` - Run logic when a session starts or resumes. - `OnSessionEnd` - Cleanup or logging when session ends. - `OnErrorOccurred` - Handle errors with retry/skip/abort strategies. +- `OnAgentStop` - Intercept a natural stop of the top-level agent. +- `OnSubagentStart` - Add context to a sub-agent before its first turn. +- `OnSubagentStop` - Inspect a sub-agent's last response, block the stop with a follow-up instruction, or replace its response. + +Sub-agent lifecycle hooks are registered on the parent session. Their input includes the parent `SessionId`, a `DateTimeOffset` timestamp, `WorkingDirectory`, `TranscriptPath` (empty if unavailable), and `AgentName`; `AgentDisplayName` and `AgentDescription` are optional. `OnSubagentStop` also receives an optional `AgentId`, `AgentType`, `StopReason` (currently `"end_turn"`), and the sub-agent's `Response`. `HookInvocation.SessionId` is the parent session ID. + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnSubagentStart = (input, invocation) => + Task.FromResult(new SubagentStartHookOutput + { + AdditionalContext = "Read the requested file before answering." + }), + OnSubagentStop = (input, invocation) => + Task.FromResult(new SubagentStopHookOutput + { + ModifiedResponse = input.Response.Trim() + }) + } +}); +``` + +`OnSubagentStart`'s `AdditionalContext` is prepended to the child's initial prompt. To request another child turn instead of accepting its response, return `new SubagentStopHookOutput { Decision = "block", Reason = "Explain your findings." }` from `OnSubagentStop`; a non-empty `Reason` is required. Otherwise, `ModifiedResponse` replaces the response reported to the parent. These hooks are distinct from `OnAgentStop`, which applies to the top-level agent. ## Elicitation Requests diff --git a/dotnet/src/Canvas.cs b/dotnet/src/Canvas.cs index 6bf8be984e..02874a79f2 100644 --- a/dotnet/src/Canvas.cs +++ b/dotnet/src/Canvas.cs @@ -171,6 +171,12 @@ public interface ICanvasHandler /// Handle a non-lifecycle action declared by the canvas. /// Default: throws . /// + /// + /// The returned value is sent to the model as the invoke_canvas_action + /// tool result. To return text and images, as tool handlers do, return a + /// with ; + /// any other value is rendered to the model as JSON text. + /// Task OnActionAsync(CanvasProviderInvokeActionRequest context, CancellationToken cancellationToken); } diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 0cf2e75bde..ab4399ef5c 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)) { @@ -556,6 +556,7 @@ await InvokeRpcAsync( /// public async Task StopAsync() { + DisconnectAhpHosts(); List errors = []; CancelPendingExternalTools(); @@ -603,6 +604,7 @@ public async Task StopAsync() /// public async Task ForceStopAsync() { + DisconnectAhpHosts(); CancelPendingExternalTools(); _sessions.Clear(); ClearGitHubTokenProviders(); @@ -692,31 +694,46 @@ or IOException if (ctx.CliProcess is { } childProcess) { - await CleanupCliProcessAsync(childProcess, ctx.StderrPump, errors, _logger); + await CleanupCliProcessAsync(childProcess, ctx.StderrPump, errors, _logger, gracefulRuntimeShutdown); } 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; } } - private static async Task CleanupCliProcessAsync(Process childProcess, ProcessStderrPump? stderrPump, List? errors, ILogger? logger) + private static async Task CleanupCliProcessAsync(Process childProcess, ProcessStderrPump? stderrPump, List? errors, ILogger? logger, bool gracefulRuntimeShutdown = false) { var processExited = false; try { + if (gracefulRuntimeShutdown && childProcess.StartInfo.RedirectStandardInput && !childProcess.HasExited) + { + try + { + // The native wrapper finalizes host telemetry after stdin EOF, + // not when it acknowledges runtime.shutdown. + childProcess.StandardInput.Close(); + await childProcess.WaitForExitAsync().WaitAsync(s_runtimeShutdownTimeout); + } + catch (Exception ex) when (ex is TimeoutException or IOException or ObjectDisposedException) + { + logger?.LogDebug(ex, "Graceful stdio runtime exit did not complete; terminating the process"); + } + catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception or NotSupportedException) + { + AddCleanupError(errors, ex, logger); + } + } + if (!childProcess.HasExited) { - // The runtime completes all cleanup before responding to - // runtime.shutdown and then leaves termination to us; it - // deliberately keeps its JSON-RPC server alive to send the - // response and never self-exits. Waiting for a self-exit that - // will never come just wastes time, so terminate the child - // immediately and only wait to reap it. + // Force-stop, failed startup, and runtimes that ignore EOF still + // require explicit termination. childProcess.Kill(entireProcessTree: true); // Kill is asynchronous; wait for the root CLI process to exit so cleanup callers // do not observe StopAsync/DisposeAsync completion while it is still tearing down. @@ -875,6 +892,7 @@ private CopilotSession InitializeSession( session.RegisterElicitationHandler(config.OnElicitationRequest); session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest); session.RegisterAutoModeSwitchHandler(config.OnAutoModeSwitchRequest); + session.RegisterSkillProvider(config.SkillProvider); if (config.OnUserInputRequest != null) { session.RegisterUserInputHandler(config.OnUserInputRequest); @@ -1173,6 +1191,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { ArgumentNullException.ThrowIfNull(config); ValidateGitHubTokenConfig(config); + if (config.Cloud is not null && config.SkillProvider is not null) + { + throw new ArgumentException("Skill providers are not supported for cloud sessions."); + } var connection = await EnsureConnectedAsync(cancellationToken); var totalTimestamp = Stopwatch.GetTimestamp(); @@ -1191,7 +1213,9 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Hooks.OnSessionStart != null || config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null || - config.Hooks.OnAgentStop != null); + config.Hooks.OnAgentStop != null || + config.Hooks.OnSubagentStart != null || + config.Hooks.OnSubagentStop != null); var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); @@ -1210,6 +1234,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance var registrationId = RegisterGitHubTokenProvider(config.GitHubTokenProvider); var registrationTransferred = false; CopilotSession? session = null; + string? serverAssignedSessionId = null; try { if (localSessionId != null) @@ -1307,7 +1332,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance GitHubMcpToolConfig: config.GitHubMcpToolConfig, ManagedSettings: config.ManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, - AdditionalDirectories: config.AdditionalDirectories); + AdditionalDirectories: config.AdditionalDirectories, + HasSkillProvider: config.SkillProvider is not null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); @@ -1326,6 +1352,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance && sessionIdProp.GetString() is string sessionId && !string.IsNullOrEmpty(sessionId)) { + serverAssignedSessionId = sessionId; session = InitializeSession( sessionId, connection.Rpc, @@ -1333,6 +1360,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance transformCallbacks, hasHooks, "CopilotClient.CreateSessionAsync"); + serverAssignedSessionId = null; } }; @@ -1364,6 +1392,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance session.SetOpenCanvases(response.OpenCanvases); await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false); + CaptureAhpSession(session, request, ClientJsonContext.Default.CreateSessionRequest); if (registrationId is not null) { session.SetGitHubTokenProviderRegistration(registrationId); @@ -1373,6 +1402,22 @@ public async Task CreateSessionAsync(SessionConfig config, Cance catch (Exception ex) { session?.Unregister(); + if (localSessionId is null && serverAssignedSessionId is not null) + { + try + { + using var cleanupTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + await DeleteSessionAsync(serverAssignedSessionId, cleanupTimeout.Token).ConfigureAwait(false); + } + catch (Exception cleanupError) when (cleanupError is OperationCanceledException + or IOException + or SocketException + or InvalidOperationException + or ObjectDisposedException) + { + _logger.LogWarning(cleanupError, "Failed to delete cloud session {SessionId} after creation failed", serverAssignedSessionId); + } + } if (ex is not OperationCanceledException) { @@ -1447,7 +1492,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Hooks.OnSessionStart != null || config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null || - config.Hooks.OnAgentStop != null); + config.Hooks.OnAgentStop != null || + config.Hooks.OnSubagentStart != null || + config.Hooks.OnSubagentStop != null); var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); @@ -1551,7 +1598,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes GitHubMcpToolConfig: config.GitHubMcpToolConfig, ManagedSettings: config.ManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, - AdditionalDirectories: config.AdditionalDirectories); + AdditionalDirectories: config.AdditionalDirectories, + AllowTranscriptRecovery: config.AllowTranscriptRecovery, + HasSkillProvider: config.SkillProvider is not null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); var response = await InvokeRpcAsync( @@ -1562,6 +1611,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes sessionId); session.WorkspacePath = response.WorkspacePath; + session.TranscriptRecovery = response.TranscriptRecovery; session.SetCapabilities(response.Capabilities); session.SetOpenCanvases(response.OpenCanvases); @@ -1571,6 +1621,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes } await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false); + CaptureAhpSession(session, request, ClientJsonContext.Default.ResumeSessionRequest); if (registrationId is not null) { session.SetGitHubTokenProviderRegistration(registrationId); @@ -1763,6 +1814,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell if (_sessions.TryRemove(sessionId, out var session)) { + session.ClearSkillProvider(); session.ReleaseGitHubTokenProviderRegistration(); } } @@ -2054,12 +2106,16 @@ await Rpc.SessionFs.SetProviderAsync( { var handler = _options.RequestHandler; var onGitHubTelemetry = _options.OnGitHubTelemetry; + var installationConfirmationHandler = _options.InstallationConfirmationHandler; return new ClientGlobalApiHandlers { ExtensionLaunchProvider = _options.ExtensionLaunchProvider, LlmInference = handler is null ? null : new LlmInferenceAdapter(handler, () => _serverRpc), GitHubTelemetry = onGitHubTelemetry is null ? null : new GitHubTelemetryAdapter(onGitHubTelemetry, _logger), GitHubToken = new GitHubTokenAdapter(this), + Installations = installationConfirmationHandler is null + ? null + : new InstallationConfirmationAdapter(installationConfirmationHandler), }; } @@ -2171,6 +2227,11 @@ private void ConfigureSessionFsHandlers(CopilotSession session, Func envir startInfo.Environment["COPILOT_DISABLE_KEYTAR"] = "1"; } + if (options.Mode != CopilotClientMode.Empty) + { + startInfo.Environment["COPILOT_RUNTIME_PROCESS_FILE_LOGGING"] = "1"; + } + // Set telemetry environment variables if configured ApplyTelemetryEnvironment(startInfo.Environment, options.Telemetry); @@ -2695,6 +2761,8 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? rpc.SetLocalRpcMethod("autoModeSwitch.request", handler.OnAutoModeSwitchRequest); rpc.SetLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke); rpc.SetLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform); + rpc.SetLocalRpcMethod("skillProvider.list", handler.OnSkillProviderList, singleObjectParam: true); + rpc.SetLocalRpcMethod("skillProvider.read", handler.OnSkillProviderRead, singleObjectParam: true); ClientSessionApiRegistration.RegisterClientSessionApiHandlers(rpc, sessionId => { var session = GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); @@ -2702,8 +2770,13 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? }); if (_clientGlobalApis is not null) { + if (_clientGlobalApis.Installations is InstallationConfirmationAdapter installationConfirmationAdapter) + { + installationConfirmationAdapter.Attach(rpc); + } ClientGlobalApiRegistration.RegisterClientGlobalApiHandlers(rpc, _clientGlobalApis); } + RegisterAhpHandlers(rpc); if (cliProcess is not null) { RegisterRpcProcessExit(cliProcess, rpc); @@ -2789,6 +2862,7 @@ private async Task CancelExternalToolsWhenConnectionClosesAsync(JsonRpc rpc) { return; } + DisconnectAhpHosts(); CancelPendingExternalTools(); } @@ -2800,6 +2874,7 @@ private void CancelPendingExternalTools() } foreach (var session in _sessions.Values) { + session.ClearSkillProvider(); session.CancelPendingExternalTools(); } } @@ -2828,6 +2903,8 @@ private static JsonSerializerOptions CreateSerializerOptions() DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + options.Converters.Add(SessionEventJsonConverter.Default); + options.TypeInfoResolverChain.Add(SessionEventJsonTypeInfoResolver.Default); options.TypeInfoResolverChain.Add(ClientJsonContext.Default); options.TypeInfoResolverChain.Add(TypesJsonContext.Default); options.TypeInfoResolverChain.Add(CopilotSession.SessionJsonContext.Default); @@ -2973,6 +3050,32 @@ public async ValueTask OnSystemMessageTransfo return await session.HandleSystemMessageTransformAsync(sections); } + public async ValueTask OnSkillProviderList(SkillProviderListRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (string.IsNullOrEmpty(request.SessionId)) + { + throw new ArgumentException("Skill provider list request is missing a session id."); + } + + var session = client.GetSession(request.SessionId) + ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}"); + return await session.HandleSkillProviderListAsync(cancellationToken); + } + + public async ValueTask OnSkillProviderRead(SkillProviderReadRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (string.IsNullOrEmpty(request.SessionId) || string.IsNullOrEmpty(request.Name)) + { + throw new ArgumentException("Skill provider read request is missing a session id or skill name."); + } + + var session = client.GetSession(request.SessionId) + ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}"); + return await session.HandleSkillProviderReadAsync(request.Name, cancellationToken); + } + } private class Connection( @@ -3131,7 +3234,8 @@ internal record CreateSessionRequest( [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null, [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null, - IList? AdditionalDirectories = null); + IList? AdditionalDirectories = null, + bool? HasSkillProvider = null); #pragma warning restore GHCP001 internal record ToolDefinition( @@ -3252,7 +3356,9 @@ internal record ResumeSessionRequest( [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null, [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null, - IList? AdditionalDirectories = null); + IList? AdditionalDirectories = null, + bool? AllowTranscriptRecovery = null, + bool? HasSkillProvider = null); #pragma warning restore GHCP001 internal record ResumeSessionResponse( @@ -3260,7 +3366,8 @@ internal record ResumeSessionResponse( string? WorkspacePath, SessionCapabilities? Capabilities = null, #pragma warning disable GHCP001 - IList? OpenCanvases = null); + IList? OpenCanvases = null, + TranscriptRecoveryReport? TranscriptRecovery = null); #pragma warning restore GHCP001 internal record CommandWireDefinition( @@ -3339,6 +3446,21 @@ internal record UserInputRequestResponse( string Answer, bool WasFreeform); + internal record SkillProviderListRequest( + string SessionId); + +#pragma warning disable GHCP001 + internal record SkillProviderListResult( + IReadOnlyList Skills); +#pragma warning restore GHCP001 + + internal record SkillProviderReadRequest( + string SessionId, + string Name); + + internal record SkillProviderReadResult( + [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? Markdown); + internal record AutoModeSwitchRequestResponse( AutoModeSwitchResponse Response); @@ -3351,6 +3473,11 @@ internal record HooksInvokeResponse( NumberHandling = JsonNumberHandling.AllowReadingFromString, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(CreateSessionRequest))] + [JsonSerializable(typeof(SessionConfig))] + [JsonSerializable(typeof(ResumeSessionConfig))] + [JsonSerializable(typeof(AhpMaterializeRequest))] + [JsonSerializable(typeof(AhpMaterializeResult))] + [JsonSerializable(typeof(AhpReleaseRequest))] [JsonSerializable(typeof(CreateSessionResponse))] [JsonSerializable(typeof(AutoModeSwitchRequest))] [JsonSerializable(typeof(AutoModeSwitchRequestResponse))] @@ -3362,6 +3489,12 @@ internal record HooksInvokeResponse( [JsonSerializable(typeof(ExitPlanModeResult))] [JsonSerializable(typeof(GetLastSessionIdResponse))] [JsonSerializable(typeof(HooksInvokeResponse))] + [JsonSerializable(typeof(SkillProviderListRequest))] +#pragma warning disable GHCP001 + [JsonSerializable(typeof(SkillProviderListResult))] +#pragma warning restore GHCP001 + [JsonSerializable(typeof(SkillProviderReadRequest))] + [JsonSerializable(typeof(SkillProviderReadResult))] [JsonSerializable(typeof(ListSessionsRequest))] [JsonSerializable(typeof(ListSessionsResponse))] [JsonSerializable(typeof(GetSessionMetadataRequest))] diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index f4b993c288..eb06ae1b62 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -107,6 +107,379 @@ 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 +{ + /// Canonical runtime ID of the published session. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// AHP resource URI for the session on this listener. + [JsonPropertyName("sessionUri")] + public string SessionUri { get; set; } = string.Empty; +} + +/// Publishes a resident session attached to the listener's owning connection. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostPublishSessionRequest +{ + /// Listener UUID returned by host.start. + [JsonPropertyName("hostId")] + public string HostId { get; set; } = string.Empty; + + /// Canonical runtime session ID attached to the listener's owning connection. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// The resident session the application has materialized on its own connection. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostCreateSessionResult +{ + /// Runtime session UUID materialized on the application's SDK connection. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// One application-owned session handoff, requested by the supervised hosting participant. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostCreateSessionRequest +{ + /// Host-selected SDK creation or resume settings, without executable callbacks or tools. + [JsonPropertyName("config")] + public IDictionary Config { get => field ??= new Dictionary(); set; } + + /// Unique identity for this participation, independent of the session lifetime. + [JsonPropertyName("handoffId")] + public string HandoffId { get; set; } = string.Empty; + + /// Resume an app-owned durable session instead of creating a new session. + [JsonPropertyName("resume")] + public bool? Resume { get; set; } +} + +/// Empty acknowledgement for a completed host lifecycle operation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostReleaseSessionResult +{ +} + +/// Ends one participation, not the application's session lifetime. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostReleaseSessionRequest +{ + /// Identity of the participation to release without destroying the session. + [JsonPropertyName("handoffId")] + public string HandoffId { get; set; } = string.Empty; +} + +/// Listener readiness, returned only after binding and the supervised participant's SDK handshake. +[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; + + /// Separate host process ID, when provided by a legacy runtime. Absent for in-process listeners. + [JsonPropertyName("pid")] + public long? Pid { get; set; } + + /// Secret connection token, absent when authentication is disabled. + [JsonPropertyName("token")] + public string? Token { get; set; } + + /// Actual bound WebSocket URL, including the allocated port. + [JsonPropertyName("url")] + public string? Url { get; set; } +} + +/// GitHub Mission Control registration options. The compute ID is application-owned and stable. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HostGitHubEnvironmentOptions +{ + /// 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; } + + /// Listener port. Omitted or zero requests an OS-allocated port. + [JsonPropertyName("port")] + public int? Port { get; set; } + + /// Require token authentication (default true). Cannot be false with a token. + [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; } + + /// Ask the owning SDK application to materialize AHP sessions. + [JsonPropertyName("sessionFactory")] + public bool? SessionFactory { get; set; } +} + +/// Empty acknowledgement for a completed host lifecycle operation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HostDisposeResult +{ +} + +/// Stops a connection-owned listener and joins its teardown. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostDisposeRequest +{ + /// Listener UUID. Unknown or successfully stopped IDs are harmless. + [JsonPropertyName("hostId")] + public string HostId { get; set; } = string.Empty; +} + +/// Private credentials delivered only to a runtime-owned Mission Control hosting participant. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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")] + public string Hostname { get; set; } = string.Empty; + + /// Port to bind, with zero requesting OS allocation. + [JsonPropertyName("port")] + public int Port { get; set; } + + /// Whether the listener requires token authentication. + [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; } + + /// Whether session materialization is delegated to the owning application. + [JsonPropertyName("sessionFactory")] + public bool SessionFactory { get; set; } +} + +/// Empty acknowledgement for a completed host lifecycle operation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostReadyResult +{ +} + +/// Readiness reported by the supervised hosting participant on its own SDK connection. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostReadyRequest +{ + /// Actual bound WebSocket URL. + [JsonPropertyName("address")] + 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")] + public string? Token { get; set; } +} + /// One server-discovered hook action from user, repository, plugin, or managed-policy configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class DiscoveredHook @@ -203,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; } @@ -223,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; } @@ -252,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; } @@ -272,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; } @@ -364,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; } @@ -412,6 +797,23 @@ public sealed class ModelPolicy public string? Terms { get; set; } } +/// A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderRef +{ + /// Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// The provider kind. + [JsonPropertyName("kind")] + public ModelProviderKind Kind { get; set; } + + /// Human-readable provider label, owned by the runtime so every consumer renders identical text. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; +} + /// Service-published warning text that hosts should display when presenting a model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ModelWarningText @@ -465,6 +867,10 @@ public sealed class Model [JsonPropertyName("policy")] public ModelPolicy? Policy { get; set; } + /// The model provider that produced this model, as a neutral reference (opaque id, human-readable label, and provider kind). Present on models returned by `session.model.list`, which resolves provider attribution from the session's account roster; absent on the flat `server.models.list`, which does not resolve a session roster. The model picker groups by this reference. + [JsonPropertyName("provider")] + public ModelProviderRef? Provider { get; set; } + /// Context-window tiers this model offers, when the provider advertises them independently of tiered token pricing. Copilot models carry their tiers in `billing.tokenPrices`; a provider that has no pricing to publish (an agent host reached over AHP, for example) declares them here instead, so the model picker can still offer the tier toggle. [JsonPropertyName("supportedContextTiers")] public IList? SupportedContextTiers { get; set; } @@ -473,6 +879,10 @@ public sealed class Model [JsonPropertyName("supportedReasoningEfforts")] public IList? SupportedReasoningEfforts { get; set; } + /// Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. + [JsonPropertyName("vendor")] + public string? Vendor { get; set; } + /// Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. [JsonPropertyName("warningMessages")] public IList? WarningMessages { get; set; } @@ -522,11 +932,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; @@ -556,6 +966,234 @@ public sealed class SandboxHostSupport public bool Supported { get; set; } } +/// Status of the persistent certificate authority of the sandbox credential proxy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxProxyCaStatus +{ + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + [JsonPropertyName("canInstall")] + public bool CanInstall { get; set; } + + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// The state of the certificate authority. + [JsonPropertyName("state")] + public SandboxProxyCaState State { get; set; } +} + +/// Credential-injection capability flags applied while the sandbox is enabled. For the same capability independent of sandboxing, and matched to the credential's GitHub host, see `shell.credentials`; the two are additive. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigAuth +{ + /// Whether to authenticate sandboxed gh through the local masking proxy. The child receives a fake GH_TOKEN; its real value is substituted only at github.com, api.github.com and uploads.github.com (github.com because gh repo clone authenticates git through gh auth git-credential). The repository's GitHub account takes precedence over the Copilot login. Default: false (opt-in). + [JsonPropertyName("gh")] + public bool? Gh { get; set; } + + /// Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http.<url>.extraheader`; the real Authorization header is substituted only at its original HTTPS host, port, and repository path scope. github.com uses the Copilot token; other forges use credentials resolved from the user's own helper on the host. Default: false (opt-in). + [JsonPropertyName("git")] + public bool? Git { get; set; } +} + +/// Destinations authorized to receive one masked environment credential. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxMaskedEnvVar +{ + /// Nonempty list of HTTPS injection hostnames or *.example.com patterns. Bare * is not accepted. These grants never override the sandbox network policy. Values in plaintext HTTP requests, URLs, bodies, encoded credentials, and signed requests are not substituted. + [JsonPropertyName("injectHosts")] + public IList InjectHosts { get => field ??= []; set; } +} + +/// Whole-value environment credential masking for sandboxed children. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxCredentialsConfig +{ + /// Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. + [JsonPropertyName("envVars")] + public IDictionary EnvVars { get => field ??= new Dictionary(); set; } +} + +/// macOS seatbelt experimental options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyExperimentalSeatbelt +{ + /// Whether the macOS seatbelt profile may access the keychain. + [JsonPropertyName("keychainAccess")] + public bool? KeychainAccess { get; set; } +} + +/// Platform-specific experimental policy fields. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyExperimental +{ + /// macOS seatbelt experimental options. + [JsonPropertyName("seatbelt")] + public SandboxConfigUserPolicyExperimentalSeatbelt? Seatbelt { get; set; } +} + +/// Filesystem rules to merge into the base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyFilesystem +{ + /// Whether to clear the policy when the session exits. + [JsonPropertyName("clearPolicyOnExit")] + public bool? ClearPolicyOnExit { get; set; } + + /// Paths explicitly denied. + [JsonPropertyName("deniedPaths")] + public IList? DeniedPaths { get; set; } + + /// Paths granted read-only access. + [JsonPropertyName("readonlyPaths")] + public IList? ReadonlyPaths { get; set; } + + /// Paths granted read/write access. + [JsonPropertyName("readwritePaths")] + public IList? ReadwritePaths { get; set; } +} + +/// HTTP proxy configuration for sandboxed traffic. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyNetworkProxy +{ + /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. + [JsonPropertyName("password")] + public string? Password { get; set; } + + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. + [JsonPropertyName("username")] + public string? Username { get; set; } +} + +/// Network rules to merge into the base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyNetwork +{ + /// Hosts allowed through the built-in sandbox proxy. A non-empty list denies unmatched hosts; an absent or empty list allows all hosts not blocked. Supports exact hostnames, IP addresses, and *.example.com for strict subdomains. Host rules do not override the outbound or local-network toggles. + [JsonPropertyName("allowedHosts")] + public IList? AllowedHosts { get; set; } + + /// Whether traffic to local/loopback addresses is allowed. + [JsonPropertyName("allowLocalNetwork")] + public bool? AllowLocalNetwork { get; set; } + + /// Whether outbound network traffic is allowed at all. + [JsonPropertyName("allowOutbound")] + public bool? AllowOutbound { get; set; } + + /// Hosts denied by the built-in sandbox proxy. Deny rules take precedence over allowedHosts. A domain also denies all its subdomains. IP addresses match exactly; *.example.com matches strict subdomains, and * denies every host. + [JsonPropertyName("blockedHosts")] + public IList? BlockedHosts { get; set; } + + /// HTTP(S) proxy for sandboxed traffic. This is the built-in local proxy's upstream: every sandboxed command reaches it through a loopback listener, so credentials stay in the runtime and never reach the child. On Windows the sandbox also needs local network access, because it reaches that listener over host loopback. Configure credentials in the separate username/password fields. The transient local listener URL is never persisted. + [JsonPropertyName("proxy")] + public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } +} + +/// macOS seatbelt-specific options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicySeatbelt +{ + /// Whether the macOS seatbelt profile may access the keychain. + [JsonPropertyName("keychainAccess")] + public bool? KeychainAccess { get; set; } +} + +/// User-managed sandbox policy fragment merged into the auto-discovered base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicy +{ + /// Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. + [JsonPropertyName("experimental")] + public SandboxConfigUserPolicyExperimental? Experimental { get; set; } + + /// Filesystem rules to merge into the base policy. + [JsonPropertyName("filesystem")] + public SandboxConfigUserPolicyFilesystem? Filesystem { get; set; } + + /// Network rules to merge into the base policy. + [JsonPropertyName("network")] + public SandboxConfigUserPolicyNetwork? Network { get; set; } + + /// macOS seatbelt options to merge into the base policy. + [JsonPropertyName("seatbelt")] + public SandboxConfigUserPolicySeatbelt? Seatbelt { get; set; } +} + +/// Resolved sandbox configuration. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfig +{ + /// Whether to auto-add the current working directory to readwritePaths. Default: true. + [JsonPropertyName("addCurrentWorkingDirectory")] + public bool? AddCurrentWorkingDirectory { get; set; } + + /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + [JsonPropertyName("allowBypass")] + public bool? AllowBypass { get; set; } + + /// Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("allowDevToolAccess")] + public bool? AllowDevToolAccess { get; set; } + + /// Credential-injection capability flags. + [JsonPropertyName("auth")] + public SandboxConfigAuth? Auth { get; set; } + + /// Opt-in whole-value environment masking for sandboxed shell, MCP, and LSP children. Configured names get random sentinels; the local proxy substitutes them only in HTTPS request headers at their injection hosts. Approved bypasses skip masking and the sandbox proxy, so bypassed shells may receive the real environment values. Disabled or explicitly opted-out routes are not protected. No credential values are stored in this configuration. + [JsonPropertyName("credentials")] + public SandboxCredentialsConfig? Credentials { get; set; } + + /// Whether sandboxing is enabled for the session. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + [JsonInclude] + [JsonPropertyName("managedLspRoutingLocked")] + internal bool? ManagedLspRoutingLocked { get; set; } + + /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + [JsonInclude] + [JsonPropertyName("managedMcpRoutingLocked")] + internal bool? ManagedMcpRoutingLocked { get; set; } + + /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxLspServers")] + public bool? SandboxLspServers { get; set; } + + /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxMcpServers")] + public bool? SandboxMcpServers { get; set; } + + /// User-managed sandbox policy fragment merged into the auto-discovered base policy. + [JsonPropertyName("userPolicy")] + public SandboxConfigUserPolicy? UserPolicy { get; set; } +} + +/// Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SandboxProxyCaRequest +{ + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + [JsonPropertyName("sandboxConfig")] + public SandboxConfig? SandboxConfig { get; set; } +} + +/// Result of creating the persistent certificate authority of the sandbox credential proxy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxProxyCaCreateResult +{ + /// Absolute path of the public certificate of the certificate authority, in PEM format. + [JsonPropertyName("certificatePath")] + public string CertificatePath { get; set; } = string.Empty; +} + /// Built-in tool metadata with identifier, optional namespaced name, description, input-parameter schema, and usage instructions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class Tool @@ -675,6 +1313,7 @@ internal sealed class AccountGetQuotaRequest [JsonDerivedType(typeof(AuthInfoTokenProvider), "token-provider")] [JsonDerivedType(typeof(AuthInfoCopilotApiToken), "copilot-api-token")] [JsonDerivedType(typeof(AuthInfoUser), "user")] +[JsonDerivedType(typeof(AuthInfoAccount), "account")] [JsonDerivedType(typeof(AuthInfoGhCli), "gh-cli")] [JsonDerivedType(typeof(AuthInfoApiKey), "api-key")] public partial class AuthInfo @@ -1171,6 +1810,24 @@ public partial class AuthInfoUser : AuthInfo public required string Login { get; set; } } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthInfoAccount : AuthInfo +{ + /// + [JsonIgnore] + public override string Type => "account"; + + /// Host coordinate owned by the account's model provider. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// Login identifying the provider-owned account. + [JsonPropertyName("login")] + public required string Login { get; set; } +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// The gh-cli variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -2319,11 +2976,18 @@ public partial class McpPlanInstallSourceCard : McpPlanInstallSource /// A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpPlanInstallRequest +public sealed class McpPlanInstallRequest { /// Protocol version and capabilities the caller requires. [JsonPropertyName("contract")] - public CatalogClientContract Contract { get => field ??= new(); set; } + public required CatalogClientContract Contract { get; set; } + + /// The same existing attached session that owns the original catalogue candidate. + [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(256)] + [JsonPropertyName("policySessionId")] + public string? PolicySessionId { get; set; } /// Configuration scope the plan targets. Defaults to user scope when omitted. [JsonPropertyName("scope")] @@ -2331,171 +2995,19 @@ internal sealed class McpPlanInstallRequest /// What to plan: either a candidate handle from a previous search, or a card supplied directly. [JsonPropertyName("source")] - public McpPlanInstallSource Source { get => field ??= new(); set; } -} - -/// User-configured MCP servers, keyed by server name. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpConfigList -{ - /// All MCP servers from user config, keyed by name. - [JsonPropertyName("servers")] - public IDictionary Servers { get => field ??= new Dictionary(); set; } -} - -/// MCP server name and configuration to add to user configuration. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigAddRequest -{ - /// MCP server configuration (stdio process or remote HTTP/SSE). - [JsonPropertyName("config")] - public JsonElement Config { get; set; } - - /// Unique name for the MCP server. - [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; -} - -/// MCP server name and replacement configuration to write to user configuration. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigUpdateRequest -{ - /// MCP server configuration (stdio process or remote HTTP/SSE). - [JsonPropertyName("config")] - public JsonElement Config { get; set; } - - /// Name of the MCP server to update. - [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; -} - -/// MCP server name to remove from user configuration. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigRemoveRequest -{ - /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. - [JsonPropertyName("authClientIdMetadataUrl")] - public string? AuthClientIdMetadataUrl { get; set; } - - /// Name of the MCP server to remove. - [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; -} - -/// MCP server names to enable for new sessions. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigEnableRequest -{ - /// Names of MCP servers to enable. Each server is removed from the persisted disabled list so new sessions spawn it. Unknown or already-enabled names are ignored. - [JsonPropertyName("names")] - public IList Names { get => field ??= []; set; } -} - -/// MCP server names to disable for new sessions. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigDisableRequest -{ - /// Names of MCP servers to disable. Each server is added to the persisted disabled list so new sessions skip it. Already-disabled names are ignored. Active sessions keep their current connections until they end. - [JsonPropertyName("names")] - public IList Names { get => field ??= []; set; } -} - -/// Installed plugin that contributes a discovered extension. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiscoveredExtensionPlugin -{ - /// Installed plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; -} - -/// Discovered extension metadata and persistent enablement state. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiscoveredExtension -{ - /// Whether this extension's persistent per-ID preference is enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } - - /// Source-qualified ID accepted by both server and session extension enablement methods. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Human-readable extension name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Absolute path to the extension entry module, suitable for revealing it in a file manager. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Containing plugin metadata for plugin-contributed extensions. - [JsonPropertyName("plugin")] - public DiscoveredExtensionPlugin? Plugin { get; set; } - - /// Discovery source. - [JsonPropertyName("source")] - public DiscoveredExtensionSource Source { get; set; } -} - -/// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiscoveredExtensions -{ - /// Discovered user and enabled installed-plugin extensions from persisted Copilot home state. - [JsonPropertyName("extensions")] - public IList Extensions { get => field ??= []; set; } - - /// Effective extension loading mode. Defaults to load_and_augment when unset. - [JsonPropertyName("mode")] - public DiscoveredExtensionMode Mode { get; set; } -} - -/// Source-qualified extension identifiers to persistently enable for future sessions. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DiscoveredExtensionsEnableRequest -{ - /// Source-qualified user or plugin extension IDs to enable. - [JsonPropertyName("ids")] - public IList Ids { get => field ??= []; set; } -} - -/// Source-qualified extension identifiers to persistently disable for future sessions. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DiscoveredExtensionsDisableRequest -{ - /// Source-qualified user or plugin extension IDs to disable. - [JsonPropertyName("ids")] - public IList Ids { get => field ??= []; set; } + public required McpPlanInstallSource Source { get; set; } } -/// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. +/// Management result with contract receipt, or a typed request/negotiation refusal. /// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(CatalogSearchResultSucceeded), "succeeded")] -[JsonDerivedType(typeof(CatalogSearchResultNegotiationRefused), "negotiation-refused")] -[JsonDerivedType(typeof(CatalogSearchResultUnsupportedKind), "unsupported-kind")] -[JsonDerivedType(typeof(CatalogSearchResultInvalidRequest), "invalid-request")] -[JsonDerivedType(typeof(CatalogSearchResultAuthenticationRequired), "authentication-required")] -[JsonDerivedType(typeof(CatalogSearchResultPolicyRejected), "policy-rejected")] -[JsonDerivedType(typeof(CatalogSearchResultNetworkFailure), "network-failure")] -[JsonDerivedType(typeof(CatalogSearchResultUnsafeRetrieval), "unsafe-retrieval")] -[JsonDerivedType(typeof(CatalogSearchResultMalformedCard), "malformed-card")] -[JsonDerivedType(typeof(CatalogSearchResultContractViolation), "contract-violation")] -[JsonDerivedType(typeof(CatalogSearchResultUnavailable), "unavailable")] -public partial class CatalogSearchResult +[JsonDerivedType(typeof(McpInstallationManagementResultOutcome), "outcome")] +[JsonDerivedType(typeof(McpInstallationManagementResultNegotiationRefused), "negotiation-refused")] +[JsonDerivedType(typeof(McpInstallationManagementResultInvalidRequest), "invalid-request")] +public partial class McpInstallationManagementResult { /// The type discriminator. [JsonPropertyName("kind")] @@ -2503,16 +3015,20 @@ public partial class CatalogSearchResult } -/// One inert catalog result, represented as an MCP server, discovery-only AI skill, or opt-in Agent Plugin variant so kind, media type, provenance, and available operations cannot contradict each other. +/// Read-only or recovery management result, never permission to activate or replay. /// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(CatalogCandidateMcpServer), "mcp-server")] -[JsonDerivedType(typeof(CatalogCandidateAiSkill), "ai-skill")] -[JsonDerivedType(typeof(CatalogCandidatePlugin), "plugin")] -public partial class CatalogCandidate +[JsonDerivedType(typeof(McpInstallationManagementOutcomeRecoveryRequired), "recovery-required")] +[JsonDerivedType(typeof(McpInstallationManagementOutcomeInstallPrepared), "install-prepared")] +[JsonDerivedType(typeof(McpInstallationManagementOutcomeListed), "listed")] +[JsonDerivedType(typeof(McpInstallationManagementOutcomeRecovered), "recovered")] +[JsonDerivedType(typeof(McpInstallationManagementOutcomeUninstallPlanned), "uninstall-planned")] +[JsonDerivedType(typeof(McpInstallationManagementOutcomeOperation), "operation")] +[JsonDerivedType(typeof(McpInstallationManagementOutcomeRefused), "refused")] +public partial class McpInstallationManagementOutcome { /// The type discriminator. [JsonPropertyName("kind")] @@ -2520,65 +3036,40 @@ public partial class CatalogCandidate } -/// Where and when an MCP server catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CatalogMcpServerCandidateProvenance -{ - /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. - [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("authority")] - public string Authority { get; set; } = string.Empty; - - /// JSON MCP media type advertised for the referenced card. - [JsonPropertyName("mediaType")] - public McpServerCardMediaType MediaType { get; set; } - - /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. - [JsonPropertyName("observedAt")] - public string ObservedAt { get; set; } = string.Empty; -} - -/// Where a candidate's card came from. Exactly one of a URL or embedded data: the union has no variant carrying both, and no variant carrying neither, so the rule holds structurally rather than by validation. -/// Polymorphic base type discriminated by kind. +/// Already-confirmed durable work must be reconciled before new mutations or inventory. +/// The recovery-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(CatalogCandidateSourceUrl), "url")] -[JsonDerivedType(typeof(CatalogCandidateSourceEmbedded), "embedded")] -public partial class CatalogCandidateSource +public partial class McpInstallationManagementOutcomeRecoveryRequired : McpInstallationManagementOutcome { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "recovery-required"; } - -/// Candidate whose card is retrieved from a URL through the runtime's hardened fetch boundary. -/// The url variant of . +/// Inert, runtime-owned admission. The operation ID is known before confirmation or effects. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogCandidateSourceUrl : CatalogCandidateSource +public sealed class McpPreparedInstall { - /// - [JsonIgnore] - public override string Kind => "url"; + /// Original plan expiry in Unix epoch milliseconds; preparation does not extend it. + [JsonPropertyName("expiresAtEpochMs")] + public long ExpiresAtEpochMs { get; set; } - /// Card URL as advertised. Inert untrusted data: the runtime retrieves it only through its own hardened boundary, and it is never logged. - [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("url")] - public required string Url { get; set; } + /// Original connection-owned operation, known before the first confirmation callback. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; } -/// Candidate whose card reference arrived inline. The document and its content-derived properties stay behind the runtime boundary. -/// The embedded variant of . +/// The install-prepared variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogCandidateSourceEmbedded : CatalogCandidateSource +public partial class McpInstallationManagementOutcomeInstallPrepared : McpInstallationManagementOutcome { /// [JsonIgnore] - public override string Kind => "embedded"; + public override string Kind => "install-prepared"; + + /// Known original operation identity, returned before callback or effects. + [JsonPropertyName("operation")] + public required McpPreparedInstall Operation { get; set; } } /// A versioned, bounded trust observation carried unchanged with a catalog candidate and its private handle context. Current observations require a recognised T1/T2 tier; every non-current state structurally forbids a tier. Eligibility remains `unknown` while Agent Finder supplies no exposure decision, and states absent from its current wire are never inferred from age, relevance, popularity, or a tier transition. @@ -2776,543 +3267,472 @@ public partial class CatalogTrustSnapshotMalformed : CatalogTrustSnapshot public required CatalogTrustSnapshotSchemaVersion SchemaVersion { get; set; } } -/// An inert MCP server catalog result. Every free-text field is untrusted external data and must never be treated as an instruction, and the handle is the only way to refer to the candidate in a later operation. -/// The mcp-server variant of . +/// Catalogue identity retained from a bound candidate or plan at installation time. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogCandidateMcpServer : CatalogCandidate +public sealed class InstallationCatalogueIdentity { - /// - [JsonIgnore] - public override string Kind => "mcp-server"; - - /// Description taken verbatim from the card. Inert untrusted text. - [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(1000)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Catalogue description retained at install planning time. [JsonPropertyName("description")] public string? Description { get; set; } - /// Display name taken verbatim from the card. Inert untrusted text. - [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(200)] + /// Human display name retained from the catalogue candidate. [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } - - /// Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. - [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("handle")] - public required string Handle { get; set; } - - /// ISO 8601 timestamp after which the handle is stale and will be rejected. - [JsonPropertyName("handleExpiresAt")] - public required string HandleExpiresAt { get; set; } - - /// Whether this MCP server can be planned for installation, and if policy prevents it. - [JsonPropertyName("installability")] - public required CatalogMcpServerInstallability Installability { get; set; } - - /// JSON MCP media type of the underlying card. - [JsonPropertyName("mediaType")] - public required McpServerCardMediaType MediaType { get; set; } + public string DisplayName { get; set; } = string.Empty; - /// Where the catalog reference was observed, without the card itself or any content digest. - [JsonPropertyName("provenance")] - public required CatalogMcpServerCandidateProvenance Provenance { get; set; } + /// Catalogue item URL when supplied by the authority. + [JsonPropertyName("itemUrl")] + public string? ItemUrl { get; set; } - /// Publisher taken verbatim from the card. Inert untrusted text. - [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(200)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Catalogue publisher retained at install planning time. [JsonPropertyName("publisher")] public string? Publisher { get; set; } - /// Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. + /// Authority resource identifier when supplied by the catalogue. + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + + /// Catalogue authority/source string that supplied the candidate. [JsonPropertyName("source")] - public required CatalogCandidateSource Source { get; set; } + public string Source { get; set; } = string.Empty; - /// Versioned trust metadata observed from the catalog authority. Optional for protocol-3 compatibility with runtimes that predate trust snapshots. A trust-capable runtime emits an explicit snapshot even when the authority omitted or malformed its trust field. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("trust")] - public CatalogTrustSnapshot? Trust { get; set; } + /// Catalogue trust observation retained at install planning time. + [JsonPropertyName("trustAtInstall")] + public CatalogTrustSnapshot? TrustAtInstall { get; set; } + + /// Catalogue version retained at install planning time. + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Where and when an AI skill catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. +/// Durable configuration ownership is distinct from session-specific usability. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CatalogAiSkillCandidateProvenance +public sealed class McpInstallationSummary { - /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. - [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("authority")] - public string Authority { get; set; } = string.Empty; + /// Catalogue identity retained from the installed plan when available. + [JsonPropertyName("catalogue")] + public InstallationCatalogueIdentity? Catalogue { get; set; } - /// Media type advertised for the referenced AI skill card. - [JsonPropertyName("mediaType")] - public CatalogAiSkillMediaType MediaType { get; set; } + /// Exact alternative retained in the installing receipt. + [JsonPropertyName("choiceId")] + public string ChoiceId { get; set; } = string.Empty; - /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. - [JsonPropertyName("observedAt")] - public string ObservedAt { get; set; } = string.Empty; + /// Identity retained from the validated original plan. + [JsonPropertyName("identity")] + public McpPlanResourceIdentity Identity { get => field ??= new(); set; } + + /// Exact durable installation receipt identity. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; + + /// ISO 8601 wall-clock installation time when available. + [JsonPropertyName("installedAt")] + public string? InstalledAt { get; set; } + + /// Original installing operation, not a fresh management operation. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; + + /// Ownership or setup state, never inferred proof of tool usability. + [JsonPropertyName("state")] + public McpInstallationState State { get; set; } } -/// An inert AI skill catalog result. AI skills are discovery-only and cannot be represented as installable through this surface. -/// The ai-skill variant of . +/// The listed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogCandidateAiSkill : CatalogCandidate +public partial class McpInstallationManagementOutcomeListed : McpInstallationManagementOutcome { /// [JsonIgnore] - public override string Kind => "ai-skill"; + public override string Kind => "listed"; - /// Description taken verbatim from the card. Inert untrusted text. - [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(1000)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Owned receipts visible to the selected account and host. + [JsonPropertyName("installations")] + public required IList Installations { get; set; } +} - /// Display name taken verbatim from the card. Inert untrusted text. - [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(200)] - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } +/// The recovered variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpInstallationManagementOutcomeRecovered : McpInstallationManagementOutcome +{ + /// + [JsonIgnore] + public override string Kind => "recovered"; - /// Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. - [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("handle")] - public required string Handle { get; set; } + /// Freshly inspected receipts after successful durable reconciliation. + [JsonPropertyName("installations")] + public required IList Installations { get; set; } +} - /// ISO 8601 timestamp after which the handle is stale and will be rejected. - [JsonPropertyName("handleExpiresAt")] - public required string HandleExpiresAt { get; set; } +/// Exact inert removal plan. No configuration or credentials have changed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpUninstallPlan +{ + /// Original wall-clock expiry in milliseconds. Applying never renews it. + [JsonPropertyName("expiresAtEpochMs")] + public long ExpiresAtEpochMs { get; set; } - /// AI skills are discovery-only and cannot be installed through this surface. - [JsonPropertyName("installability")] - public required CatalogAiSkillInstallability Installability { get; set; } + /// Original owned receipt being removed. + [JsonPropertyName("installation")] + public McpInstallationSummary Installation { get => field ??= new(); set; } - /// Media type of the underlying AI skill card. - [JsonPropertyName("mediaType")] - public required CatalogAiSkillMediaType MediaType { get; set; } + /// The original operation, inspectable and cancellable on this same connection. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; - /// Where the catalog reference was observed, without the card itself or any content digest. - [JsonPropertyName("provenance")] - public required CatalogAiSkillCandidateProvenance Provenance { get; set; } + /// Exact configured input slots owned by this installation, never shared OAuth tokens. + [JsonPropertyName("ownedSecretCount")] + public long OwnedSecretCount { get; set; } - /// Publisher taken verbatim from the card. Inert untrusted text. - [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(200)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("publisher")] - public string? Publisher { get; set; } + /// One-use original connection and authority-bound plan handle. + [JsonPropertyName("planHandle")] + public string PlanHandle { get; set; } = string.Empty; - /// Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. - [JsonPropertyName("source")] - public required CatalogCandidateSource Source { get; set; } + /// Shared authentication is deliberately retained; revocation is a separate action. + [JsonPropertyName("preservesSharedAuthentication")] + public bool PreservesSharedAuthentication { get; set; } - /// Versioned trust metadata observed from the catalog authority. Optional for protocol-3 compatibility with runtimes that predate trust snapshots. A trust-capable runtime emits an explicit snapshot even when the authority omitted or malformed its trust field. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("trust")] - public CatalogTrustSnapshot? Trust { get; set; } + /// Whether removal restores a protected earlier configuration. + [JsonPropertyName("restoresPreviousConfiguration")] + public bool RestoresPreviousConfiguration { get; set; } } -/// Where and when an Agent Plugin catalog reference was observed. Discovery provenance deliberately carries no descriptor URL, raw data, candidate handle, or content digest. +/// The uninstall-planned variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CatalogAgentPluginCandidateProvenance +public partial class McpInstallationManagementOutcomeUninstallPlanned : McpInstallationManagementOutcome { - /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. - [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("authority")] - public string Authority { get; set; } = string.Empty; - - /// Canonical Agent Plugin media type. - [JsonPropertyName("mediaType")] - public CatalogAgentPluginMediaType MediaType { get; set; } + /// + [JsonIgnore] + public override string Kind => "uninstall-planned"; - /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. - [JsonPropertyName("observedAt")] - public string ObservedAt { get; set; } = string.Empty; + /// Original owned removal plan and operation. + [JsonPropertyName("plan")] + public required McpUninstallPlan Plan { get; set; } } -/// Syntactically validated GitHub repository provenance declared by catalog metadata. This is a source claim rather than proof that the descriptor URL resolves to the repository. +/// Status snapshot from the original connection, independent of new-work account availability. +/// Polymorphic base type discriminated by phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CatalogPluginRepositorySource +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "phase", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpInstallationOperationStatusPreparing), "preparing")] +[JsonDerivedType(typeof(McpInstallationOperationStatusPrepared), "prepared")] +[JsonDerivedType(typeof(McpInstallationOperationStatusAwaitingConfirmation), "awaiting-confirmation")] +[JsonDerivedType(typeof(McpInstallationOperationStatusRevalidating), "revalidating")] +[JsonDerivedType(typeof(McpInstallationOperationStatusApplying), "applying")] +[JsonDerivedType(typeof(McpInstallationOperationStatusCompleted), "completed")] +public partial class McpInstallationOperationStatus { - /// Case-preserving safe relative POSIX path derived from metadata.repoPath. - [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(256)] - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Canonical lowercase owner/repository name derived from metadata.sourceSet. - [RegularExpression("^[a-z0-9_.-]+/[a-z0-9_.-]+$")] - [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(3)] - [MaxLength(256)] - [JsonPropertyName("repository")] - public string Repository { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("phase")] + public virtual string Phase { get; set; } = string.Empty; } -/// An inert Agent Plugin catalog result. Its canonical catalog identity, declared version, repository source claim, and explicit compatibility tags are safe to correlate, while its descriptor, URL, raw data, and installed-plugin state remain runtime-private. This contract-only variant does not mint or expose a candidate handle. -/// The plugin variant of . + +/// The preparing variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogCandidatePlugin : CatalogCandidate +public partial class McpInstallationOperationStatusPreparing : McpInstallationOperationStatus { /// [JsonIgnore] - public override string Kind => "plugin"; + public override string Phase => "preparing"; - /// Explicit validated compatibility tags, in canonical order. An empty list means the source declared no recognised compatibility; clients must not infer compatibility from other fields. `canvas-only` requires both `canvas` and `github-copilot`. - [JsonPropertyName("compatibilityTags")] - public required IList CompatibilityTags { get; set; } + /// Whether cancellation has been requested, not proof that a write was undone. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// Description taken verbatim from the card. Inert untrusted text. - [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(1000)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } +} - /// Display name taken verbatim from the card. Inert untrusted text. - [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(200)] - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } +/// The prepared variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpInstallationOperationStatusPrepared : McpInstallationOperationStatus +{ + /// + [JsonIgnore] + public override string Phase => "prepared"; - /// Validated, normalised catalogue resource URN. This identity comes only from the catalog identifier and is never inferred from display text or installed-plugin state. - [JsonPropertyName("identity")] - public required string Identity { get; set; } + /// Whether the inert prepared operation was asked to cancel. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// Canonical Agent Plugin media type. - [JsonPropertyName("mediaType")] - public required CatalogAgentPluginMediaType MediaType { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } +} - /// Where the Agent Plugin catalog reference was observed, without its descriptor, URL, raw data, or content digest. - [JsonPropertyName("provenance")] - public required CatalogAgentPluginCandidateProvenance Provenance { get; set; } +/// The awaiting-confirmation variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpInstallationOperationStatusAwaitingConfirmation : McpInstallationOperationStatus +{ + /// + [JsonIgnore] + public override string Phase => "awaiting-confirmation"; - /// Publisher taken verbatim from the card. Inert untrusted text. - [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(200)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("publisher")] - public string? Publisher { get; set; } + /// Whether the pending human callback was asked to cancel. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// Bounded repository provenance declared through the catalog's sourceSet and repoPath metadata. It contains no descriptor URL. - [JsonPropertyName("source")] - public required CatalogPluginRepositorySource Source { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } +} - /// Versioned trust metadata observed from the catalog authority. Optional for protocol-3 compatibility and emitted only when the caller also requires the trust-snapshot capability. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("trust")] - public CatalogTrustSnapshot? Trust { get; set; } +/// The revalidating variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpInstallationOperationStatusRevalidating : McpInstallationOperationStatus +{ + /// + [JsonIgnore] + public override string Phase => "revalidating"; - /// Optional version declared by the catalog source. Omitted rather than guessed when the source supplies no version. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Whether source or authority revalidation was asked to cancel. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } + + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Authority-reported navigation metadata, returned only to callers requiring catalog-search-pagination and only when a supported token is present. Tokenless first-page and continuation responses omit this object; no counts are inferred from candidates. The opaque token may be retained for previous or numbered navigation even when hasNextPage is false. +/// The applying variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CatalogSearchPagination +public partial class McpInstallationOperationStatusApplying : McpInstallationOperationStatus { - /// One-based page returned by the authority. - [JsonPropertyName("currentPage")] - public int CurrentPage { get; set; } + /// + [JsonIgnore] + public override string Phase => "applying"; - /// Whether the authority token advertises a valid next target within the navigation window. Not inferred from token presence, truncated, or currentPage being less than pageCount. - [JsonPropertyName("hasNextPage")] - public bool HasNextPage { get; set; } + /// Whether applying was asked to cancel; already-started effects retain their lease. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// Navigation window ceiling ceil(1000 / pageSize), not the number of existing pages. Legal targets must not exceed this ceiling or the token's signed pageCount. - [JsonPropertyName("maxPage")] - public int MaxPage { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } +} - /// Backend-reported page count, which may exceed maxPage. Present pagination metadata always describes a multi-page result; zero- and single-page responses omit pagination. Navigation targets must also be within the signed pageCount carried by the supplied token. - [JsonPropertyName("pageCount")] - public long PageCount { get; set; } +/// Terminal mutation result. Uncertainty is not approval, rollback or permission to replay. +/// Polymorphic base type discriminated by kind. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpInstallationOutcomeInstalled), "installed")] +[JsonDerivedType(typeof(McpInstallationOutcomeUninstalled), "uninstalled")] +[JsonDerivedType(typeof(McpInstallationOutcomeRolledBack), "rolled-back")] +[JsonDerivedType(typeof(McpInstallationOutcomeRecoveryRequired), "recovery-required")] +[JsonDerivedType(typeof(McpInstallationOutcomeDeclined), "declined")] +[JsonDerivedType(typeof(McpInstallationOutcomeCancelled), "cancelled")] +[JsonDerivedType(typeof(McpInstallationOutcomeRefused), "refused")] +public partial class McpInstallationOutcome +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Page size bound to the search, equal to the effective request limit. - [JsonPropertyName("pageSize")] - public int PageSize { get; set; } - /// Opaque authority-issued pagination token. Only the runtime decodes it or changes targetPage; SDK consumers must not decode, modify or log it. It has no runtime-created expiry or cache. - [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(4096)] - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; +/// The installed variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpInstallationOutcomeInstalled : McpInstallationOutcome +{ + /// + [JsonIgnore] + public override string Kind => "installed"; - /// Backend-reported count for this response, not the number of returned candidates. Its relationship to the full query result set is unknown. - [JsonPropertyName("totalCount")] - public long TotalCount { get; set; } + /// Durable installation succeeded but final transaction cleanup remains. + [JsonPropertyName("cleanupPending")] + public required bool CleanupPending { get; set; } - /// The relationship of totalCount to the complete query result set is unknown; neither exactness nor a lower-bound guarantee is implied. - [JsonPropertyName("totalCountRelation")] - public CatalogSearchTotalCountRelation TotalCountRelation { get; set; } + /// Receipt identity produced by the confirmed transaction. + [JsonPropertyName("installation")] + public required McpInstallationSummary Installation { get; set; } } -/// A completed catalog search containing inert candidate summaries. MCP server and AI skill variants carry a single-use handle; the Agent Plugin variant is handleless. -/// The succeeded variant of . +/// The uninstalled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultSucceeded : CatalogSearchResult +public partial class McpInstallationOutcomeUninstalled : McpInstallationOutcome { /// [JsonIgnore] - public override string Kind => "succeeded"; + public override string Kind => "uninstalled"; - /// Matching candidates, never more than the requested limit. All text is inert untrusted data. - [JsonPropertyName("candidates")] - public required IList Candidates { get; set; } + /// Durable removal succeeded but final cleanup remains. + [JsonPropertyName("cleanupPending")] + public required bool CleanupPending { get; set; } - /// Protocol version and capabilities the runtime honoured. - [JsonPropertyName("negotiated")] - public required CatalogNegotiatedContract Negotiated { get; set; } + /// Exact removed receipt identity. + [JsonPropertyName("installationId")] + public required string InstallationId { get; set; } - /// Navigation metadata for callers explicitly requiring catalog-search-pagination. Omitted when the authority returns no token, including tokenless first-page and continuation responses. Counts are never substituted from candidates.length. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pagination")] - public CatalogSearchPagination? Pagination { get; set; } + /// Original removal operation. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } - /// Pseudonymous identifier for this search, issued by the runtime or by the catalog authority it queried and never by the caller, so it cannot be forged or replayed to attribute an install to a search that never happened. Always present on a success, so a result set can be tied to the installs it leads to. It identifies a search rather than a person: it is derived from no user, account, device, or query data, and must never be joined with user identity to re-identify anyone. - [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(64)] - [JsonPropertyName("searchId")] - public required string SearchId { get; set; } + /// Any grants in the incumbent shared OAuth store remain unowned and retained. + [JsonPropertyName("preservedSharedAuthentication")] + public required bool PreservedSharedAuthentication { get; set; } - /// Legacy indication that the authority returned a page token. Preserved for compatibility; this is not a has-next-page indicator. Use pagination.hasNextPage when pagination metadata is present. - [JsonPropertyName("truncated")] - public required bool Truncated { get; set; } + /// Exact owned input slots removed, excluding shared OAuth credentials. + [JsonPropertyName("removedOwnedSecrets")] + public required long RemovedOwnedSecrets { get; set; } + + /// Whether protected pre-install configuration was restored. + [JsonPropertyName("restoredPreviousConfiguration")] + public required bool RestoredPreviousConfiguration { get; set; } } -/// The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. -/// The negotiation-refused variant of . +/// The durable transaction was aborted or fully compensated. +/// The rolled-back variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultNegotiationRefused : CatalogSearchResult +public partial class McpInstallationOutcomeRolledBack : McpInstallationOutcome { /// [JsonIgnore] - public override string Kind => "negotiation-refused"; + public override string Kind => "rolled-back"; - /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } - - /// Lowest caller protocol version this runtime will serve. - [JsonPropertyName("minimumSupportedProtocolVersion")] - public required long MinimumSupportedProtocolVersion { get; set; } + /// Original connection-owned operation. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } - /// Whether the version or the capability set was the problem. + /// Cause of the fully aborted or compensated operation. [JsonPropertyName("reason")] - public required CatalogNegotiationRefusedReason Reason { get; set; } - - /// Protocol version of the runtime that refused the request. - [JsonPropertyName("runtimeProtocolVersion")] - public required long RuntimeProtocolVersion { get; set; } - - /// Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. - [JsonPropertyName("supportedCapabilities")] - public required IList SupportedCapabilities { get; set; } - - /// The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. - [JsonPropertyName("unsupportedCapabilities")] - public required IList UnsupportedCapabilities { get; set; } + public required McpInstallationFailureReason Reason { get; set; } } -/// The request asked for a candidate kind this runtime does not serve. -/// The unsupported-kind variant of . +/// A write may have completed. Recover and inspect durable state before retrying. +/// The recovery-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultUnsupportedKind : CatalogSearchResult +public partial class McpInstallationOutcomeRecoveryRequired : McpInstallationOutcome { /// [JsonIgnore] - public override string Kind => "unsupported-kind"; - - /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } - - /// The kinds from the request that are not supported. - [JsonPropertyName("requestedKinds")] - public required IList RequestedKinds { get; set; } + public override string Kind => "recovery-required"; - /// Every candidate kind this runtime can serve. - [JsonPropertyName("supportedKinds")] - public required IList SupportedKinds { get; set; } + /// Operation whose durable result must be recovered and inspected. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. -/// The invalid-request variant of . +/// The declined variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultInvalidRequest : CatalogSearchResult +public partial class McpInstallationOutcomeDeclined : McpInstallationOutcome { /// [JsonIgnore] - public override string Kind => "invalid-request"; - - /// Which request field was rejected. - [JsonPropertyName("field")] - public required CatalogInvalidRequestField Field { get; set; } + public override string Kind => "declined"; - /// Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Original operation explicitly declined by the user. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// An optional catalog authentication exchange did not establish the caller's identity. Anonymous search remains supported; this refusal is reserved for an operation that cannot continue after the attempted exchange. It is distinct from `policy-rejected` and from a network failure, and the reason identifies the recovery action. -/// The authentication-required variant of . +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultAuthenticationRequired : CatalogSearchResult +public partial class McpInstallationOutcomeCancelled : McpInstallationOutcome { /// [JsonIgnore] - public override string Kind => "authentication-required"; - - /// Human-readable explanation, safe to surface. Never contains a credential or token, nor a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + public override string Kind => "cancelled"; - /// Why authentication failed. Only an expired credential justifies attempting a silent refresh; an absent or rejected credential requires sign-in. - [JsonPropertyName("reason")] - public required CatalogAuthenticationRequiredReason Reason { get; set; } + /// Original operation cancelled before a terminal application result. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Registry or enterprise policy refused the operation. -/// The policy-rejected variant of . +/// The refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultPolicyRejected : CatalogSearchResult +public partial class McpInstallationOutcomeRefused : McpInstallationOutcome { /// [JsonIgnore] - public override string Kind => "policy-rejected"; + public override string Kind => "refused"; - /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Present once an operation has been allocated; never a plan handle. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operationId")] + public string? OperationId { get; set; } - /// Which authority produced the decision. - [JsonPropertyName("source")] - public required McpPlanPolicySource Source { get; set; } + /// Specific bounded refusal, never a success-shaped fallback. + [JsonPropertyName("reason")] + public required McpInstallationFailureReason Reason { get; set; } } -/// The runtime could not reach the catalog authority or retrieve a card. Covers being offline as well as transport-level failure. -/// The network-failure variant of . +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultNetworkFailure : CatalogSearchResult +public partial class McpInstallationOperationStatusCompleted : McpInstallationOperationStatus { /// [JsonIgnore] - public override string Kind => "network-failure"; - - /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + public override string Phase => "completed"; - /// Categorised failure, low cardinality so it can be aggregated without carrying a URL. - [JsonPropertyName("reason")] - public required CatalogNetworkFailureReason Reason { get; set; } + /// Whether cancellation was requested before the terminal result. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("retryAfterSeconds")] - public int? RetryAfterSeconds { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } - /// HTTP status code, when the failure was a rejected response. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("statusCode")] - public int? StatusCode { get; set; } + /// Immutable terminal receipt. + [JsonPropertyName("outcome")] + public required McpInstallationOutcome Outcome { get; set; } } -/// Retrieval was refused by the runtime's hardened fetch boundary before any request left the process, or before a redirect was followed. -/// The unsafe-retrieval variant of . +/// The operation variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultUnsafeRetrieval : CatalogSearchResult +public partial class McpInstallationManagementOutcomeOperation : McpInstallationManagementOutcome { /// [JsonIgnore] - public override string Kind => "unsafe-retrieval"; - - /// Human-readable explanation, safe to surface. Never contains the refused URL, nor a query, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + public override string Kind => "operation"; - /// Which control refused the retrieval, low cardinality so it can be aggregated without carrying a URL. - [JsonPropertyName("reason")] - public required CatalogUnsafeRetrievalReason Reason { get; set; } + /// Original-connection operation snapshot. + [JsonPropertyName("operation")] + public required McpInstallationOperationStatus Operation { get; set; } } -/// A card could not be parsed or did not satisfy its declared media type's schema. -/// The malformed-card variant of . +/// The refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultMalformedCard : CatalogSearchResult +public partial class McpInstallationManagementOutcomeRefused : McpInstallationManagementOutcome { /// [JsonIgnore] - public override string Kind => "malformed-card"; - - /// Media type the card was interpreted as, when it declared one this runtime recognises. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("mediaType")] - public CatalogMediaType? MediaType { get; set; } - - /// Human-readable explanation, safe to surface. Never echoes card content, nor a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + public override string Kind => "refused"; - /// How the card failed validation. + /// Specific bounded refusal. [JsonPropertyName("reason")] - public required CatalogMalformedCardReason Reason { get; set; } + public required McpInstallationFailureReason Reason { get; set; } } -/// An upstream catalog response broke the wire contract. Most importantly, every result must carry exactly one of a URL or embedded data: a result carrying both, or neither, is refused here rather than being guessed at. -/// The contract-violation variant of . +/// The outcome variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultContractViolation : CatalogSearchResult +public partial class McpInstallationManagementResultOutcome : McpInstallationManagementResult { /// [JsonIgnore] - public override string Kind => "contract-violation"; + public override string Kind => "outcome"; - /// Human-readable explanation, safe to surface. Never echoes response content, nor a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Capabilities actually honoured for this request. + [JsonPropertyName("negotiated")] + public required CatalogNegotiatedContract Negotiated { get; set; } - /// Which rule the response broke. - [JsonPropertyName("reason")] - public required CatalogContractViolationReason Reason { get; set; } + /// Observed management outcome. + [JsonPropertyName("outcome")] + public required McpInstallationManagementOutcome Outcome { get; set; } } -/// The operation is not available on this runtime. Distinct from a network failure: nothing was attempted. -/// The unavailable variant of . +/// The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. +/// The negotiation-refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSearchResultUnavailable : CatalogSearchResult +public partial class McpInstallationManagementResultNegotiationRefused : McpInstallationManagementResult { /// [JsonIgnore] - public override string Kind => "unavailable"; + public override string Kind => "negotiation-refused"; /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] @@ -3320,241 +3740,158 @@ public partial class CatalogSearchResultUnavailable : CatalogSearchResult [JsonPropertyName("message")] public required string Message { get; set; } - /// Why the operation is unavailable. - [JsonPropertyName("reason")] - public required CatalogUnavailableReason Reason { get; set; } -} - -/// An explicit numbered-page request. The SDK treats the token as opaque; only the runtime decodes it and changes its targetPage. Authority validation binds navigation to the original search. No snapshot stability or token TTL is promised. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CatalogSearchPage -{ - /// Requested one-based page. Must not exceed either the token's signed pageCount or the navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly available pages beyond that signed pageCount. - [JsonPropertyName("number")] - public int Number { get; set; } - - /// Opaque authority-issued pagination token from an earlier response. Never decode, modify or log it in an SDK consumer. - [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(4096)] - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; -} - -/// 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CatalogSearchRequest -{ - /// Protocol version and capabilities the caller requires. - [JsonPropertyName("contract")] - public CatalogClientContract Contract { get => field ??= new(); set; } - - /// Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. - [JsonPropertyName("kinds")] - public IList? Kinds { get; set; } + /// Lowest caller protocol version this runtime will serve. + [JsonPropertyName("minimumSupportedProtocolVersion")] + public required long MinimumSupportedProtocolVersion { get; set; } - /// Maximum number of candidates to return. Defaults to 10 when omitted. - [JsonPropertyName("limit")] - public int? Limit { get; set; } + /// Whether the version or the capability set was the problem. + [JsonPropertyName("reason")] + public required CatalogNegotiationRefusedReason Reason { get; set; } - /// Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh first-page search. - [JsonPropertyName("page")] - public CatalogSearchPage? Page { get; set; } + /// Protocol version of the runtime that refused the request. + [JsonPropertyName("runtimeProtocolVersion")] + public required long RuntimeProtocolVersion { get; set; } - /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. - [RegularExpression("\\S")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [MaxLength(256)] - [JsonPropertyName("query")] - public string Query { get; set; } = string.Empty; -} + /// Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. + [JsonPropertyName("supportedCapabilities")] + public required IList SupportedCapabilities { get; set; } -/// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(CatalogSelectionResultSelected), "selected")] -[JsonDerivedType(typeof(CatalogSelectionResultDeclined), "declined")] -[JsonDerivedType(typeof(CatalogSelectionResultCancelled), "cancelled")] -[JsonDerivedType(typeof(CatalogSelectionResultTimedOut), "timed-out")] -[JsonDerivedType(typeof(CatalogSelectionResultInvalid), "invalid")] -[JsonDerivedType(typeof(CatalogSelectionResultStale), "stale")] -[JsonDerivedType(typeof(CatalogSelectionResultReplayed), "replayed")] -[JsonDerivedType(typeof(CatalogSelectionResultForeign), "foreign")] -[JsonDerivedType(typeof(CatalogSelectionResultWrongKind), "wrong-kind")] -[JsonDerivedType(typeof(CatalogSelectionResultNegotiationRefused), "negotiation-refused")] -[JsonDerivedType(typeof(CatalogSelectionResultInvalidRequest), "invalid-request")] -[JsonDerivedType(typeof(CatalogSelectionResultUnavailable), "unavailable")] -public partial class CatalogSelectionResult -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. + [JsonPropertyName("unsupportedCapabilities")] + public required IList UnsupportedCapabilities { get; set; } } - -/// The chosen candidate was transferred into a fresh bounded single-use handle for a later explicit planning request. -/// The selected variant of . +/// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. +/// The invalid-request variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultSelected : CatalogSelectionResult +public partial class McpInstallationManagementResultInvalidRequest : McpInstallationManagementResult { /// [JsonIgnore] - public override string Kind => "selected"; + public override string Kind => "invalid-request"; - /// Fresh single-use candidate handle accepted by mcp.planInstall. Returned only to the native host and never included in model-tool output. - [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(256)] - [JsonPropertyName("candidateHandle")] - public required string CandidateHandle { get; set; } + /// Which request field was rejected. + [JsonPropertyName("field")] + public required CatalogInvalidRequestField Field { get; set; } - /// The exact search identifier privately bound to the selected candidate. + /// Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. [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(64)] - [JsonPropertyName("searchId")] - public required string SearchId { get; set; } + [MaxLength(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// The caller explicitly declined every candidate and the retained search state was released. -/// The declined variant of . +/// One Registry string-valued configuration entry for the selected transport. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultDeclined : CatalogSelectionResult +public sealed class McpInstallationInput { - /// - [JsonIgnore] - public override string Kind => "declined"; + /// Exact category declared by the selected choice. + [JsonPropertyName("category")] + public McpPlanValueCategory Category { get; set; } - /// The search identifier privately bound to the released selection group. - [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(64)] - [JsonPropertyName("searchId")] - public required string SearchId { get; set; } + /// Exact key declared by the selected choice. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + /// Explicit non-secret value. Secret placeholders use a separate input channel. + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; } -/// The caller cancelled the selection interaction and the retained search state was released. -/// The cancelled variant of . +/// A request-local value for one exact reviewed placeholder. Never logged or persisted in a plan. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultCancelled : CatalogSelectionResult +public sealed class McpInstallationSecret { - /// - [JsonIgnore] - public override string Kind => "cancelled"; + /// Exact placeholder from the selected choice, not a caller-chosen backend identifier. + [JsonPropertyName("placeholder")] + public string Placeholder { get; set; } = string.Empty; - /// The search identifier privately bound to the released selection group. - [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(64)] - [JsonPropertyName("searchId")] - public required string SearchId { get; set; } + /// Fresh explicit secret value. It is omitted from confirmation reviews and telemetry. + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; } -/// The host declared that its live selection interaction timed out, and the retained search state was released. -/// The timed-out variant of . +/// Side-effect-free preparation of one original bound remote MCP choice. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultTimedOut : CatalogSelectionResult +internal sealed class McpPrepareInstallRequest { - /// - [JsonIgnore] - public override string Kind => "timed-out"; + /// Exact selected alternative from that plan. + [JsonPropertyName("choiceId")] + public string ChoiceId { get; set; } = string.Empty; - /// The search identifier privately bound to the released selection group. - [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(64)] - [JsonPropertyName("searchId")] - public required string SearchId { get; set; } -} + /// Required bound catalogue and confirmed remote installation capabilities. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } -/// The selection reference was malformed or unknown. -/// The invalid variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultInvalid : CatalogSelectionResult -{ - /// - [JsonIgnore] - public override string Kind => "invalid"; + /// + /// Declared non-secret values. Non-empty only when the caller requires + /// `mcp-configured-remote-installation`; omitted values use the card default. + /// + [JsonPropertyName("inputs")] + public IList Inputs { get => field ??= []; set; } - /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } -} + /// Original single-use bound plan, never a client-authored configuration. + [JsonPropertyName("planHandle")] + public string PlanHandle { get; set; } = string.Empty; -/// The runtime-enforced selection reference lifetime elapsed before the request arrived. -/// The stale variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultStale : CatalogSelectionResult -{ - /// - [JsonIgnore] - public override string Kind => "stale"; + /// An existing local session attached to this connection, not permission to attach one. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; - /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } -} + /// + /// One entry per declared secret placeholder of the selected choice. Non-empty + /// only when the caller requires `mcp-configured-remote-installation`. + /// + [JsonPropertyName("secrets")] + public IList Secrets { get => field ??= []; set; } -/// The selection group was already terminated or its pending host hand-off was already claimed. -/// The replayed variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultReplayed : CatalogSelectionResult -{ - /// - [JsonIgnore] - public override string Kind => "replayed"; + /// The trusted host presents this choice alongside the exact secret placeholders. + [JsonPropertyName("secretStorage")] + public McpInstallationSecretStorage SecretStorage { get; set; } - /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + /// The exact original source, used transiently only after confirmation. + [JsonPropertyName("source")] + public McpServerCardReference Source { get => field ??= new(); set; } } -/// The selection reference belongs to another runtime instance or session. -/// The foreign variant of . +/// An installation result together with the exact honoured contract, or a negotiation refusal. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultForeign : CatalogSelectionResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpInstallationResultOutcome), "outcome")] +[JsonDerivedType(typeof(McpInstallationResultNegotiationRefused), "negotiation-refused")] +[JsonDerivedType(typeof(McpInstallationResultInvalidRequest), "invalid-request")] +public partial class McpInstallationResult { - /// - [JsonIgnore] - public override string Kind => "foreign"; - - /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// The presented opaque handle was issued for another catalog operation. -/// The wrong-kind variant of . + +/// The outcome variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultWrongKind : CatalogSelectionResult +public partial class McpInstallationResultOutcome : McpInstallationResult { /// [JsonIgnore] - public override string Kind => "wrong-kind"; + public override string Kind => "outcome"; - /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Capabilities actually honoured for this request. + [JsonPropertyName("negotiated")] + public required CatalogNegotiatedContract Negotiated { get; set; } + + /// Terminal result of the original operation. + [JsonPropertyName("outcome")] + public required McpInstallationOutcome Outcome { get; set; } } /// The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. -/// The negotiation-refused variant of . +/// The negotiation-refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultNegotiationRefused : CatalogSelectionResult +public partial class McpInstallationResultNegotiationRefused : McpInstallationResult { /// [JsonIgnore] @@ -3588,9 +3925,9 @@ public partial class CatalogSelectionResultNegotiationRefused : CatalogSelection } /// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. -/// The invalid-request variant of . +/// The invalid-request variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultInvalidRequest : CatalogSelectionResult +public partial class McpInstallationResultInvalidRequest : McpInstallationResult { /// [JsonIgnore] @@ -3607,2052 +3944,2377 @@ public partial class CatalogSelectionResultInvalidRequest : CatalogSelectionResu public required string Message { get; set; } } -/// The operation is not available on this runtime. Distinct from a network failure: nothing was attempted. -/// The unavailable variant of . +/// Applies exactly one previously prepared operation on its original connection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class CatalogSelectionResultUnavailable : CatalogSelectionResult +internal sealed class McpApplyInstallRequest { - /// - [JsonIgnore] - public override string Kind => "unavailable"; + /// Capabilities required by the original prepared operation. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } - /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. - [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(1000)] - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Runtime-issued ID already returned by prepareInstall, never reused or rebound. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; - /// Why the operation is unavailable. - [JsonPropertyName("reason")] - public required CatalogUnavailableReason Reason { get; set; } + /// Same existing attached or privately borrowed session as preparation. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. +/// Read-only preparation of one owned removal under fresh selected-session authority. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CatalogSelectionRequest +internal sealed class McpPlanUninstallRequest { - /// Protocol version and capabilities the caller requires. + /// Required authenticated bound installation capabilities. [JsonPropertyName("contract")] public CatalogClientContract Contract { get => field ??= new(); set; } - /// The terminal outcome declared by the caller. Timed-out means the host's live interaction deadline elapsed; a reference whose runtime TTL elapsed is rejected separately as stale. - [JsonPropertyName("outcome")] - public CatalogSelectionDecision Outcome { get; set; } - - /// Opaque runtime-instance scoped reference to one visible candidate. For a non-selected outcome, any candidate reference from the same search closes that search's retained group. - [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(256)] - [JsonPropertyName("selectionRef")] - public string SelectionRef { get; set; } = string.Empty; + /// Exact receipt to inspect, not a server-name guess. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; - /// Locally owned root session whose retained search state is being resolved. - [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(256)] - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Existing selected session on the original connection. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Information about an installed plugin tracked in global state. +/// One-use application of the exact retained removal plan. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstalledPluginInfo +internal sealed class McpApplyUninstallRequest { - /// Opaque, stable hash identifying a direct (non-marketplace) install source. Present only for direct repo / URL / local installs; absent for marketplace plugins. Same source yields the same id; distinct sources never collide. - [JsonPropertyName("directSourceId")] - public string? DirectSourceId { get; set; } - - /// Whether the plugin is currently enabled for new sessions. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } - - /// Whether the managed desired plugin currently has an installed or live record. Set to false for a managed desired entry retained in the listing after installation or reconciliation failed. - [JsonPropertyName("installed")] - public bool? Installed { get; set; } - - /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — a plugin belonging to a directory/local marketplace, which is loaded from its real directory on every pass instead of a copy under the installed-plugins cache. Its presence is what marks a listed plugin as live: such a plugin is always present on disk, so `enabled` is its only meaningful state and it is never "not installed". - [JsonPropertyName("installedFrom")] - public string? InstalledFrom { get; set; } - - /// Whether enterprise managed settings control this plugin's enabled state. - [JsonPropertyName("managed")] - public bool? Managed { get; set; } - - /// The enabled state required by enterprise managed settings, when this plugin spec is managed. - [JsonPropertyName("managedDesiredEnabled")] - public bool? ManagedDesiredEnabled { get; set; } - - /// Marketplace the plugin came from. Empty string ("") for direct repo / URL / local installs. - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; - - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Required authenticated bound installation capabilities. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } - /// Runtime-reported plugin provenance. Currently set to "builtin" only for plugins registered through the trusted host built-in boundary; absent for installed, marketplace, direct, and live plugins. - [JsonPropertyName("source")] - public string? Source { get; set; } + /// Opaque original removal plan, consumed once. + [JsonPropertyName("planHandle")] + public string PlanHandle { get; set; } = string.Empty; - /// Installed version (when reported by the plugin manifest). - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Same existing selected session as removal preparation. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Plugins installed in user/global state. +/// User-configured MCP servers, keyed by server name. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginListResult +public sealed class McpConfigList { - /// Installed plugins. - [JsonPropertyName("plugins")] - public IList Plugins { get => field ??= []; set; } + /// All MCP servers from user config, keyed by name. + [JsonPropertyName("servers")] + public IDictionary Servers { get => field ??= new Dictionary(); set; } } -/// Result of installing a plugin. +/// MCP server name and configuration to add to user configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginInstallResult +internal sealed class McpConfigAddRequest { - /// Set when the install path is deprecated (e.g. direct repo / URL / local installs). Callers should surface this to end users. - [JsonPropertyName("deprecationWarning")] - public string? DeprecationWarning { get; set; } + /// MCP server configuration (stdio process or remote HTTP/SSE). + [JsonPropertyName("config")] + public JsonElement Config { get; set; } - /// The newly installed plugin's metadata. - [JsonPropertyName("plugin")] - public InstalledPluginInfo Plugin { get => field ??= new(); set; } - - /// Optional post-install message provided by the plugin (e.g. setup instructions). - [JsonPropertyName("postInstallMessage")] - public string? PostInstallMessage { get; set; } - - /// Number of skills discovered and installed from the plugin. - [JsonPropertyName("skillsInstalled")] - public long SkillsInstalled { get; set; } - - /// Where the completed plugin tree was staged before atomic promotion. - [JsonPropertyName("stagingMode")] - public PluginInstallStagingMode? StagingMode { get; set; } + /// Unique name for the MCP server. + [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; } -/// Plugin source and optional working directory for relative-path resolution. +/// MCP server name and replacement configuration to write to user configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsInstallRequest +internal sealed class McpConfigUpdateRequest { - /// 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; + /// MCP server configuration (stdio process or remote HTTP/SSE). + [JsonPropertyName("config")] + public JsonElement Config { get; set; } - /// Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } + /// Name of the MCP server to update. + [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; } -/// Name (or spec) of the plugin to uninstall. +/// MCP server name to remove from user configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsUninstallRequest +internal sealed class McpConfigRemoveRequest { - /// 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; } + /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + [JsonPropertyName("authClientIdMetadataUrl")] + public string? AuthClientIdMetadataUrl { get; set; } - /// Plugin name or "plugin@marketplace" spec to uninstall. When ambiguous, prefer the fully-qualified spec. + /// Name of the MCP server to remove. + [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; } -/// Result of updating a single plugin. +/// MCP server names to enable for new sessions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginUpdateResult +internal sealed class McpConfigEnableRequest { - /// Version after the update, when reported by the plugin manifest. - [JsonPropertyName("newVersion")] - public string? NewVersion { get; set; } + /// Names of MCP servers to enable. Each server is removed from the persisted disabled list so new sessions spawn it. Unknown or already-enabled names are ignored. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } +} - /// Version that was previously installed, when available. - [JsonPropertyName("previousVersion")] - public string? PreviousVersion { get; set; } +/// MCP server names to disable for new sessions. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpConfigDisableRequest +{ + /// Names of MCP servers to disable. Each server is added to the persisted disabled list so new sessions skip it. Already-disabled names are ignored. Active sessions keep their current connections until they end. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } +} - /// Number of skills discovered and installed after the update. - [JsonPropertyName("skillsInstalled")] - public long SkillsInstalled { get; set; } +/// New-work inventory or recovery request under an explicitly selected existing session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpInstallationsRequest +{ + /// Required authenticated bound installation contract. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } + + /// Existing selected local session on this connection. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Name (or spec) of the plugin to update. +/// Existing-operation control. A new session selector is deliberately not accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsUpdateRequest +internal sealed class McpInstallationOperationRequest { - /// Plugin name or "plugin@marketplace" spec to update. + /// Original installation wire capability; new-work authentication is not reacquired. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } + + /// Exact runtime-issued operation ID on the original connection. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; +} + +/// Installed plugin that contributes a discovered extension. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DiscoveredExtensionPlugin +{ + /// Installed plugin name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; } -/// Per-plugin result from updating all plugins, with versions, skills installed, success flag, and optional error. +/// Discovered extension metadata and persistent enablement state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginUpdateAllEntry +public sealed class DiscoveredExtension { - /// Error message (failure only). - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Whether this extension's persistent per-ID preference is enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Marketplace the plugin came from. Empty string ("") for direct installs. - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; + /// Source-qualified ID accepted by both server and session extension enablement methods. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Plugin name that was updated. + /// Human-readable extension name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Version after the update, when available. - [JsonPropertyName("newVersion")] - public string? NewVersion { get; set; } - - /// Previously installed version, when available. - [JsonPropertyName("previousVersion")] - public string? PreviousVersion { get; set; } + /// Absolute path to the extension entry module, suitable for revealing it in a file manager. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Number of skills installed after the update (success only). - [JsonPropertyName("skillsInstalled")] - public long? SkillsInstalled { get; set; } + /// Containing plugin metadata for plugin-contributed extensions. + [JsonPropertyName("plugin")] + public DiscoveredExtensionPlugin? Plugin { get; set; } - /// Whether the update succeeded for this plugin. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Discovery source. + [JsonPropertyName("source")] + public DiscoveredExtensionSource Source { get; set; } } -/// Result of updating all installed plugins. +/// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginUpdateAllResult +public sealed class DiscoveredExtensions { - /// Per-plugin update results in deterministic order. - [JsonPropertyName("results")] - public IList Results { get => field ??= []; set; } + /// Discovered user and enabled installed-plugin extensions from persisted Copilot home state. + [JsonPropertyName("extensions")] + public IList Extensions { get => field ??= []; set; } + + /// Effective extension loading mode. Defaults to load_and_augment when unset. + [JsonPropertyName("mode")] + public DiscoveredExtensionMode Mode { get; set; } } -/// Plugin names (or specs) to enable, plus the optional working directory the repository-controlled guard is evaluated against. +/// Source-qualified extension identifiers to persistently enable for future sessions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsEnableRequest +internal sealed class DiscoveredExtensionsEnableRequest { - /// 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; } - - /// Working directory whose repository `enabledPlugins` overlay decides whether this mutation is repository-controlled. Hosts that serve sessions across several repositories (the SDK server) should pass the session's directory; otherwise the guard is evaluated against the server process's own working directory, which may belong to a different repository. Defaults to the server's current working directory. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } + /// Source-qualified user or plugin extension IDs to enable. + [JsonPropertyName("ids")] + public IList Ids { get => field ??= []; set; } } -/// Plugin names (or specs) to disable, plus the optional working directory the repository-controlled guard is evaluated against. +/// Source-qualified extension identifiers to persistently disable for future sessions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsDisableRequest +internal sealed class DiscoveredExtensionsDisableRequest { - /// 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; } - - /// Working directory whose repository `enabledPlugins` overlay decides whether this mutation is repository-controlled. Hosts that serve sessions across several repositories (the SDK server) should pass the session's directory; otherwise the guard is evaluated against the server process's own working directory, which may belong to a different repository. Defaults to the server's current working directory. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } + /// Source-qualified user or plugin extension IDs to disable. + [JsonPropertyName("ids")] + public IList Ids { get => field ??= []; set; } } -/// Trusted built-in plugin directories to use for this runtime process. +/// Skill installation management result with the honoured contract, or a typed refusal. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsBuiltinSetRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SkillInstallationManagementResultOutcome), "outcome")] +[JsonDerivedType(typeof(SkillInstallationManagementResultNegotiationRefused), "negotiation-refused")] +[JsonDerivedType(typeof(SkillInstallationManagementResultInvalidRequest), "invalid-request")] +public partial class SkillInstallationManagementResult { - /// Complete replacement set of trusted built-in plugin directories. Every entry must be an absolute local filesystem path no longer than 4096 characters. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Registered marketplace summary. + +/// Management outcome for verified Skill inventory, planning and removal. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplaceInfo +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeRecoveryRequired), "recovery-required")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeInstallPlanned), "install-planned")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeListed), "listed")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeRecovered), "recovered")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeRolledBack), "rolled-back")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeUninstallPlanned), "uninstall-planned")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeOperation), "operation")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeEnabledChanged), "enabled-changed")] +[JsonDerivedType(typeof(SkillInstallationManagementOutcomeRefused), "refused")] +public partial class SkillInstallationManagementOutcome { - /// Whether the managed marketplace currently resolved into the runtime marketplace registry. Set to false when the desired managed entry is retained for governance visibility after loading or reconciliation failed. - [JsonPropertyName("available")] - public bool? Available { get; set; } - - /// True when this is a default marketplace shipped with the runtime. Defaults are not removable. - [JsonPropertyName("isDefault")] - public bool? IsDefault { get; set; } - - /// Whether enterprise managed settings provide and control this marketplace entry. - [JsonPropertyName("managed")] - public bool? Managed { get; set; } - - /// Marketplace name (matches the @marketplace suffix in plugin specs). - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Human-readable description of where the marketplace data is fetched from (e.g. "GitHub: owner/repo"). - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// All registered marketplaces, including built-in defaults. + +/// Already-confirmed durable work must be reconciled before new mutations or inventory. +/// The recovery-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplaceListResult +public partial class SkillInstallationManagementOutcomeRecoveryRequired : SkillInstallationManagementOutcome { - /// Registered marketplaces. - [JsonPropertyName("marketplaces")] - public IList Marketplaces { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "recovery-required"; } -/// Result of registering a new marketplace. +/// Safe verified Skill review fields. No raw credential, candidate handle or plan handle. +/// Polymorphic base type discriminated by action. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplaceAddResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "action", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SkillInstallationReviewInstall), "install")] +[JsonDerivedType(typeof(SkillInstallationReviewUninstall), "uninstall")] +public partial class SkillInstallationReview { - /// Final name of the marketplace as resolved from its manifest. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("action")] + public virtual string Action { get; set; } = string.Empty; } -/// Marketplace source and optional working directory for relative-path resolution. + +/// One reviewed Skill file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesAddRequest +public sealed class SkillInstallationFileReview { - /// 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; + /// SHA-256 digest of the exact file bytes. + [JsonPropertyName("digest")] + public string Digest { 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; } + /// Whether the file is installed with executable permissions. + [JsonPropertyName("executable")] + public bool Executable { get; set; } + + /// Declared media type for the file. + [JsonPropertyName("mediaType")] + public string MediaType { get; set; } = string.Empty; + + /// Relative file path within the Skill root. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Exact reviewed file size in bytes. + [JsonPropertyName("sizeBytes")] + public long SizeBytes { get; set; } } -/// Outcome of the remove attempt, including dependent-plugin info when applicable. +/// Source identity retained from Agent Finder and the pinned GitHub descriptor. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplaceRemoveResult +public sealed class SkillInstallationSource { - /// Names of installed plugins that prevented removal. Populated only when `removed=false`. - [JsonPropertyName("dependentPlugins")] - public IList? DependentPlugins { get; set; } + /// Digest of the descriptor's bundle manifest. + [JsonPropertyName("bundleDigest")] + public string BundleDigest { get; set; } = string.Empty; - /// True when the marketplace was actually removed. False when removal was skipped because the marketplace has dependent plugins and `force` was not set. - [JsonPropertyName("removed")] - public bool Removed { get; set; } + /// Agent Finder materialisation revision identifier. + [JsonPropertyName("catalogRevisionId")] + public string CatalogRevisionId { get; set; } = string.Empty; + + /// Digest of the canonical materialisation descriptor. + [JsonPropertyName("descriptorDigest")] + public string DescriptorDigest { get; set; } = string.Empty; + + /// Repository full name, for example owner/name. + [JsonPropertyName("repository")] + public string Repository { get; set; } = string.Empty; + + /// GitHub repository database identifier. + [JsonPropertyName("repositoryId")] + public string RepositoryId { get; set; } = string.Empty; + + /// Agent Finder resource identifier. + [JsonPropertyName("resourceId")] + public string ResourceId { get; set; } = string.Empty; + + /// Pinned Git commit revision. + [JsonPropertyName("revision")] + public string Revision { get; set; } = string.Empty; + + /// Root path within the pinned repository. + [JsonPropertyName("root")] + public string Root { get; set; } = string.Empty; } -/// Name of the marketplace to remove and an optional force flag. +/// A user-facing personal Skill installation location without absolute host paths. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesRemoveRequest +public sealed class SkillInstallationLocation { - /// 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; } + /// Diagnostics-only absolute host path. Hosts must not display it by default. + [JsonPropertyName("diagnosticsAbsolutePath")] + public string? DiagnosticsAbsolutePath { get; set; } - /// Marketplace name to remove. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Safe display label, for example ~/.copilot/skills/run-checks. + [JsonPropertyName("displayLabel")] + public string DisplayLabel { get; set; } = string.Empty; + + /// Path relative to the Copilot home. + [JsonPropertyName("relativePath")] + public string RelativePath { get; set; } = string.Empty; + + /// Installation scope. Agent Finder Skills are installed in the user's personal Copilot home. + [JsonPropertyName("scope")] + public SkillInstallationScope Scope { get; set; } } -/// Plugin entry advertised by a marketplace. +/// Review for installing a verified Skill. +/// The install variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplacePluginInfo +public partial class SkillInstallationReviewInstall : SkillInstallationReview { - /// Short description from the marketplace catalog, when present. + /// + [JsonIgnore] + public override string Action => "install"; + + /// Catalogue identity retained from the bound candidate before consent. + [JsonPropertyName("catalogue")] + public required InstallationCatalogueIdentity Catalogue { get; set; } + + /// Skill description from SKILL.md when present. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } - /// Plugin name as listed in the marketplace catalog. + /// + /// Complete verified SKILL.md content. Planning refuses with review-too-large + /// when this exceeds 262144 UTF-8 bytes; it is never truncated. + /// + [JsonPropertyName("entrypointContent")] + public required string EntrypointContent { get; set; } + + /// Relative path of the verified Skill entrypoint. + [JsonPropertyName("entrypointPath")] + public required string EntrypointPath { get; set; } + + /// Reviewed files and digests. + [JsonPropertyName("files")] + public required IList Files { get; set; } + + /// Installing never grants immediate use; the Skill is written disabled. + [JsonPropertyName("installsDisabled")] + public required bool InstallsDisabled { get; set; } + + /// Skill invocation name from SKILL.md. [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + public required string Name { get; set; } + + /// Exact verified source identity. + [JsonPropertyName("source")] + public required SkillInstallationSource Source { get; set; } + + /// Exact user-scope target location without an absolute host path. + [JsonPropertyName("target")] + public required SkillInstallationLocation Target { get; set; } + + /// Total reviewed payload size in bytes. + [JsonPropertyName("totalBytes")] + public required long TotalBytes { get; set; } } -/// Plugins advertised by the marketplace. +/// Durable verified Skill ownership summary. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplaceBrowseResult +public sealed class SkillInstallationSummary { - /// Plugins advertised by the marketplace. - [JsonPropertyName("plugins")] - public IList Plugins { get => field ??= []; set; } + /// Catalogue identity retained at install time. + [JsonPropertyName("catalogue")] + public InstallationCatalogueIdentity Catalogue { get => field ??= new(); set; } + + /// Persisted enablement requested for this installation. + [JsonPropertyName("configuredEnabled")] + public bool ConfiguredEnabled { get; set; } + + /// Exact durable installation receipt identity. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; + + /// ISO 8601 wall-clock installation time. + [JsonPropertyName("installedAt")] + public string InstalledAt { get; set; } = string.Empty; + + /// Skill invocation name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Operation that installed this Skill. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; + + /// Ownership state observed from files and receipts. + [JsonPropertyName("ownershipState")] + public SkillInstallationOwnershipState OwnershipState { get; set; } + + /// Bound-session load observation. + [JsonPropertyName("sessionState")] + public SkillInstallationSessionState SessionState { get; set; } + + /// Exact retained verified source identity. + [JsonPropertyName("source")] + public SkillInstallationSource Source { get => field ??= new(); set; } + + /// User-facing installation location without an absolute host path. + [JsonPropertyName("target")] + public SkillInstallationLocation Target { get => field ??= new(); set; } } -/// Name of the marketplace whose plugin catalog to fetch. +/// Review for uninstalling an owned verified Skill. +/// The uninstall variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesBrowseRequest +public partial class SkillInstallationReviewUninstall : SkillInstallationReview { - /// Marketplace name to browse. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Action => "uninstall"; + + /// Catalogue identity retained at install time. + [JsonPropertyName("catalogue")] + public required InstallationCatalogueIdentity Catalogue { get; set; } + + /// Files recorded by the installation receipt. + [JsonPropertyName("files")] + public required IList Files { get; set; } + + /// Whether current files differ from the receipt. Apply refuses drift. + [JsonPropertyName("filesModified")] + public required bool FilesModified { get; set; } + + /// Owned installation being removed. + [JsonPropertyName("installation")] + public required SkillInstallationSummary Installation { get; set; } + + /// Total receipt-owned payload size in bytes. + [JsonPropertyName("totalBytes")] + public required long TotalBytes { get; set; } } -/// Per-marketplace refresh result, including marketplace name, success flag, and optional failure error. +/// A computed Skill install plan. Nothing has been applied. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplaceRefreshEntry +public sealed class SkillInstallPlan { - /// Error message (failure only). - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Original wall-clock expiry as an ISO 8601 timestamp. + [JsonPropertyName("expiresAt")] + public string ExpiresAt { get; set; } = string.Empty; - /// Marketplace name that was refreshed. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Original operation identifier returned before confirmation. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; - /// Whether the refresh succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// One-use plan handle, bound to the original candidate authority. + [JsonPropertyName("planHandle")] + public string PlanHandle { get; set; } = string.Empty; + + /// Safe review to present before applying the plan. + [JsonPropertyName("review")] + public SkillInstallationReview Review { get => field ??= new(); set; } } -/// Result of refreshing one or more marketplace catalogs. +/// A verified Skill installation plan was prepared. +/// The install-planned variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MarketplaceRefreshResult +public partial class SkillInstallationManagementOutcomeInstallPlanned : SkillInstallationManagementOutcome { - /// Per-marketplace refresh results in deterministic order. - [JsonPropertyName("results")] - public IList Results { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "install-planned"; + + /// Prepared install plan. + [JsonPropertyName("plan")] + public required SkillInstallPlan Plan { get; set; } } -/// RPC data type for PluginsMarketplacesRefresh operations. +/// Owned Skill installations were listed. +/// The listed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesRefreshRequest +public partial class SkillInstallationManagementOutcomeListed : SkillInstallationManagementOutcome { - /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// + [JsonIgnore] + public override string Kind => "listed"; + + /// Owned Skill installation summaries. + [JsonPropertyName("installations")] + public required IList Installations { get; set; } } -/// Server-side skill metadata, including name, description, source, enabled/invocable state, path, project path, and argument hint. +/// Recovery completed and inventory was inspected. +/// The recovered variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerSkill +public partial class SkillInstallationManagementOutcomeRecovered : SkillInstallationManagementOutcome { - /// Optional freeform hint describing the skill's expected arguments, from the `argument-hint` frontmatter field. - [JsonPropertyName("argumentHint")] - public string? ArgumentHint { get; set; } + /// + [JsonIgnore] + public override string Kind => "recovered"; - /// Canonical slash command name used to invoke the skill, without the leading '/'. - [JsonPropertyName("commandName")] - public string? CommandName { get; set; } + /// Owned Skill installation summaries after recovery. + [JsonPropertyName("installations")] + public required IList Installations { get; set; } +} - /// Description of what the skill does. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; +/// Interrupted work was safely compensated and the pending marker was cleared. +/// The rolled-back variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationManagementOutcomeRolledBack : SkillInstallationManagementOutcome +{ + /// + [JsonIgnore] + public override string Kind => "rolled-back"; - /// Whether the skill is currently enabled (based on global config). - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Original operation identity. + [JsonPropertyName("operation_id")] + public required string OperationId { get; set; } - /// Unique identifier for the skill. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Cause of the compensation. + [JsonPropertyName("reason")] + public required SkillInstallationFailureReason Reason { get; set; } +} - /// Absolute path to the skill file. - [JsonPropertyName("path")] - public string? Path { get; set; } +/// A computed Skill uninstall plan. Nothing has been removed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SkillUninstallPlan +{ + /// Original wall-clock expiry as an ISO 8601 timestamp. + [JsonPropertyName("expiresAt")] + public string ExpiresAt { get; set; } = string.Empty; - /// The project path this skill belongs to (only for project/inherited skills). - [JsonPropertyName("projectPath")] - public string? ProjectPath { get; set; } + /// Owned Skill installation being removed. + [JsonPropertyName("installation")] + public SkillInstallationSummary Installation { get => field ??= new(); set; } - /// Source location type (e.g., project, personal-copilot, plugin, builtin). - [JsonPropertyName("source")] - public SkillSource Source { get; set; } + /// Original removal operation identifier returned before confirmation. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; - /// Whether the skill can be invoked by the user as a slash command. - [JsonPropertyName("userInvocable")] - public bool UserInvocable { get; set; } + /// One-use uninstall plan handle. + [JsonPropertyName("planHandle")] + public string PlanHandle { get; set; } = string.Empty; + + /// Safe review to present before applying removal. + [JsonPropertyName("review")] + public SkillInstallationReview Review { get => field ??= new(); set; } } -/// Skills discovered across global and project sources. +/// An owned Skill uninstall plan was prepared. +/// The uninstall-planned variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerSkillList +public partial class SkillInstallationManagementOutcomeUninstallPlanned : SkillInstallationManagementOutcome { - /// Messages for skills that failed to load (e.g. malformed SKILL.md). Empty when host skills are excluded so host-local paths are not disclosed to multitenant callers. - [JsonPropertyName("errors")] - public IList? Errors { get; set; } + /// + [JsonIgnore] + public override string Kind => "uninstall-planned"; - /// All discovered skills across all sources. - [JsonPropertyName("skills")] - public IList Skills { get => field ??= []; set; } + /// Prepared uninstall plan. + [JsonPropertyName("plan")] + public required SkillUninstallPlan Plan { get; set; } } -/// Optional project paths and additional skill directories to include in discovery. +/// Status snapshot from the original connection, independent of new-work account availability. +/// Polymorphic base type discriminated by phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsDiscoverRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "phase", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SkillInstallationOperationStatusPreparing), "preparing")] +[JsonDerivedType(typeof(SkillInstallationOperationStatusPrepared), "prepared")] +[JsonDerivedType(typeof(SkillInstallationOperationStatusAwaitingConfirmation), "awaiting-confirmation")] +[JsonDerivedType(typeof(SkillInstallationOperationStatusRevalidating), "revalidating")] +[JsonDerivedType(typeof(SkillInstallationOperationStatusApplying), "applying")] +[JsonDerivedType(typeof(SkillInstallationOperationStatusCompleted), "completed")] +public partial class SkillInstallationOperationStatus { - /// When true, omit skills from the host's global sources (personal, custom, plugin, and built-in), returning only project-scoped skills. For multitenant deployments. - [JsonPropertyName("excludeHostSkills")] - public bool? ExcludeHostSkills { get; set; } - - /// Optional skill scan paths to exclude from discovery. - [JsonPropertyName("ignoredSkillsLocations")] - public IList? IgnoredSkillsLocations { get; set; } - - /// Optional list of project directory paths to scan for project-scoped skills. - [JsonPropertyName("projectPaths")] - public IList? ProjectPaths { get; set; } - - /// Optional list of additional skill directory paths to include. - [JsonPropertyName("skillDirectories")] - public IList? SkillDirectories { get; set; } + /// The type discriminator. + [JsonPropertyName("phase")] + public virtual string Phase { get; set; } = string.Empty; } -/// Canonical directory where skills can be discovered or created, with scope, preference, and optional project path. + +/// Original operation progress discriminator. +/// The preparing variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillDiscoveryPath +public partial class SkillInstallationOperationStatusPreparing : SkillInstallationOperationStatus { - /// Absolute path of the create/discovery target (may not exist on disk yet). - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Whether this is the canonical directory to create a new skill in its tier. At most one entry per tier is preferred; the `personal-agents` and `custom` scopes are never preferred. - [JsonPropertyName("preferredForCreation")] - public bool PreferredForCreation { get; set; } + /// + [JsonIgnore] + public override string Phase => "preparing"; - /// The input project path this directory was derived from (only for project scope). - [JsonPropertyName("projectPath")] - public string? ProjectPath { get; set; } + /// Whether cancellation has been requested. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// Which tier this directory belongs to. - [JsonPropertyName("scope")] - public SkillDiscoveryScope Scope { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Canonical locations where skills can be created so the runtime will recognize them. +/// Original operation progress discriminator. +/// The prepared variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillDiscoveryPathList +public partial class SkillInstallationOperationStatusPrepared : SkillInstallationOperationStatus { - /// Canonical skill create/discovery directories, in priority order. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Phase => "prepared"; + + /// Whether cancellation has been requested. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } + + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Optional project paths to enumerate. +/// Original operation progress discriminator. +/// The awaiting-confirmation variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsGetDiscoveryPathsRequest +public partial class SkillInstallationOperationStatusAwaitingConfirmation : SkillInstallationOperationStatus { - /// When true, omit the host's personal and custom skill directories, leaving only project directories. For multitenant deployments. - [JsonPropertyName("excludeHostSkills")] - public bool? ExcludeHostSkills { get; set; } + /// + [JsonIgnore] + public override string Phase => "awaiting-confirmation"; - /// Optional skill scan paths to exclude from discovery. - [JsonPropertyName("ignoredSkillsLocations")] - public IList? IgnoredSkillsLocations { get; set; } + /// Whether cancellation has been requested. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// Optional list of project directory paths. When omitted or empty, only personal and custom directories are returned. - [JsonPropertyName("projectPaths")] - public IList? ProjectPaths { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Skill names to mark as disabled in global configuration, replacing any previous list. +/// Original operation progress discriminator. +/// The revalidating variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsConfigSetDisabledSkillsRequest +public partial class SkillInstallationOperationStatusRevalidating : SkillInstallationOperationStatus { - /// List of skill names to disable. - [JsonPropertyName("disabledSkills")] - public IList DisabledSkills { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Phase => "revalidating"; + + /// Whether cancellation has been requested. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } + + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Adds or removes a single skill from the global disabled list, leaving every other entry untouched. +/// Original operation progress discriminator. +/// The applying variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsConfigSetSkillDisabledRequest +public partial class SkillInstallationOperationStatusApplying : SkillInstallationOperationStatus { - /// True to disable the skill, false to enable it. - [JsonPropertyName("disabled")] - public bool Disabled { get; set; } + /// + [JsonIgnore] + public override string Phase => "applying"; - /// Name of the skill to add to or remove from the disabled list. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Whether cancellation has been requested; already-started effects require recovery. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } + + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Agent metadata, including identifiers, display details, source, tools, model, models, reasoning effort, MCP servers, skills, and file path. +/// Terminal verified Skill mutation result. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentInfo +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SkillInstallationOutcomeInstalled), "installed")] +[JsonDerivedType(typeof(SkillInstallationOutcomeUninstalled), "uninstalled")] +[JsonDerivedType(typeof(SkillInstallationOutcomeRolledBack), "rolled-back")] +[JsonDerivedType(typeof(SkillInstallationOutcomeRecoveryRequired), "recovery-required")] +[JsonDerivedType(typeof(SkillInstallationOutcomeDeclined), "declined")] +[JsonDerivedType(typeof(SkillInstallationOutcomeCancelled), "cancelled")] +[JsonDerivedType(typeof(SkillInstallationOutcomeRefused), "refused")] +public partial class SkillInstallationOutcome { - /// Description of the agent's purpose. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// Whether model-driven invocation is disabled for this agent. - [JsonPropertyName("disableModelInvocation")] - public bool? DisableModelInvocation { get; set; } - - /// Human-readable display name. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Stable identifier for selection. For most agents this is the same as `name`; for plugin/builtin agents it may differ. Always populated; defaults to `name` when no distinct id was assigned. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - /// MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. - [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] - [JsonPropertyName("mcpServers")] - public IDictionary? McpServers { get; set; } +/// The verified Skill was installed disabled. +/// The installed variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationOutcomeInstalled : SkillInstallationOutcome +{ + /// + [JsonIgnore] + public override string Kind => "installed"; - /// Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Durable installed Skill summary. + [JsonPropertyName("installation")] + public required SkillInstallationSummary Installation { get; set; } +} - /// Whether authored models are preferences or required constraints. - [JsonPropertyName("modelPolicy")] - public AgentModelPolicy? ModelPolicy { get; set; } +/// The owned Skill was removed. +/// The uninstalled variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationOutcomeUninstalled : SkillInstallationOutcome +{ + /// + [JsonIgnore] + public override string Kind => "uninstalled"; - /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. - [JsonPropertyName("models")] - public IList? Models { get; set; } + /// Removed installation identity. + [JsonPropertyName("installationId")] + public required string InstallationId { get; set; } - /// Name of the agent. Use `id` as the stable selection identifier. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Original removal operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } +} - /// Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. - [JsonPropertyName("path")] - public string? Path { get; set; } +/// The durable transaction was aborted or fully compensated. +/// The rolled-back variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationOutcomeRolledBack : SkillInstallationOutcome +{ + /// + [JsonIgnore] + public override string Kind => "rolled-back"; - /// Authored base prompt for the agent. Runtime prompt assembly may add dynamic context at invocation time. Omitted from `session.agent.list` unless `includePrompt` is true. - [JsonPropertyName("prompt")] - public string? Prompt { get; set; } + /// Original operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } - /// Authored reasoning effort for this agent. Applied on selection to models that support it; omitted means no authored preference. - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + /// Cause of the fully aborted or compensated operation. + [JsonPropertyName("reason")] + public required SkillInstallationFailureReason Reason { get; set; } +} - /// Skill names preloaded into this agent's context. Omitted means none. - [JsonPropertyName("skills")] - public IList? Skills { get; set; } +/// A write may have completed. Recover and inspect durable state before retrying. +/// The recovery-required variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationOutcomeRecoveryRequired : SkillInstallationOutcome +{ + /// + [JsonIgnore] + public override string Kind => "recovery-required"; - /// Where the agent definition was loaded from. - [JsonPropertyName("source")] - public AgentInfoSource? Source { get; set; } + /// Operation whose durable result must be recovered and inspected. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } +} - /// Allowed tool names for this agent. Empty array means none; omitted means inherit defaults. - [JsonPropertyName("tools")] - public IList? Tools { get; set; } +/// The user declined the confirmation request. +/// The declined variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationOutcomeDeclined : SkillInstallationOutcome +{ + /// + [JsonIgnore] + public override string Kind => "declined"; - /// Whether the agent can be selected directly by the user. Agents marked `false` are subagent-only. - [JsonPropertyName("userInvocable")] - public bool? UserInvocable { get; set; } + /// Original operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Agents discovered across user, project, plugin, and remote sources. +/// The operation was cancelled before a terminal mutation. +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerAgentList +public partial class SkillInstallationOutcomeCancelled : SkillInstallationOutcome { - /// All discovered agents across all sources. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "cancelled"; + + /// Original operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } } -/// Optional project paths to include in agent discovery. +/// The operation was refused without applying changes. +/// The refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentsDiscoverRequest +public partial class SkillInstallationOutcomeRefused : SkillInstallationOutcome { - /// When true, omit the host's agents (the user-level agent directory and all plugin agents), leaving only project and remote agents. For multitenant deployments. - [JsonPropertyName("excludeHostAgents")] - public bool? ExcludeHostAgents { get; set; } + /// + [JsonIgnore] + public override string Kind => "refused"; - /// Optional list of project directory paths to scan for project-scoped agents. When omitted or empty, only user/plugin/remote-independent agents are returned (no project scan). - [JsonPropertyName("projectPaths")] - public IList? ProjectPaths { get; set; } + /// Present once an operation has been allocated. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operationId")] + public string? OperationId { get; set; } + + /// Bounded refusal reason. + [JsonPropertyName("reason")] + public required SkillInstallationFailureReason Reason { get; set; } } -/// Canonical directory where custom agents can be discovered or created, with scope, preference, and optional project path. +/// Original operation progress discriminator. +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentDiscoveryPath +public partial class SkillInstallationOperationStatusCompleted : SkillInstallationOperationStatus { - /// Absolute path of the search/create directory (may not exist on disk yet). - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Phase => "completed"; - /// Whether this is the canonical directory to create a new agent in its tier. At most one entry per tier is preferred. - [JsonPropertyName("preferredForCreation")] - public bool PreferredForCreation { get; set; } + /// Whether cancellation was requested before the terminal result. + [JsonPropertyName("cancellationRequested")] + public required bool CancellationRequested { get; set; } - /// The input project path this directory was derived from (only for project scope). - [JsonPropertyName("projectPath")] - public string? ProjectPath { get; set; } + /// Original runtime-issued operation identity. + [JsonPropertyName("operationId")] + public required string OperationId { get; set; } - /// Which tier this directory belongs to. - [JsonPropertyName("scope")] - public AgentDiscoveryPathScope Scope { get; set; } + /// Immutable terminal receipt. + [JsonPropertyName("outcome")] + public required SkillInstallationOutcome Outcome { get; set; } } -/// Canonical locations where custom agents can be created so the runtime will recognize them. +/// Original-connection operation snapshot. +/// The operation variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentDiscoveryPathList +public partial class SkillInstallationManagementOutcomeOperation : SkillInstallationManagementOutcome { - /// Canonical agent create/discovery directories, in priority order. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "operation"; + + /// Operation status. + [JsonPropertyName("operation")] + public required SkillInstallationOperationStatus Operation { get; set; } } -/// Optional project paths to include when enumerating agent discovery directories. +/// Enablement changed and the selected session was reconciled. +/// The enabled-changed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentsGetDiscoveryPathsRequest +public partial class SkillInstallationManagementOutcomeEnabledChanged : SkillInstallationManagementOutcome { - /// When true, omit the host's user-level agent directory, leaving only project directories. For multitenant deployments (mirrors `discover`'s `excludeHostAgents`). - [JsonPropertyName("excludeHostAgents")] - public bool? ExcludeHostAgents { get; set; } + /// + [JsonIgnore] + public override string Kind => "enabled-changed"; - /// Optional list of project directory paths. When omitted or empty, only the user-level directory is returned. - [JsonPropertyName("projectPaths")] - public IList? ProjectPaths { get; set; } + /// Safe reload or reconciliation diagnostics. + [JsonPropertyName("diagnostics")] + public required IList Diagnostics { get; set; } + + /// Updated installation summary. + [JsonPropertyName("installation")] + public required SkillInstallationSummary Installation { get; set; } } -/// Loaded instruction source for a session, including path, content, category, location, applicability, and optional description. +/// The management request was refused. +/// The refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstructionSource +public partial class SkillInstallationManagementOutcomeRefused : SkillInstallationManagementOutcome { - /// Glob pattern(s) from frontmatter — when set, this instruction applies only to matching files. - [JsonPropertyName("applyTo")] - public IList? ApplyTo { get; set; } + /// + [JsonIgnore] + public override string Kind => "refused"; - /// Raw content of the instruction file. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Bounded refusal reason. + [JsonPropertyName("reason")] + public required SkillInstallationFailureReason Reason { get; set; } +} - /// When true, this source starts disabled and must be toggled on by the user. - [JsonPropertyName("defaultDisabled")] - public bool? DefaultDisabled { get; set; } +/// The outcome variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationManagementResultOutcome : SkillInstallationManagementResult +{ + /// + [JsonIgnore] + public override string Kind => "outcome"; - /// Short description (body after frontmatter) for use in instruction tables. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Capabilities honoured for this request. + [JsonPropertyName("negotiated")] + public required CatalogNegotiatedContract Negotiated { get; set; } - /// Unique identifier for this source (used for toggling). - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Observed management outcome. + [JsonPropertyName("outcome")] + public required SkillInstallationManagementOutcome Outcome { get; set; } +} - /// Human-readable label. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; +/// The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. +/// The negotiation-refused variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationManagementResultNegotiationRefused : SkillInstallationManagementResult +{ + /// + [JsonIgnore] + public override string Kind => "negotiation-refused"; - /// Where this source lives — used for UI grouping. - [JsonPropertyName("location")] - public InstructionSourceLocation Location { get; set; } + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } - /// The project path this source was discovered from. Only set by sessionless discovery for repository, working-directory, and project-scoped plugin sources, where it disambiguates sources across multiple workspace roots. The session-scoped getSources leaves it unset. - [JsonPropertyName("projectPath")] - public string? ProjectPath { get; set; } + /// Lowest caller protocol version this runtime will serve. + [JsonPropertyName("minimumSupportedProtocolVersion")] + public required long MinimumSupportedProtocolVersion { get; set; } - /// File path relative to repo or absolute for home. - [JsonPropertyName("sourcePath")] - public string SourcePath { get; set; } = string.Empty; + /// Whether the version or the capability set was the problem. + [JsonPropertyName("reason")] + public required CatalogNegotiationRefusedReason Reason { get; set; } - /// Category of instruction source — used for merge logic. - [JsonPropertyName("type")] - public InstructionSourceType Type { get; set; } + /// Protocol version of the runtime that refused the request. + [JsonPropertyName("runtimeProtocolVersion")] + public required long RuntimeProtocolVersion { get; set; } + + /// Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. + [JsonPropertyName("supportedCapabilities")] + public required IList SupportedCapabilities { get; set; } + + /// The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. + [JsonPropertyName("unsupportedCapabilities")] + public required IList UnsupportedCapabilities { get; set; } } -/// Instruction sources discovered across user, repository, and plugin sources. +/// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. +/// The invalid-request variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerInstructionSourceList +public partial class SkillInstallationManagementResultInvalidRequest : SkillInstallationManagementResult { - /// All discovered instruction sources. - [JsonPropertyName("sources")] - public IList Sources { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "invalid-request"; + + /// Which request field was rejected. + [JsonPropertyName("field")] + public required CatalogInvalidRequestField Field { get; set; } + + /// Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Optional project paths to include in instruction discovery. +/// Side-effect-free planning of one verified Agent Finder Skill candidate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class InstructionsDiscoverRequest +internal sealed class SkillPlanInstallRequest { - /// When true, omit the host's instruction sources (user/home-level files and plugin rules), leaving only repository and working-directory sources. For multitenant deployments. - [JsonPropertyName("excludeHostInstructions")] - public bool? ExcludeHostInstructions { get; set; } + /// Fresh single-use AI skill candidate handle returned by a bound catalogue search. + [JsonPropertyName("candidateHandle")] + public string CandidateHandle { get; set; } = string.Empty; - /// Optional list of project directory paths to scan for repository/working-directory instruction sources. When omitted or empty, only user-level and plugin instruction sources are returned (no project scan). - [JsonPropertyName("projectPaths")] - public IList? ProjectPaths { get; set; } + /// Required authenticated bound catalogue and Skill installation capabilities. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } + + /// Existing local session attached to this connection. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Canonical file or directory where custom instructions can be discovered or created, with location, kind, preference, and project path. +/// Skill installation result with the honoured contract, or a typed request/negotiation refusal. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstructionDiscoveryPath +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SkillInstallationResultOutcome), "outcome")] +[JsonDerivedType(typeof(SkillInstallationResultNegotiationRefused), "negotiation-refused")] +[JsonDerivedType(typeof(SkillInstallationResultInvalidRequest), "invalid-request")] +public partial class SkillInstallationResult { - /// Whether the target is a single file or a directory of instruction files. + /// The type discriminator. [JsonPropertyName("kind")] - public InstructionDiscoveryPathKind Kind { get; set; } + public virtual string Kind { get; set; } = string.Empty; +} - /// Which tier this target belongs to. - [JsonPropertyName("location")] - public InstructionDiscoveryPathLocation Location { get; set; } - /// Absolute path of the file or directory (may not exist on disk yet). - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; +/// The outcome variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SkillInstallationResultOutcome : SkillInstallationResult +{ + /// + [JsonIgnore] + public override string Kind => "outcome"; - /// Whether this is the canonical target to create new instructions in its tier. At most one entry per tier is preferred. - [JsonPropertyName("preferredForCreation")] - public bool PreferredForCreation { get; set; } + /// Capabilities honoured for this request. + [JsonPropertyName("negotiated")] + public required CatalogNegotiatedContract Negotiated { get; set; } - /// The input project path this target was derived from (only for repository targets). - [JsonPropertyName("projectPath")] - public string? ProjectPath { get; set; } + /// Terminal operation outcome. + [JsonPropertyName("outcome")] + public required SkillInstallationOutcome Outcome { get; set; } } -/// Canonical files and directories where custom instructions can be created so the runtime will recognize them. +/// The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. +/// The negotiation-refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstructionDiscoveryPathList +public partial class SkillInstallationResultNegotiationRefused : SkillInstallationResult { - /// Canonical instruction create/discovery files and directories, in priority order. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "negotiation-refused"; + + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// Lowest caller protocol version this runtime will serve. + [JsonPropertyName("minimumSupportedProtocolVersion")] + public required long MinimumSupportedProtocolVersion { get; set; } + + /// Whether the version or the capability set was the problem. + [JsonPropertyName("reason")] + public required CatalogNegotiationRefusedReason Reason { get; set; } + + /// Protocol version of the runtime that refused the request. + [JsonPropertyName("runtimeProtocolVersion")] + public required long RuntimeProtocolVersion { get; set; } + + /// Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. + [JsonPropertyName("supportedCapabilities")] + public required IList SupportedCapabilities { get; set; } + + /// The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. + [JsonPropertyName("unsupportedCapabilities")] + public required IList UnsupportedCapabilities { get; set; } } -/// Optional project paths to include when enumerating instruction discovery targets. +/// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. +/// The invalid-request variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class InstructionsGetDiscoveryPathsRequest +public partial class SkillInstallationResultInvalidRequest : SkillInstallationResult { - /// When true, omit the host's user-level instruction targets, leaving only repository targets. For multitenant deployments (mirrors `discover`'s `excludeHostInstructions`). - [JsonPropertyName("excludeHostInstructions")] - public bool? ExcludeHostInstructions { get; set; } + /// + [JsonIgnore] + public override string Kind => "invalid-request"; - /// Optional list of project directory paths. When omitted or empty, only the user-level targets are returned. - [JsonPropertyName("projectPaths")] - public IList? ProjectPaths { get; set; } + /// Which request field was rejected. + [JsonPropertyName("field")] + public required CatalogInvalidRequestField Field { get; set; } + + /// Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// A literal choice the command input accepts, with a human-facing description. +/// Applies exactly one retained verified Skill installation plan. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandInputChoice +internal sealed class SkillApplyInstallRequest { - /// Human-readable description shown alongside the choice. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Required authenticated bound catalogue and Skill installation capabilities. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } - /// The literal choice value (e.g. 'on', 'off', 'show'). - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Opaque original plan, consumed once. + [JsonPropertyName("planHandle")] + public string PlanHandle { get; set; } = string.Empty; + + /// Same existing selected session as planning. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Optional unstructured input hint. +/// Read-only preparation of one owned Skill removal under fresh selected-session authority. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandInput +internal sealed class SkillPlanUninstallRequest { - /// Optional literal choices the input accepts, each with a human-facing description; clients may render these as selectable options. - [JsonPropertyName("choices")] - public IList? Choices { get; set; } + /// Required authenticated bound installation contract. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } - /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). - [JsonPropertyName("completion")] - public SlashCommandInputCompletion? Completion { get; set; } + /// Exact receipt to inspect. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; - /// Hint to display when command input has not been provided. - [JsonPropertyName("hint")] - public string Hint { get; set; } = string.Empty; + /// Existing selected local session on this connection. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; +} - /// When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace. - [JsonPropertyName("preserveMultilineInput")] - public bool? PreserveMultilineInput { get; set; } +/// One-use application of the exact retained Skill removal plan. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SkillApplyUninstallRequest +{ + /// Required authenticated bound installation contract. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } - /// When true, the command requires non-empty input; clients should render the input hint as required. - [JsonPropertyName("required")] - public bool? Required { get; set; } + /// Opaque original removal plan, consumed once. + [JsonPropertyName("planHandle")] + public string PlanHandle { get; set; } = string.Empty; + + /// Same existing selected session as removal preparation. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Slash-command metadata with name, aliases, description, kind, input hint, execution allowance, and schedulability. +/// Server-side skill metadata, including name, description, source, enabled/invocable state, path, project path, and argument hint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandInfo +public sealed class ServerSkill { - /// Canonical aliases without leading slashes. - [JsonPropertyName("aliases")] - public IList? Aliases { get; set; } + /// Optional freeform hint describing the skill's expected arguments, from the `argument-hint` frontmatter field. + [JsonPropertyName("argumentHint")] + public string? ArgumentHint { get; set; } - /// Whether the command may run while an agent turn is active. - [JsonPropertyName("allowDuringAgentExecution")] - public bool AllowDuringAgentExecution { get; set; } + /// Canonical slash command name used to invoke the skill, without the leading '/'. + [JsonPropertyName("commandName")] + public string? CommandName { get; set; } - /// Human-readable command description. + /// Description of what the skill does. [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; - /// Whether the command is experimental. - [JsonPropertyName("experimental")] - public bool? Experimental { get; set; } - - /// Optional unstructured input hint. - [JsonPropertyName("input")] - public SlashCommandInput? Input { get; set; } - - /// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. - [JsonPropertyName("kind")] - public SlashCommandKind Kind { get; set; } + /// Whether the skill is currently enabled (based on global config). + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Canonical command name without a leading slash. + /// Unique identifier for the skill. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt. - [JsonPropertyName("schedulable")] - public bool? Schedulable { get; set; } + /// Absolute path to the skill file. + [JsonPropertyName("path")] + public string? Path { get; set; } + + /// The project path this skill belongs to (only for project/inherited skills). + [JsonPropertyName("projectPath")] + public string? ProjectPath { 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; } } -/// Slash commands available in the session, after applying any include/exclude filters. +/// Skills discovered across global and project sources. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CommandList +public sealed class ServerSkillList { - /// Commands available in this session. - [JsonPropertyName("commands")] - public IList Commands { get => field ??= []; set; } + /// Messages for skills that failed to load (e.g. malformed SKILL.md). Empty when host skills are excluded so host-local paths are not disclosed to multitenant callers. + [JsonPropertyName("errors")] + public IList? Errors { get; set; } + + /// All discovered skills across all sources. + [JsonPropertyName("skills")] + public IList Skills { get => field ??= []; set; } } -/// A single user setting's effective value alongside its default, so consumers can render settings left at their default. +/// Optional project paths and additional skill directories to include in discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserSettingMetadata +internal sealed class SkillsDiscoverRequest { - /// The centrally-known default for this setting (null when no default is registered). - [JsonPropertyName("default")] - public JsonElement Default { get; set; } + /// When true, omit skills from the host's global sources (personal, custom, plugin, and built-in), returning only project-scoped skills. For multitenant deployments. + [JsonPropertyName("excludeHostSkills")] + public bool? ExcludeHostSkills { get; set; } - /// True when the user has not set an explicit value for this setting (i.e. it is left at its default). Reflects whether the user has overridden the key, not whether the effective value happens to equal the default — a key explicitly set to a value identical to the default still reports false. - [JsonPropertyName("isDefault")] - public bool IsDefault { get; set; } + /// Optional skill scan paths to exclude from discovery. + [JsonPropertyName("ignoredSkillsLocations")] + public IList? IgnoredSkillsLocations { get; set; } - /// The effective value: the user's value if set, otherwise the default. - [JsonPropertyName("value")] - public JsonElement Value { get; set; } + /// Optional list of project directory paths to scan for project-scoped skills. + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } + + /// Optional list of additional skill directory paths to include. + [JsonPropertyName("skillDirectories")] + public IList? SkillDirectories { get; set; } } -/// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. +/// Canonical directory where skills can be discovered or created, with scope, preference, and optional project path. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserSettingsGetResult +public sealed class SkillDiscoveryPath { - /// Every known user setting keyed by setting name, each with its effective value, default, and whether it is at the default. - [JsonPropertyName("settings")] - public IDictionary Settings { get => field ??= new Dictionary(); set; } + /// Absolute path of the create/discovery target (may not exist on disk yet). + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Whether this is the canonical directory to create a new skill in its tier. At most one entry per tier is preferred; the `personal-agents` and `custom` scopes are never preferred. + [JsonPropertyName("preferredForCreation")] + public bool PreferredForCreation { get; set; } + + /// The input project path this directory was derived from (only for project scope). + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } + + /// Which tier this directory belongs to. + [JsonPropertyName("scope")] + public SkillDiscoveryScope Scope { get; set; } } -/// Outcome of writing user settings. +/// Canonical locations where skills can be created so the runtime will recognize them. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserSettingsSetResult +public sealed class SkillDiscoveryPathList { - /// Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. - [JsonPropertyName("shadowedKeys")] - public IList ShadowedKeys { get => field ??= []; set; } + /// Canonical skill create/discovery directories, in priority order. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } } -/// Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. +/// Optional project paths to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UserSettingsSetRequest +internal sealed class SkillsGetDiscoveryPathsRequest { - /// Partial user settings to write, as a free-form object keyed by setting name. - [JsonPropertyName("settings")] - public JsonElement Settings { get; set; } + /// When true, omit the host's personal and custom skill directories, leaving only project directories. For multitenant deployments. + [JsonPropertyName("excludeHostSkills")] + public bool? ExcludeHostSkills { get; set; } + + /// Optional skill scan paths to exclude from discovery. + [JsonPropertyName("ignoredSkillsLocations")] + public IList? IgnoredSkillsLocations { get; set; } + + /// Optional list of project directory paths. When omitted or empty, only personal and custom directories are returned. + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } } -/// Validated device-managed settings discovered before a session exists. +/// Inventory request under an explicitly selected existing session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ManagedSettingsReadResult +internal sealed class SkillInstallationsRequest { - /// Discovery or validation error text when managed settings could not be read safely. - [JsonPropertyName("errorMessage")] - public string? ErrorMessage { get; set; } + /// Required authenticated bound installation contract. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } - /// Validated, canonical managed-settings JSON. Omitted when no managed settings were discovered or when discovered settings failed validation. - [JsonPropertyName("settingsJson")] - public JsonElement? SettingsJson { get; set; } + /// Existing selected local session on this connection. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Indicates whether the calling client was registered as the session filesystem provider. +/// Existing-operation control. A new session selector is deliberately not accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSetProviderResult +internal sealed class SkillInstallationOperationRequest { - /// Whether the provider was set successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Required authenticated bound Skill installation capability. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } -/// Optional capabilities declared by the provider. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSetProviderCapabilities -{ - /// Whether the provider supports SQLite query/exists operations. - [JsonPropertyName("sqlite")] - public bool? Sqlite { get; set; } + /// Exact runtime-issued operation ID on the original connection. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; } -/// 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. +/// Persisted enablement update for one owned Skill installation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionFsSetProviderRequest +internal sealed class SkillSetEnabledRequest { - /// Optional capabilities declared by the provider. - [JsonPropertyName("capabilities")] - public SessionFsSetProviderCapabilities? Capabilities { get; set; } - - /// Path conventions used by this filesystem. - [JsonPropertyName("conventions")] - public SessionFsSetProviderConventions Conventions { get; set; } + /// Required authenticated bound Skill installation capability. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } - /// Absolute initial working directory for sessions. Registering the provider establishes this path as the root of its virtual namespace; the runtime does not require the provider to materialize or stat it before creating a session. - [JsonPropertyName("initialCwd")] - public string InitialCwd { get; set; } = string.Empty; + /// Persisted enablement value. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Path within each session's SessionFs where the runtime stores files for that session. - [JsonPropertyName("sessionStatePath")] - public string SessionStatePath { get; set; } = string.Empty; -} + /// Exact receipt identity to update. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; -/// Indicates whether the calling client was registered as the LLM inference provider. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceSetProviderResult -{ - /// Whether the provider was set successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Existing selected local session to reconcile after persistence. + [JsonPropertyName("policySessionId")] + public string PolicySessionId { get; set; } = string.Empty; } -/// Whether the start frame was accepted. +/// Skill names to mark as disabled in global configuration, replacing any previous list. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpResponseStartResult +internal sealed class SkillsConfigSetDisabledSkillsRequest { - /// True when the response start was matched to a pending request; false when unknown. - [JsonPropertyName("accepted")] - public bool Accepted { get; set; } + /// List of skill names to disable. + [JsonPropertyName("disabledSkills")] + public IList DisabledSkills { get => field ??= []; set; } } -/// Response head. +/// Adds or removes a single skill from the global disabled list, leaving every other entry untouched. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class LlmInferenceHttpResponseStartRequest +internal sealed class SkillsConfigSetSkillDisabledRequest { - /// HTTP response headers, preserving multiple values per name. - [JsonPropertyName("headers")] - public IDictionary> Headers { get => field ??= new Dictionary>(); set; } - - /// Matches the requestId from the originating httpRequestStart frame. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; - - /// HTTP status code. - [JsonPropertyName("status")] - public long Status { get; set; } + /// True to disable the skill, false to enable it. + [JsonPropertyName("disabled")] + public bool Disabled { get; set; } - /// Optional HTTP status reason phrase. - [JsonPropertyName("statusText")] - public string? StatusText { get; set; } + /// Name of the skill to add to or remove from the disabled list. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Whether the chunk was accepted. +/// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpResponseChunkResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(CatalogSearchResultSucceeded), "succeeded")] +[JsonDerivedType(typeof(CatalogSearchResultNegotiationRefused), "negotiation-refused")] +[JsonDerivedType(typeof(CatalogSearchResultUnsupportedKind), "unsupported-kind")] +[JsonDerivedType(typeof(CatalogSearchResultInvalidRequest), "invalid-request")] +[JsonDerivedType(typeof(CatalogSearchResultAuthenticationRequired), "authentication-required")] +[JsonDerivedType(typeof(CatalogSearchResultPolicyRejected), "policy-rejected")] +[JsonDerivedType(typeof(CatalogSearchResultNetworkFailure), "network-failure")] +[JsonDerivedType(typeof(CatalogSearchResultUnsafeRetrieval), "unsafe-retrieval")] +[JsonDerivedType(typeof(CatalogSearchResultMalformedCard), "malformed-card")] +[JsonDerivedType(typeof(CatalogSearchResultContractViolation), "contract-violation")] +[JsonDerivedType(typeof(CatalogSearchResultUnavailable), "unavailable")] +public partial class CatalogSearchResult { - /// True when the chunk was matched to a pending request; false when unknown. - [JsonPropertyName("accepted")] - public bool Accepted { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Set to terminate the response with a transport-level failure. Implies end-of-stream; any further chunks for this requestId are ignored. + +/// One inert catalog result, represented as an MCP server, discovery-only AI skill, or opt-in Agent Plugin variant so kind, media type, provenance, and available operations cannot contradict each other. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpResponseChunkError +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(CatalogCandidateMcpServer), "mcp-server")] +[JsonDerivedType(typeof(CatalogCandidateAiSkill), "ai-skill")] +[JsonDerivedType(typeof(CatalogCandidatePlugin), "plugin")] +public partial class CatalogCandidate { - /// Optional machine-readable error code. - [JsonPropertyName("code")] - public string? Code { get; set; } - - /// Human-readable failure description. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// A response body chunk or terminal error. + +/// Where and when an MCP server catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class LlmInferenceHttpResponseChunkRequest +public sealed class CatalogMcpServerCandidateProvenance { - /// When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text. - [JsonPropertyName("binary")] - public bool? Binary { get; set; } - - /// Body byte range. UTF-8 text when `binary` is absent or false; base64-encoded bytes when `binary` is true. May be empty (e.g. when the response body is empty: send a single chunk with empty data and end=true). - [JsonPropertyName("data")] - public string Data { get; set; } = string.Empty; - - /// When true, this is the final body chunk for the response. The runtime treats the response body as complete after receiving an end-marked chunk. - [JsonPropertyName("end")] - public bool? End { get; set; } + /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + [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("authority")] + public string Authority { get; set; } = string.Empty; - /// Set to terminate the response with a transport-level failure. Implies end-of-stream; any further chunks for this requestId are ignored. - [JsonPropertyName("error")] - public LlmInferenceHttpResponseChunkError? Error { get; set; } + /// JSON MCP media type advertised for the referenced card. + [JsonPropertyName("mediaType")] + public McpServerCardMediaType MediaType { get; set; } - /// Matches the requestId from the originating httpRequestStart frame. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + [JsonPropertyName("observedAt")] + public string ObservedAt { get; set; } = string.Empty; } -/// Pre-resolved working-directory context for session startup. +/// Where a candidate's card came from. Exactly one of a URL or embedded data: the union has no variant carrying both, and no variant carrying neither, so the rule holds structurally rather than by validation. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionContext +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(CatalogCandidateSourceUrl), "url")] +[JsonDerivedType(typeof(CatalogCandidateSourceEmbedded), "embedded")] +public partial class CatalogCandidateSource { - /// Active git branch. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// Most recent working directory for this session. - [JsonPropertyName("cwd")] - public string Cwd { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Git repository root, if the cwd was inside a git repo. - [JsonPropertyName("gitRoot")] - public string? GitRoot { get; set; } - /// Repository host type. - [JsonPropertyName("hostType")] - public SessionContextHostType? HostType { get; set; } +/// Candidate whose card is retrieved from a URL through the runtime's hardened fetch boundary. +/// The url variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class CatalogCandidateSourceUrl : CatalogCandidateSource +{ + /// + [JsonIgnore] + public override string Kind => "url"; - /// Repository slug in `owner/name` form, when known. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// Card URL as advertised. Inert untrusted data: the runtime retrieves it only through its own hardened boundary, and it is never logged. + [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("url")] + public required string Url { get; set; } } -/// GitHub repository the remote session belongs to. +/// Candidate whose card reference arrived inline. The document and its content-derived properties stay behind the runtime boundary. +/// The embedded variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteSessionMetadataRepository +public partial class CatalogCandidateSourceEmbedded : CatalogCandidateSource { - /// Branch associated with the remote session. - [JsonPropertyName("branch")] - public string Branch { get; set; } = string.Empty; - - /// Repository name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Repository owner. - [JsonPropertyName("owner")] - public string Owner { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "embedded"; } -/// Remote session metadata for the session to hand off (typically obtained from `sessions.list` with `source: "remote"`). +/// An inert MCP server catalog result. Every free-text field is untrusted external data and must never be treated as an instruction, and the handle is the only way to refer to the candidate in a later operation. +/// The mcp-server variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteSessionMetadataValue +public partial class CatalogCandidateMcpServer : CatalogCandidate { - /// Most recent working directory context. - [JsonPropertyName("context")] - public SessionContext? Context { get; set; } - - /// Host-supplied human description of what the session is doing right now ("running tests", "waiting for approval"). Optional in the protocol and absent on hosts that do not publish it, so never rely on it -- it enriches `hostStatus`, it does not replace it. - [JsonPropertyName("hostActivity")] - public string? HostActivity { get; set; } - - /// Live status as the owning host reports it in its session listing, so a row for a session running elsewhere can show that it is running. Absent for hosts that publish no such status (the cloud task managers), which read as idle. - [JsonPropertyName("hostStatus")] - public RemoteSessionHostStatus? HostStatus { get; set; } - - /// Always true for remote sessions. - [JsonPropertyName("isRemote")] - public bool IsRemote { get; set; } - - /// Last-modified time as an ISO 8601 timestamp. - [JsonPropertyName("modifiedTime")] - public string ModifiedTime { get; set; } = string.Empty; - - /// Optional human-friendly name set via /rename. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// + [JsonIgnore] + public override string Kind => "mcp-server"; - /// Pull request number associated with the session. - [JsonPropertyName("pullRequestNumber")] - public long? PullRequestNumber { get; set; } + /// Description taken verbatim from the card. Inert untrusted text. + [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(1000)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Backing remote session IDs (most recent first). - [JsonPropertyName("remoteSessionIds")] - public IList RemoteSessionIds { get => field ??= []; set; } + /// Display name taken verbatim from the card. Inert untrusted text. + [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(200)] + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } - /// GitHub repository the remote session belongs to. - [JsonPropertyName("repository")] - public RemoteSessionMetadataRepository Repository { get => field ??= new(); set; } + /// Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. + [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("handle")] + public required string Handle { get; set; } - /// Original remote resource identifier (task ID or PR node ID). - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } + /// ISO 8601 timestamp after which the handle is stale and will be rejected. + [JsonPropertyName("handleExpiresAt")] + public required string HandleExpiresAt { get; set; } - /// Stable session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether this MCP server can be planned for installation, and if policy prevents it. + [JsonPropertyName("installability")] + public required CatalogMcpServerInstallability Installability { get; set; } - /// Deadline (ISO 8601) at which a CLI remote session becomes stale without further heartbeats. - [JsonPropertyName("staleAt")] - public string? StaleAt { get; set; } + /// JSON MCP media type of the underlying card. + [JsonPropertyName("mediaType")] + public required McpServerCardMediaType MediaType { get; set; } - /// Session creation time as an ISO 8601 timestamp. - [JsonPropertyName("startTime")] - public string StartTime { get; set; } = string.Empty; + /// Where the catalog reference was observed, without the card itself or any content digest. + [JsonPropertyName("provenance")] + public required CatalogMcpServerCandidateProvenance Provenance { get; set; } - /// Server-side task state returned by GitHub. - [JsonPropertyName("state")] - public string? State { get; set; } + /// Publisher taken verbatim from the card. Inert untrusted text. + [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(200)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("publisher")] + public string? Publisher { get; set; } - /// Short summary of the session, when one has been derived. - [JsonPropertyName("summary")] - public string? Summary { get; set; } + /// Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. + [JsonPropertyName("source")] + public required CatalogCandidateSource Source { get; set; } - /// Whether the remote task originated from CCA or CLI `--remote`. - [JsonPropertyName("taskType")] - public RemoteSessionMetadataTaskType? TaskType { get; set; } + /// Versioned trust metadata observed from the catalog authority. Optional for protocol-3 compatibility with runtimes that predate trust snapshots. A trust-capable runtime emits an explicit snapshot even when the authority omitted or malformed its trust field. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("trust")] + public CatalogTrustSnapshot? Trust { get; set; } } -/// `sessions.open` handoff progress update with step, status, and optional message. +/// Where and when an AI skill catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsOpenProgress +public sealed class CatalogAiSkillCandidateProvenance { - /// Optional step message. - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + [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("authority")] + public string Authority { get; set; } = string.Empty; - /// Step status. - [JsonPropertyName("status")] - public SessionsOpenProgressStatus Status { get; set; } + /// Media type advertised for the referenced AI skill card. + [JsonPropertyName("mediaType")] + public CatalogAiSkillMediaType MediaType { get; set; } - /// Handoff step. - [JsonPropertyName("step")] - public SessionsOpenProgressStep Step { get; set; } + /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + [JsonPropertyName("observedAt")] + public string ObservedAt { get; set; } = string.Empty; } -/// Result of opening a session. +/// An inert AI skill catalog result. Verified Skill candidates may be installable only when the runtime reports installability and the selected session is permitted to plan installation. +/// The ai-skill variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionOpenResult +public partial class CatalogCandidateAiSkill : CatalogCandidate { - /// Remote session metadata, present when status is `connected`. - [JsonPropertyName("metadata")] - public RemoteSessionMetadataValue? Metadata { get; set; } + /// + [JsonIgnore] + public override string Kind => "ai-skill"; - /// Handoff progress steps, present when status is `handed_off`. - [JsonPropertyName("progress")] - public IList? Progress { get; set; } + /// Description taken verbatim from the card. Inert untrusted text. + [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(1000)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Remote session ID, present when status is `connected`. - [JsonPropertyName("remoteSessionId")] - public string? RemoteSessionId { get; set; } + /// Display name taken verbatim from the card. Inert untrusted text. + [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(200)] + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } - /// Opened session ID. Omitted when status is `not_found`. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + /// Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. + [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("handle")] + public required string Handle { get; set; } - /// Startup prompts queued by user-level hook configs at session creation. Only populated when status is `created`; resumed sessions return an empty array. - [JsonPropertyName("startupPrompts")] - public IList? StartupPrompts { get; set; } + /// ISO 8601 timestamp after which the handle is stale and will be rejected. + [JsonPropertyName("handleExpiresAt")] + public required string HandleExpiresAt { get; set; } - /// Outcome of the open request. - [JsonPropertyName("status")] - public SessionsOpenStatus Status { get; set; } -} + /// Whether this AI skill candidate can be planned for verified installation in the selected session. + [JsonPropertyName("installability")] + public required CatalogAiSkillInstallability Installability { get; set; } -/// Identifier and optional friendly name assigned to the newly forked session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsForkResult -{ - /// Friendly name assigned to the forked session, if any. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Media type of the underlying AI skill card. + [JsonPropertyName("mediaType")] + public required CatalogAiSkillMediaType MediaType { get; set; } - /// The new forked session's ID. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Where the catalog reference was observed, without the card itself or any content digest. + [JsonPropertyName("provenance")] + public required CatalogAiSkillCandidateProvenance Provenance { get; set; } + + /// Publisher taken verbatim from the card. Inert untrusted text. + [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(200)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("publisher")] + public string? Publisher { get; set; } + + /// Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. + [JsonPropertyName("source")] + public required CatalogCandidateSource Source { get; set; } + + /// Versioned trust metadata observed from the catalog authority. Optional for protocol-3 compatibility with runtimes that predate trust snapshots. A trust-capable runtime emits an explicit snapshot even when the authority omitted or malformed its trust field. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("trust")] + public CatalogTrustSnapshot? Trust { get; set; } } -/// Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. +/// Where and when an Agent Plugin catalog reference was observed. Discovery provenance deliberately carries no descriptor URL, raw data, candidate handle, or content digest. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsForkRequest +public sealed class CatalogAgentPluginCandidateProvenance { - /// Optional friendly name to assign to the forked session. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + [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("authority")] + public string Authority { get; set; } = string.Empty; - /// Source session ID to fork from. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Canonical Agent Plugin media type. + [JsonPropertyName("mediaType")] + public CatalogAgentPluginMediaType MediaType { get; set; } - /// Optional event ID boundary. When provided, the fork includes only events before this ID (exclusive). When omitted, all events are included. - [JsonPropertyName("toEventId")] - public string? ToEventId { get; set; } + /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + [JsonPropertyName("observedAt")] + public string ObservedAt { get; set; } = string.Empty; } -/// Repository associated with the connected remote session. +/// Syntactically validated GitHub repository provenance declared by catalog metadata. This is a source claim rather than proof that the descriptor URL resolves to the repository. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectedRemoteSessionMetadataRepository +public sealed class CatalogPluginRepositorySource { - /// Branch associated with the remote session. - [JsonPropertyName("branch")] - public string Branch { get; set; } = string.Empty; - - /// Repository name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Case-preserving safe relative POSIX path derived from metadata.repoPath. + [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(256)] + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Repository owner or organization login. - [JsonPropertyName("owner")] - public string Owner { get; set; } = string.Empty; + /// Canonical lowercase owner/repository name derived from metadata.sourceSet. + [RegularExpression("^[a-z0-9_.-]+/[a-z0-9_.-]+$")] + [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(3)] + [MaxLength(256)] + [JsonPropertyName("repository")] + public string Repository { get; set; } = string.Empty; } -/// Metadata for a connected remote session. +/// An inert Agent Plugin catalog result. Its canonical catalog identity, declared version, repository source claim, and explicit compatibility tags are safe to correlate, while its descriptor, URL, raw data, and installed-plugin state remain runtime-private. This contract-only variant does not mint or expose a candidate handle. +/// The plugin variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectedRemoteSessionMetadata +public partial class CatalogCandidatePlugin : CatalogCandidate { - /// Neutral SDK discriminator for the connected remote session kind. - [JsonPropertyName("kind")] - public ConnectedRemoteSessionMetadataKind Kind { get; set; } - - /// Last session update time as an ISO 8601 string. - [JsonPropertyName("modifiedTime")] - public DateTimeOffset ModifiedTime { get; set; } + /// + [JsonIgnore] + public override string Kind => "plugin"; - /// Optional friendly session name. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Explicit validated compatibility tags, in canonical order. An empty list means the source declared no recognised compatibility; clients must not infer compatibility from other fields. `canvas-only` requires both `canvas` and `github-copilot`. + [JsonPropertyName("compatibilityTags")] + public required IList CompatibilityTags { get; set; } - /// Pull request number associated with the session. - [JsonPropertyName("pullRequestNumber")] - public long? PullRequestNumber { get; set; } + /// Description taken verbatim from the card. Inert untrusted text. + [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(1000)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Repository associated with the connected remote session. - [JsonPropertyName("repository")] - public ConnectedRemoteSessionMetadataRepository Repository { get => field ??= new(); set; } - - /// Original remote resource identifier. - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } + /// Display name taken verbatim from the card. Inert untrusted text. + [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(200)] + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } - /// SDK session ID for the connected remote session. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Validated, normalised catalogue resource URN. This identity comes only from the catalog identifier and is never inferred from display text or installed-plugin state. + [JsonPropertyName("identity")] + public required string Identity { get; set; } - /// Remote session staleness deadline as an ISO 8601 string. - [JsonPropertyName("staleAt")] - public DateTimeOffset? StaleAt { get; set; } + /// Canonical Agent Plugin media type. + [JsonPropertyName("mediaType")] + public required CatalogAgentPluginMediaType MediaType { get; set; } - /// Session start time as an ISO 8601 string. - [JsonPropertyName("startTime")] - public DateTimeOffset StartTime { get; set; } + /// Where the Agent Plugin catalog reference was observed, without its descriptor, URL, raw data, or content digest. + [JsonPropertyName("provenance")] + public required CatalogAgentPluginCandidateProvenance Provenance { get; set; } - /// Remote session state returned by the backing service. - [JsonPropertyName("state")] - public string? State { get; set; } + /// Publisher taken verbatim from the card. Inert untrusted text. + [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(200)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("publisher")] + public string? Publisher { get; set; } - /// Optional session summary. - [JsonPropertyName("summary")] - public string? Summary { get; set; } -} + /// Bounded repository provenance declared through the catalog's sourceSet and repoPath metadata. It contains no descriptor URL. + [JsonPropertyName("source")] + public required CatalogPluginRepositorySource Source { get; set; } -/// Remote session connection result. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteSessionConnectionResult -{ - /// Metadata for a connected remote session. - [JsonPropertyName("metadata")] - public ConnectedRemoteSessionMetadata Metadata { get => field ??= new(); set; } + /// Versioned trust metadata observed from the catalog authority. Optional for protocol-3 compatibility and emitted only when the caller also requires the trust-snapshot capability. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("trust")] + public CatalogTrustSnapshot? Trust { get; set; } - /// SDK session ID for the connected remote session. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Optional version declared by the catalog source. Omitted rather than guessed when the source supplies no version. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Remote session connection parameters. +/// Authority-reported navigation metadata, returned only to callers requiring catalog-search-pagination and only when a supported token is present. Tokenless first-page and continuation responses omit this object; no counts are inferred from candidates. The opaque token may be retained for previous or numbered navigation even when hasNextPage is false. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectRemoteSessionParams +public sealed class CatalogSearchPagination { - /// Session ID to connect to. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// One-based page returned by the authority. + [JsonPropertyName("currentPage")] + public int CurrentPage { get; set; } -/// Local or remote session metadata entry. Narrow on `isRemote` to access source-specific fields. -/// Data type discriminated by isRemote. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionListEntry -{ - /// The boolean discriminator. - [JsonPropertyName("isRemote")] - public bool IsRemote { get; set; } + /// Whether the authority token advertises a valid next target within the navigation window. Not inferred from token presence, truncated, or currentPage being less than pageCount. + [JsonPropertyName("hasNextPage")] + public bool HasNextPage { get; set; } - /// Runtime client name that created/last resumed this session. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// Navigation window ceiling ceil(1000 / pageSize), not the number of existing pages. Legal targets must not exceed this ceiling or the token's signed pageCount. + [JsonPropertyName("maxPage")] + public int MaxPage { get; set; } - /// Pre-resolved working-directory context for session startup. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("context")] - public SessionContext? Context { get; set; } + /// Backend-reported page count, which may exceed maxPage. Present pagination metadata always describes a multi-page result; zero- and single-page responses omit pagination. Navigation targets must also be within the signed pageCount carried by the supplied token. + [JsonPropertyName("pageCount")] + public long PageCount { get; set; } - /// Host-supplied human description of what the session is doing right now ("running tests", "waiting for approval"). Optional in the protocol and absent on hosts that do not publish it, so never rely on it -- it enriches `hostStatus`, it does not replace it. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("hostActivity")] - public string? HostActivity { get; set; } + /// Page size bound to the search, equal to the effective request limit. + [JsonPropertyName("pageSize")] + public int PageSize { get; set; } - /// Live status as the owning host reports it in its session listing, so a row for a session running elsewhere can show that it is running. Absent for hosts that publish no such status (the cloud task managers), which read as idle. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("hostStatus")] - public RemoteSessionHostStatus? HostStatus { get; set; } + /// Opaque pagination token. Session-bound search returns an expiring runtime-owned reference retaining the exact private authority token, original search and authority. Legacy unbound search returns the authority token unchanged, without a runtime-created expiry. Only the runtime unwraps tokens or changes targetPage; SDK consumers must not decode, modify or log them. + [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(4096)] + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; - /// True for detached maintenance sessions that should be hidden from normal resume lists. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("isDetached")] - public bool? IsDetached { get; set; } + /// Backend-reported count for this response, not the number of returned candidates. Its relationship to the full query result set is unknown. + [JsonPropertyName("totalCount")] + public long TotalCount { get; set; } - /// GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("mcTaskId")] - public string? McTaskId { get; set; } + /// The relationship of totalCount to the complete query result set is unknown; neither exactness nor a lower-bound guarantee is implied. + [JsonPropertyName("totalCountRelation")] + public CatalogSearchTotalCountRelation TotalCountRelation { get; set; } +} - /// Last-modified time of the session's persisted state, as ISO 8601. - [JsonPropertyName("modifiedTime")] - public required string ModifiedTime { get; set; } +/// A completed catalog search containing inert candidate summaries. MCP server and AI skill variants carry a single-use handle; the Agent Plugin variant is handleless. +/// The succeeded variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class CatalogSearchResultSucceeded : CatalogSearchResult +{ + /// + [JsonIgnore] + public override string Kind => "succeeded"; - /// Optional human-friendly name set via /rename. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Matching candidates, never more than the requested limit. All text is inert untrusted data. + [JsonPropertyName("candidates")] + public required IList Candidates { get; set; } - /// Pull request number associated with the session. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pullRequestNumber")] - public long? PullRequestNumber { get; set; } + /// Protocol version and capabilities the runtime honoured. + [JsonPropertyName("negotiated")] + public required CatalogNegotiatedContract Negotiated { get; set; } - /// Backing remote session IDs (most recent first). + /// Navigation metadata for callers explicitly requiring catalog-search-pagination. Omitted when the authority returns no token, including tokenless first-page and continuation responses. Counts are never substituted from candidates.length. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("remoteSessionIds")] - public IList? RemoteSessionIds { get; set; } + [JsonPropertyName("pagination")] + public CatalogSearchPagination? Pagination { get; set; } - /// GitHub repository the remote session belongs to. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("repository")] - public RemoteSessionMetadataRepository? Repository { get; set; } + /// Pseudonymous identifier for this search, issued by the runtime or by the catalog authority it queried and never by the caller, so it cannot be forged or replayed to attribute an install to a search that never happened. Always present on a success, so a result set can be tied to the installs it leads to. It identifies a search rather than a person: it is derived from no user, account, device, or query data, and must never be joined with user identity to re-identify anyone. + [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(64)] + [JsonPropertyName("searchId")] + public required string SearchId { get; set; } - /// Original remote resource identifier (task ID or PR node ID). - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } + /// Legacy indication that the authority returned a page token. Preserved for compatibility; this is not a has-next-page indicator. Use pagination.hasNextPage when pagination metadata is present. + [JsonPropertyName("truncated")] + public required bool Truncated { get; set; } +} - /// Stable session identifier. - [JsonPropertyName("sessionId")] - public required string SessionId { get; set; } +/// The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. +/// The negotiation-refused variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class CatalogSearchResultNegotiationRefused : CatalogSearchResult +{ + /// + [JsonIgnore] + public override string Kind => "negotiation-refused"; - /// Deadline (ISO 8601) at which a CLI remote session becomes stale without further heartbeats. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("staleAt")] - public string? StaleAt { get; set; } + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Session creation time as an ISO 8601 timestamp. - [JsonPropertyName("startTime")] - public required string StartTime { get; set; } + /// Lowest caller protocol version this runtime will serve. + [JsonPropertyName("minimumSupportedProtocolVersion")] + public required long MinimumSupportedProtocolVersion { get; set; } - /// Server-side task state returned by GitHub. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("state")] - public string? State { get; set; } + /// Whether the version or the capability set was the problem. + [JsonPropertyName("reason")] + public required CatalogNegotiationRefusedReason Reason { get; set; } - /// Short summary of the session, when one has been derived. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("summary")] - public string? Summary { get; set; } + /// Protocol version of the runtime that refused the request. + [JsonPropertyName("runtimeProtocolVersion")] + public required long RuntimeProtocolVersion { get; set; } - /// Whether the remote task originated from CCA or CLI `--remote`. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("taskType")] - public RemoteSessionMetadataTaskType? TaskType { get; set; } -} + /// Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. + [JsonPropertyName("supportedCapabilities")] + public required IList SupportedCapabilities { get; set; } -/// Sessions matching the filter, ordered most-recently-modified first. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionList -{ - /// Sessions ordered most-recently-modified first. Discriminated by `isRemote`. - [JsonPropertyName("sessions")] - public IList Sessions { get => field ??= []; set; } + /// The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. + [JsonPropertyName("unsupportedCapabilities")] + public required IList UnsupportedCapabilities { get; set; } } -/// Optional filter applied to the returned sessions. +/// The request asked for a candidate kind this runtime does not serve. +/// The unsupported-kind variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionListFilter +public partial class CatalogSearchResultUnsupportedKind : CatalogSearchResult { - /// Match sessions whose context.branch equals this value. - [JsonPropertyName("branch")] - public string? Branch { get; set; } + /// + [JsonIgnore] + public override string Kind => "unsupported-kind"; - /// Match sessions whose context.cwd equals this value. - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Match sessions whose context.gitRoot equals this value. - [JsonPropertyName("gitRoot")] - public string? GitRoot { get; set; } + /// The kinds from the request that are not supported. + [JsonPropertyName("requestedKinds")] + public required IList RequestedKinds { get; set; } - /// Match sessions whose context.repository equals this value. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// Every candidate kind this runtime can serve. + [JsonPropertyName("supportedKinds")] + public required IList SupportedKinds { get; set; } } -/// Optional source filter, metadata-load limit, and context filter applied to the returned sessions. +/// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. +/// The invalid-request variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsListRequest +public partial class CatalogSearchResultInvalidRequest : CatalogSearchResult { - /// Optional filter applied to the returned sessions. - [JsonPropertyName("filter")] - public SessionListFilter? Filter { get; set; } - - /// When true, include detached maintenance sessions. Defaults to false for user-facing session lists. - [JsonPropertyName("includeDetached")] - public bool? IncludeDetached { get; set; } - - /// When provided, only the first N local sessions (sorted by modification time, newest first) load full metadata; remaining sessions return basic info only. Use 0 to return only basic info for every local session. Has no effect on remote entries (which always carry their full shape). - [JsonPropertyName("metadataLimit")] - public long? MetadataLimit { get; set; } + /// + [JsonIgnore] + public override string Kind => "invalid-request"; - /// Which session sources to include. Defaults to `local` for backward compatibility. - [JsonPropertyName("source")] - public SessionSource? Source { get; set; } + /// Which request field was rejected. + [JsonPropertyName("field")] + public required CatalogInvalidRequestField Field { get; set; } - /// Only meaningful when `source` includes remote. When true, propagates errors from the remote service instead of silently returning an empty remote list. Defaults to false. - [JsonPropertyName("throwOnError")] - public bool? ThrowOnError { get; set; } + /// Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Persisted local session metadata, including identifiers, timestamps, summary/name, client, context, detached state, and task ID. +/// An optional catalog authentication exchange did not establish the caller's identity. Anonymous search remains supported; this refusal is reserved for an operation that cannot continue after the attempted exchange. It is distinct from `policy-rejected` and from a network failure, and the reason identifies the recovery action. +/// The authentication-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LocalSessionMetadataValue +public partial class CatalogSearchResultAuthenticationRequired : CatalogSearchResult { - /// Runtime client name that created/last resumed this session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } - - /// Pre-resolved working-directory context for session startup. - [JsonPropertyName("context")] - public SessionContext? Context { get; set; } - - /// True for detached maintenance sessions that should be hidden from normal resume lists. - [JsonPropertyName("isDetached")] - public bool? IsDetached { get; set; } - - /// Always false for local sessions. - [JsonPropertyName("isRemote")] - public bool IsRemote { get; set; } - - /// GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control. - [JsonPropertyName("mcTaskId")] - public string? McTaskId { get; set; } - - /// Last-modified time of the session's persisted state, as ISO 8601. - [JsonPropertyName("modifiedTime")] - public string ModifiedTime { get; set; } = string.Empty; - - /// Optional human-friendly name set via /rename. - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// Stable session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "authentication-required"; - /// Session creation time as an ISO 8601 timestamp. - [JsonPropertyName("startTime")] - public string StartTime { get; set; } = string.Empty; + /// Human-readable explanation, safe to surface. Never contains a credential or token, nor a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Short summary of the session, when one has been derived. - [JsonPropertyName("summary")] - public string? Summary { get; set; } + /// Why authentication failed. Only an expired credential justifies attempting a silent refresh; an absent or rejected credential requires sign-in. + [JsonPropertyName("reason")] + public required CatalogAuthenticationRequiredReason Reason { get; set; } } -/// Persisted local session metadata when the session exists. +/// Registry or enterprise policy refused the operation. +/// The policy-rejected variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetMetadataResult +public partial class CatalogSearchResultPolicyRejected : CatalogSearchResult { - /// Local session metadata, omitted when the session does not exist. - [JsonPropertyName("session")] - public LocalSessionMetadataValue? Session { get; set; } -} + /// + [JsonIgnore] + public override string Kind => "policy-rejected"; -/// Session ID whose persisted metadata should be read. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetMetadataRequest -{ - /// Session ID to inspect. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } -/// Client metadata outcome for one requested local session. -/// Polymorphic base type discriminated by status. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "status", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(SessionsClientMetadataEntryOk), "ok")] -[JsonDerivedType(typeof(SessionsClientMetadataEntryNotFound), "notFound")] -[JsonDerivedType(typeof(SessionsClientMetadataEntryCorrupt), "corrupt")] -[JsonDerivedType(typeof(SessionsClientMetadataEntryUnsupportedVersion), "unsupportedVersion")] -[JsonDerivedType(typeof(SessionsClientMetadataEntryUnavailable), "unavailable")] -public partial class SessionsClientMetadataEntry -{ - /// The type discriminator. - [JsonPropertyName("status")] - public virtual string Status { get; set; } = string.Empty; + /// Which authority produced the decision. + [JsonPropertyName("source")] + public required McpPlanPolicySource Source { get; set; } } - -/// The ok variant of . +/// The runtime could not reach the catalog authority or retrieve a card. Covers being offline as well as transport-level failure. +/// The network-failure variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionsClientMetadataEntryOk : SessionsClientMetadataEntry +public partial class CatalogSearchResultNetworkFailure : CatalogSearchResult { /// [JsonIgnore] - public override string Status => "ok"; + public override string Kind => "network-failure"; - /// Validated client metadata, possibly empty or projected to requested keys. - [JsonPropertyName("metadata")] - public required IDictionary Metadata { get; set; } + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Requested session ID. - [JsonPropertyName("sessionId")] - public required string SessionId { get; set; } + /// Categorised failure, low cardinality so it can be aggregated without carrying a URL. + [JsonPropertyName("reason")] + public required CatalogNetworkFailureReason Reason { get; set; } + + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAfterSeconds")] + public int? RetryAfterSeconds { get; set; } + + /// HTTP status code, when the failure was a rejected response. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("statusCode")] + public int? StatusCode { get; set; } } -/// The notFound variant of . +/// Retrieval was refused by the runtime's hardened fetch boundary before any request left the process, or before a redirect was followed. +/// The unsafe-retrieval variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionsClientMetadataEntryNotFound : SessionsClientMetadataEntry +public partial class CatalogSearchResultUnsafeRetrieval : CatalogSearchResult { /// [JsonIgnore] - public override string Status => "notFound"; + public override string Kind => "unsafe-retrieval"; - /// Requested session ID. - [JsonPropertyName("sessionId")] - public required string SessionId { get; set; } + /// Human-readable explanation, safe to surface. Never contains the refused URL, nor a query, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// Which control refused the retrieval, low cardinality so it can be aggregated without carrying a URL. + [JsonPropertyName("reason")] + public required CatalogUnsafeRetrievalReason Reason { get; set; } } -/// The corrupt variant of . +/// A card could not be parsed or did not satisfy its declared media type's schema. +/// The malformed-card variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionsClientMetadataEntryCorrupt : SessionsClientMetadataEntry +public partial class CatalogSearchResultMalformedCard : CatalogSearchResult { /// [JsonIgnore] - public override string Status => "corrupt"; + public override string Kind => "malformed-card"; - /// Requested session ID. - [JsonPropertyName("sessionId")] - public required string SessionId { get; set; } + /// Media type the card was interpreted as, when it declared one this runtime recognises. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mediaType")] + public CatalogMediaType? MediaType { get; set; } + + /// Human-readable explanation, safe to surface. Never echoes card content, nor a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// How the card failed validation. + [JsonPropertyName("reason")] + public required CatalogMalformedCardReason Reason { get; set; } } -/// The unsupportedVersion variant of . +/// An upstream catalog response broke the wire contract. Most importantly, every result must carry exactly one of a URL or embedded data: a result carrying both, or neither, is refused here rather than being guessed at. +/// The contract-violation variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionsClientMetadataEntryUnsupportedVersion : SessionsClientMetadataEntry +public partial class CatalogSearchResultContractViolation : CatalogSearchResult { /// [JsonIgnore] - public override string Status => "unsupportedVersion"; + public override string Kind => "contract-violation"; - /// Requested session ID. - [JsonPropertyName("sessionId")] - public required string SessionId { get; set; } + /// Human-readable explanation, safe to surface. Never echoes response content, nor a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// Which rule the response broke. + [JsonPropertyName("reason")] + public required CatalogContractViolationReason Reason { get; set; } } -/// The unavailable variant of . +/// The operation is not available on this runtime. Distinct from a network failure: nothing was attempted. +/// The unavailable variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionsClientMetadataEntryUnavailable : SessionsClientMetadataEntry +public partial class CatalogSearchResultUnavailable : CatalogSearchResult { /// [JsonIgnore] - public override string Status => "unavailable"; - - /// Filesystem or provider error code. Clients should not assume every provider uses operating-system error codes. - [JsonPropertyName("code")] - public required string Code { get; set; } + public override string Kind => "unavailable"; - /// Human-readable diagnostic message. Not stable for programmatic matching. + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] [JsonPropertyName("message")] public required string Message { get; set; } - /// Requested session ID. - [JsonPropertyName("sessionId")] - public required string SessionId { get; set; } -} + /// Why the operation is unavailable. + [JsonPropertyName("reason")] + public required CatalogUnavailableReason Reason { get; set; } +} -/// Bounded batch request for client-owned metadata from persisted local sessions. +/// An explicit numbered-page request. SDK consumers treat the token as opaque. For bound search, the runtime unwraps an expiring owner-bound reference to the private authority token; only the runtime changes the authority token's targetPage. Legacy unbound navigation keeps its authority-issued token semantics. No snapshot stability is promised. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetClientMetadataRequest +public sealed class CatalogSearchPage { - /// Case-sensitive keys to project from each valid bag. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. Omit to return every entry. - [JsonPropertyName("keys")] - public IList? Keys { get; set; } + /// Requested one-based page. Must not exceed either the token's signed pageCount or the navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly available pages beyond that signed pageCount. + [JsonPropertyName("number")] + public int Number { get; set; } - /// Session IDs to inspect. Results preserve this order. - [JsonPropertyName("sessionIds")] - public IList SessionIds { get => field ??= []; set; } + /// Opaque pagination token from an earlier response, owner-bound when session-bound search was requested. Never decode, modify or log it in an SDK consumer. Expired or foreign bound references require a fresh bound search, not a legacy retry. + [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(4096)] + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; } -/// Batch of session events returned by a read, with cursor and continuation metadata. +/// 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. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class EventsReadResult +public sealed class CatalogSearchRequest { - /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). - [JsonPropertyName("cursor")] - public string Cursor { get; set; } = string.Empty; - - /// Cursor status: 'ok' means the cursor was applied successfully. For session.eventLog.read, 'expired' means the cursor referred to an event that no longer exists in active history and the read fell back to a boundary of the remaining history: the beginning for a forward read or the newest window for a backward read. That fallback may overlap already rendered events, so active-session consumers should reset, rebase, or deduplicate before continuing. sessions.readPersistedEvents has stricter snapshot semantics: 'expired' returns an empty terminal page and never switches to a replacement journal generation. Other persisted-read I/O failures are RPC errors with diagnostics, not cursor expiry. - [JsonPropertyName("cursorStatus")] - public EventsCursorStatus CursorStatus { get; set; } - - /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. - [JsonPropertyName("events")] - public IList Events { get => field ??= []; set; } + /// Protocol version and capabilities the caller requires. + [JsonPropertyName("contract")] + public required CatalogClientContract Contract { get; set; } - /// True when more events are available in the read's direction. For a backward read, true means older persisted events remain before the returned window. A persisted-event page may contain fewer than `max` events because of its byte budget while still reporting hasMore true; continue according to this flag rather than the event count. - [JsonPropertyName("hasMore")] - public bool HasMore { get; set; } -} + /// Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. + [JsonPropertyName("kinds")] + public IList? Kinds { get; set; } -/// Pagination options for reading an inactive or active local session's persisted event journal. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsReadPersistedEventsRequest -{ - /// Opaque, process-local, single-use cursor returned by the previous persisted-event read. Omit on the first call and issue continuations sequentially; reusing the same cursor returns an expired terminal page. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } + /// Maximum number of candidates to return. Defaults to 10 when omitted. + [JsonPropertyName("limit")] + public int? Limit { get; set; } - /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. This selects the initial read only; a continuation always uses the direction bound into its cursor. - [JsonPropertyName("direction")] - public EventsReadDirection? Direction { get; set; } + /// Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh first-page search. + [JsonPropertyName("page")] + public CatalogSearchPage? Page { get; set; } - /// Maximum number of events to return in this batch (1–1000, default 200). Pages may contain fewer events to keep the serialized event array within a soft 1 MiB budget including resolved binary assets; one oversized event is returned alone to guarantee progress. - [JsonPropertyName("max")] - public long? Max { get; set; } + /// + /// Select an existing attached local session. Requires authenticated, session-bound search. + /// The runtime never creates, resumes or reconfigures a session to honour this selector. + /// + [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(256)] + [JsonPropertyName("policySessionId")] + public string? PolicySessionId { get; set; } - /// Session ID whose persisted event journal should be read. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. + [RegularExpression("\\S")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(256)] + [JsonPropertyName("query")] + public required string Query { get; set; } } -/// Recent local session IDs that contain user-visible history. +/// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsListNonEmptySessionIdsResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(CatalogSelectionResultSelected), "selected")] +[JsonDerivedType(typeof(CatalogSelectionResultDeclined), "declined")] +[JsonDerivedType(typeof(CatalogSelectionResultCancelled), "cancelled")] +[JsonDerivedType(typeof(CatalogSelectionResultTimedOut), "timed-out")] +[JsonDerivedType(typeof(CatalogSelectionResultInvalid), "invalid")] +[JsonDerivedType(typeof(CatalogSelectionResultStale), "stale")] +[JsonDerivedType(typeof(CatalogSelectionResultReplayed), "replayed")] +[JsonDerivedType(typeof(CatalogSelectionResultForeign), "foreign")] +[JsonDerivedType(typeof(CatalogSelectionResultWrongKind), "wrong-kind")] +[JsonDerivedType(typeof(CatalogSelectionResultNegotiationRefused), "negotiation-refused")] +[JsonDerivedType(typeof(CatalogSelectionResultInvalidRequest), "invalid-request")] +[JsonDerivedType(typeof(CatalogSelectionResultUnavailable), "unavailable")] +public partial class CatalogSelectionResult { - /// Session IDs ordered newest-first. - [JsonPropertyName("sessionIds")] - public IList SessionIds { get => field ??= []; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Limit for non-empty local session IDs. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsListNonEmptySessionIdsRequest -{ - /// Maximum number of session IDs to return. - [JsonPropertyName("limit")] - public long? Limit { get; set; } -} -/// ID of the local session bound to the given GitHub task, or omitted when none. +/// The chosen candidate was transferred into a fresh bounded single-use handle for a later explicit planning request. +/// The selected variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsFindByTaskIDResult +public partial class CatalogSelectionResultSelected : CatalogSelectionResult { - /// Omitted when no local session is bound to that GitHub task. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } -} + /// + [JsonIgnore] + public override string Kind => "selected"; -/// GitHub task ID to look up. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsFindByTaskIDRequest -{ - /// GitHub task ID to look up. - [JsonPropertyName("taskId")] - public string TaskId { get; set; } = string.Empty; -} + /// Fresh single-use candidate handle accepted by mcp.planInstall. Returned only to the native host and never included in model-tool output. + [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(256)] + [JsonPropertyName("candidateHandle")] + public required string CandidateHandle { get; set; } -/// Session ID matching the prefix, omitted when no unique match exists. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsFindByPrefixResult -{ - /// Omitted when no unique session matches the prefix (no match or ambiguous). - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + /// The exact search identifier privately bound to the selected candidate. + [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(64)] + [JsonPropertyName("searchId")] + public required string SearchId { get; set; } } -/// UUID prefix to resolve to a unique session ID. +/// The caller explicitly declined every candidate and the retained search state was released. +/// The declined variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsFindByPrefixRequest +public partial class CatalogSelectionResultDeclined : CatalogSelectionResult { - /// UUID prefix (>=7 hex chars, <36 chars). Returns the unique session ID, or undefined when there is no match or the prefix matches multiple sessions. - [JsonPropertyName("prefix")] - public string Prefix { get; set; } = string.Empty; -} + /// + [JsonIgnore] + public override string Kind => "declined"; -/// Most-relevant session ID for the supplied context, or omitted when no sessions exist. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsGetLastForContextResult -{ - /// Most-relevant session ID for the supplied context, or omitted when no sessions exist. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + /// The search identifier privately bound to the released selection group. + [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(64)] + [JsonPropertyName("searchId")] + public required string SearchId { get; set; } } -/// Optional working-directory context used to score session relevance. +/// The caller cancelled the selection interaction and the retained search state was released. +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetLastForContextRequest +public partial class CatalogSelectionResultCancelled : CatalogSelectionResult { - /// Optional working-directory context used to score session relevance. When omitted the most-recently-modified session wins. - [JsonPropertyName("context")] - public SessionContext? Context { get; set; } -} + /// + [JsonIgnore] + public override string Kind => "cancelled"; -/// Absolute path to the session's events.jsonl file on disk. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetEventFilePathResult -{ - /// Absolute path to the session's events.jsonl file. - [JsonPropertyName("filePath")] - public string FilePath { get; set; } = string.Empty; + /// The search identifier privately bound to the released selection group. + [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(64)] + [JsonPropertyName("searchId")] + public required string SearchId { get; set; } } -/// Session ID whose event-log file path to compute. +/// The host declared that its live selection interaction timed out, and the retained search state was released. +/// The timed-out variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetEventFilePathRequest +public partial class CatalogSelectionResultTimedOut : CatalogSelectionResult { - /// Session ID whose event-log file path to compute. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + [JsonIgnore] + public override string Kind => "timed-out"; -/// Map of sessionId -> on-disk size in bytes for each session's workspace directory. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSizes -{ - /// Map of sessionId -> on-disk size in bytes for the session's workspace directory. - [JsonPropertyName("sizes")] - public IDictionary Sizes { get => field ??= new Dictionary(); set; } + /// The search identifier privately bound to the released selection group. + [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(64)] + [JsonPropertyName("searchId")] + public required string SearchId { get; set; } } -/// Session IDs from the input set that are currently in use by another process. +/// The selection reference was malformed or unknown. +/// The invalid variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsCheckInUseResult +public partial class CatalogSelectionResultInvalid : CatalogSelectionResult { - /// Session IDs from the input set that are currently held by another running process via an alive lock file. - [JsonPropertyName("inUse")] - public IList InUse { get => field ??= []; set; } -} + /// + [JsonIgnore] + public override string Kind => "invalid"; -/// Session IDs to test for live in-use locks. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsCheckInUseRequest -{ - /// Session IDs to test for live in-use locks. - [JsonPropertyName("sessionIds")] - public IList SessionIds { get => field ??= []; set; } + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// The session's persisted remote-steerable flag, or omitted when no value has been persisted. +/// The runtime-enforced selection reference lifetime elapsed before the request arrived. +/// The stale variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetPersistedRemoteSteerableResult +public partial class CatalogSelectionResultStale : CatalogSelectionResult { - /// The session's persisted remote-steerable flag if recorded; omitted when no value has been persisted. - [JsonPropertyName("remoteSteerable")] - public bool? RemoteSteerable { get; set; } -} + /// + [JsonIgnore] + public override string Kind => "stale"; -/// Session ID to look up the persisted remote-steerable flag for. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetPersistedRemoteSteerableRequest -{ - /// Session ID to look up the persisted remote-steerable flag for. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Closes a session: emits shutdown, flushes pending events to disk, releases the in-use lock, disposes the active session. Idempotent: succeeds even if the session is not currently active. +/// The selection group was already terminated or its pending host hand-off was already claimed. +/// The replayed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsCloseResult +public partial class CatalogSelectionResultReplayed : CatalogSelectionResult { -} + /// + [JsonIgnore] + public override string Kind => "replayed"; -/// Session ID to close. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsCloseRequest -{ - /// Session ID to close. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Map of sessionId -> bytes freed by removing the session's workspace directory. +/// The selection reference belongs to another runtime instance or session. +/// The foreign variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionBulkDeleteResult +public partial class CatalogSelectionResultForeign : CatalogSelectionResult { - /// Map of sessionId -> bytes freed by removing the session's workspace directory. Sessions whose deletion failed are omitted from this map (failures are logged on the server but not surfaced per-id; check the map for absent IDs to detect them). - [JsonPropertyName("freedBytes")] - public IDictionary FreedBytes { get => field ??= new Dictionary(); set; } -} + /// + [JsonIgnore] + public override string Kind => "foreign"; -/// Session IDs to close, deactivate, and delete from disk. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsBulkDeleteRequest -{ - /// Session IDs to close, deactivate, and delete from disk. - [JsonPropertyName("sessionIds")] - public IList SessionIds { get => field ??= []; set; } + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Session ID to delete from disk. +/// The presented opaque handle was issued for another catalog operation. +/// The wrong-kind variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsDeleteRequest +public partial class CatalogSelectionResultWrongKind : CatalogSelectionResult { - /// Session ID to delete. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "wrong-kind"; - /// Internal resolved session directory path to delete. - [JsonPropertyName("sessionPath")] - public string? SessionPath { get; set; } + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Outcome of the prune operation: deleted IDs, dry-run candidates, skipped IDs, total bytes freed, and the dry-run flag. +/// The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. +/// The negotiation-refused variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionPruneResult +public partial class CatalogSelectionResultNegotiationRefused : CatalogSelectionResult { - /// Session IDs that would be deleted in dry-run mode (always empty otherwise). - [JsonPropertyName("candidates")] - public IList Candidates { get => field ??= []; set; } - - /// Session IDs that were deleted (always empty in dry-run mode). - [JsonPropertyName("deleted")] - public IList Deleted { get => field ??= []; set; } - - /// True when no deletions were actually performed. - [JsonPropertyName("dryRun")] - public bool DryRun { get; set; } + /// + [JsonIgnore] + public override string Kind => "negotiation-refused"; - /// Total bytes freed (actual when not dry-run, projected when dry-run). - [JsonPropertyName("freedBytes")] - public long FreedBytes { get; set; } + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Session IDs that were skipped (e.g., named sessions). - [JsonPropertyName("skipped")] - public IList Skipped { get => field ??= []; set; } -} + /// Lowest caller protocol version this runtime will serve. + [JsonPropertyName("minimumSupportedProtocolVersion")] + public required long MinimumSupportedProtocolVersion { get; set; } -/// Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsPruneOldRequest -{ - /// When true, only report what would be deleted without performing any deletion. - [JsonPropertyName("dryRun")] - public bool? DryRun { get; set; } + /// Whether the version or the capability set was the problem. + [JsonPropertyName("reason")] + public required CatalogNegotiationRefusedReason Reason { get; set; } - /// Session IDs that should never be considered for pruning. - [JsonPropertyName("excludeSessionIds")] - public IList? ExcludeSessionIds { get; set; } + /// Protocol version of the runtime that refused the request. + [JsonPropertyName("runtimeProtocolVersion")] + public required long RuntimeProtocolVersion { get; set; } - /// When true, named sessions (set via /rename) are also eligible for pruning. - [JsonPropertyName("includeNamed")] - public bool? IncludeNamed { get; set; } + /// Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. + [JsonPropertyName("supportedCapabilities")] + public required IList SupportedCapabilities { get; set; } - /// Delete sessions whose modifiedTime is at least this many days old. - [JsonPropertyName("olderThanDays")] - public long OlderThanDays { get; set; } + /// The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. + [JsonPropertyName("unsupportedCapabilities")] + public required IList UnsupportedCapabilities { get; set; } } -/// Flush a session's pending events to disk. No-op when no writer exists for the session (e.g., already closed). +/// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. +/// The invalid-request variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsSaveResult +public partial class CatalogSelectionResultInvalidRequest : CatalogSelectionResult { -} + /// + [JsonIgnore] + public override string Kind => "invalid-request"; -/// Session ID whose pending events should be flushed to disk. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsSaveRequest -{ - /// Session ID whose pending events should be flushed to disk. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Which request field was rejected. + [JsonPropertyName("field")] + public required CatalogInvalidRequestField Field { get; set; } -/// Release the in-use lock held by this process for the given session. No-op when this process does not currently hold a lock for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsReleaseLockResult -{ + /// Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Session ID whose in-use lock should be released. +/// The operation is not available on this runtime. Distinct from a network failure: nothing was attempted. +/// The unavailable variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsReleaseLockRequest +public partial class CatalogSelectionResultUnavailable : CatalogSelectionResult { - /// Session ID whose in-use lock should be released. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + [JsonIgnore] + public override string Kind => "unavailable"; -/// The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionEnrichMetadataResult -{ - /// Enriched records, with summary and context backfilled. Sessions confirmed empty and unnamed may be omitted. - [JsonPropertyName("sessions")] - public IList Sessions { get => field ??= []; set; } -} + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + [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(1000)] + [JsonPropertyName("message")] + public required string Message { get; set; } -/// Session metadata records to enrich with summary and context information. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsEnrichMetadataRequest -{ - /// Session metadata records to enrich. Records that already have summary and context are returned unchanged. - [JsonPropertyName("sessions")] - public IList Sessions { get => field ??= []; set; } + /// Why the operation is unavailable. + [JsonPropertyName("reason")] + public required CatalogUnavailableReason Reason { get; set; } } -/// Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId. +/// Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsReloadPluginHooksResult +internal sealed class CatalogSelectionRequest { -} + /// Protocol version and capabilities the caller requires. + [JsonPropertyName("contract")] + public CatalogClientContract Contract { get => field ??= new(); set; } -/// Active session ID and an optional flag for deferring repo-level hooks until folder trust. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsReloadPluginHooksRequest -{ - /// When true, skip repo-level hooks. Use before folder trust is confirmed; loadDeferredRepoHooks loads them post-trust. - [JsonPropertyName("deferRepoHooks")] - public bool? DeferRepoHooks { get; set; } + /// The terminal outcome declared by the caller. Timed-out means the host's live interaction deadline elapsed; a reference whose runtime TTL elapsed is rejected separately as stale. + [JsonPropertyName("outcome")] + public CatalogSelectionDecision Outcome { get; set; } - /// Active session ID to reload hooks for. + /// Opaque runtime-instance scoped reference to one visible candidate. For a non-selected outcome, any candidate reference from the same search closes that search's retained group. + [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(256)] + [JsonPropertyName("selectionRef")] + public string SelectionRef { get; set; } = string.Empty; + + /// Locally owned root session whose retained search state is being resolved. + [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(256)] [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Queued repo-level startup prompts and the total hook command count after loading. +/// Information about an installed plugin tracked in global state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLoadDeferredRepoHooksResult +public sealed class InstalledPluginInfo { - /// Total hook command count (user + plugin + repo) loaded for the session by this call. Captured atomically with startupPrompts so callers don't need to read a separate counter. - [JsonPropertyName("hookCount")] - public long HookCount { get; set; } + /// Opaque, stable hash identifying a direct (non-marketplace) install source. Present only for direct repo / URL / local installs; absent for marketplace plugins. Same source yields the same id; distinct sources never collide. + [JsonPropertyName("directSourceId")] + public string? DirectSourceId { get; set; } - /// Repo-level startup prompts queued from repo hook configs. Empty on resume, when no repo configs were pending, or when disableAllHooks is set. - [JsonPropertyName("startupPrompts")] - public IList StartupPrompts { get => field ??= []; set; } -} + /// Whether the plugin is currently enabled for new sessions. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } -/// Active session ID whose deferred repo-level hooks should be loaded. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsLoadDeferredRepoHooksRequest -{ - /// Active session ID whose deferred repo-level hooks should be loaded. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} - -/// Replace the manager-wide additional plugins. New session creations and subsequent hook reloads see the new set; already-running sessions keep their existing hook installation until the next reload. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionsSetAdditionalPluginsResult -{ -} - -/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstalledPlugin -{ - /// Path where the plugin is cached locally. - [JsonPropertyName("cache_path")] - public string? CachePath { get; set; } + /// Whether the managed desired plugin currently has an installed or live record. Set to false for a managed desired entry retained in the listing after installation or reconciliation failed. + [JsonPropertyName("installed")] + public bool? Installed { get; set; } - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — a plugin belonging to a directory/local marketplace, which is loaded from its real directory on every pass instead of a copy under the installed-plugins cache. Its presence is what marks a listed plugin as live: such a plugin is always present on disk, so `enabled` is its only meaningful state and it is never "not installed". + [JsonPropertyName("installedFrom")] + public string? InstalledFrom { get; set; } - /// Installation timestamp. - [JsonPropertyName("installed_at")] - public string InstalledAt { get; set; } = string.Empty; + /// Whether enterprise managed settings control this plugin's enabled state. + [JsonPropertyName("managed")] + public bool? Managed { 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; } + /// The enabled state required by enterprise managed settings, when this plugin spec is managed. + [JsonPropertyName("managedDesiredEnabled")] + public bool? ManagedDesiredEnabled { get; set; } - /// Marketplace the plugin came from (empty string for direct repo installs). + /// Marketplace the plugin came from. Empty string ("") for direct repo / URL / local installs. [JsonPropertyName("marketplace")] public string Marketplace { get; set; } = string.Empty; @@ -5660,4057 +6322,4031 @@ public sealed class InstalledPlugin [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Source for direct repo installs (when marketplace is empty). + /// Runtime-reported plugin provenance. Currently set to "builtin" only for plugins registered through the trusted host built-in boundary; absent for installed, marketplace, direct, and live plugins. [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; } + public string? Source { get; set; } - /// Version installed (if available). + /// Installed version (when reported by the plugin manifest). [JsonPropertyName("version")] public string? Version { get; set; } } -/// Manager-wide additional plugins to register; replaces any previously-configured set. +/// Plugins installed in user/global state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsSetAdditionalPluginsRequest +public sealed class PluginListResult { - /// Manager-wide additional plugins to register. Replaces any previously-configured set. Pass an empty array to clear. + /// Installed plugins. [JsonPropertyName("plugins")] - public IList Plugins { get => field ??= []; set; } + public IList Plugins { get => field ??= []; set; } } -/// Dynamic-context board entry count, when available. +/// Result of installing a plugin. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetBoardEntryCountResult +public sealed class PluginInstallResult { - /// Board entry count, when available. - [JsonPropertyName("count")] - public long? Count { get; set; } + /// Set when the install path is deprecated (e.g. direct repo / URL / local installs). Callers should surface this to end users. + [JsonPropertyName("deprecationWarning")] + public string? DeprecationWarning { get; set; } + + /// The newly installed plugin's metadata. + [JsonPropertyName("plugin")] + public InstalledPluginInfo Plugin { get => field ??= new(); set; } + + /// Optional post-install message provided by the plugin (e.g. setup instructions). + [JsonPropertyName("postInstallMessage")] + public string? PostInstallMessage { get; set; } + + /// Number of skills discovered and installed from the plugin. + [JsonPropertyName("skillsInstalled")] + public long SkillsInstalled { get; set; } + + /// Where the completed plugin tree was staged before atomic promotion. + [JsonPropertyName("stagingMode")] + public PluginInstallStagingMode? StagingMode { get; set; } } -/// Session ID whose board entry count should be returned. +/// Plugin source and optional working directory for relative-path resolution. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsGetBoardEntryCountRequest +internal sealed class PluginsInstallRequest { - /// Session ID whose board entry count should be returned. - [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; + + /// 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; } } -/// State of the runtime-managed remote-control singleton. -/// Polymorphic base type discriminated by state. +/// Name (or spec) of the plugin to uninstall. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "state", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(RemoteControlStatusOff), "off")] -[JsonDerivedType(typeof(RemoteControlStatusConnecting), "connecting")] -[JsonDerivedType(typeof(RemoteControlStatusActive), "active")] -[JsonDerivedType(typeof(RemoteControlStatusError), "error")] -public partial class RemoteControlStatus +internal sealed class PluginsUninstallRequest { - /// The type discriminator. - [JsonPropertyName("state")] - public virtual string State { get; set; } = string.Empty; -} + /// Stable source identity for a direct (non-marketplace) install. Disambiguates uninstall when multiple installed plugins share the same name. + [JsonPropertyName("directSourceId")] + public string? DirectSourceId { get; set; } + /// Plugin name or "plugin@marketplace" spec to uninstall. When ambiguous, prefer the fully-qualified spec. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} -/// Remote control is not connected. -/// The off variant of . +/// Result of updating a single plugin. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class RemoteControlStatusOff : RemoteControlStatus +public sealed class PluginUpdateResult { - /// - [JsonIgnore] - public override string State => "off"; + /// Version after the update, when reported by the plugin manifest. + [JsonPropertyName("newVersion")] + public string? NewVersion { get; set; } + + /// Version that was previously installed, when available. + [JsonPropertyName("previousVersion")] + public string? PreviousVersion { get; set; } + + /// Number of skills discovered and installed after the update. + [JsonPropertyName("skillsInstalled")] + public long SkillsInstalled { get; set; } } -/// Remote control is in the middle of initial setup. -/// The connecting variant of . +/// Name (or spec) of the plugin to update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class RemoteControlStatusConnecting : RemoteControlStatus +internal sealed class PluginsUpdateRequest { - /// - [JsonIgnore] - public override string State => "connecting"; - - /// Session id the connection is attaching to. - [JsonPropertyName("attachedSessionId")] - public required string AttachedSessionId { get; set; } + /// Plugin name or "plugin@marketplace" spec to update. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Remote control is connected to a local session. -/// The active variant of . +/// Per-plugin result from updating all plugins, with versions, skills installed, success flag, and optional error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class RemoteControlStatusActive : RemoteControlStatus +public sealed class PluginUpdateAllEntry { - /// - [JsonIgnore] - public override string State => "active"; + /// Error message (failure only). + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Session id remote control is pointed at. - [JsonPropertyName("attachedSessionId")] - public required string AttachedSessionId { get; set; } + /// Marketplace the plugin came from. Empty string ("") for direct installs. + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; - /// True while a read-only/session-sync export is deferred, awaiting the first `user.message` before its MC session exists. Marked internal: this field is excluded from the public SDK surface and is populated only on the CLI in-process path. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonInclude] - [JsonPropertyName("awaitingFirstMessage")] - internal bool? AwaitingFirstMessage { get; set; } + /// Plugin name that was updated. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// MC frontend URL for this session, when known. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("frontendUrl")] - public string? FrontendUrl { get; set; } + /// Version after the update, when available. + [JsonPropertyName("newVersion")] + public string? NewVersion { get; set; } - /// Whether the MC session may steer this session. - [JsonPropertyName("isSteerable")] - public required bool IsSteerable { get; set; } + /// Previously installed version, when available. + [JsonPropertyName("previousVersion")] + public string? PreviousVersion { get; set; } + + /// Number of skills installed after the update (success only). + [JsonPropertyName("skillsInstalled")] + public long? SkillsInstalled { get; set; } + + /// Whether the update succeeded for this plugin. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// The last setup attempt failed. The singleton is otherwise off. -/// The error variant of . +/// Result of updating all installed plugins. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class RemoteControlStatusError : RemoteControlStatus +public sealed class PluginUpdateAllResult { - /// - [JsonIgnore] - public override string State => "error"; - - /// Session id the failing setup attempt targeted, when known. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("attachedSessionId")] - public string? AttachedSessionId { get; set; } - - /// Human-readable error message from the last setup attempt. - [JsonPropertyName("error")] - public required string Error { get; set; } + /// Per-plugin update results in deterministic order. + [JsonPropertyName("results")] + public IList Results { get => field ??= []; set; } } -/// Wrapper for the singleton's current status. +/// Plugin names (or specs) to enable, plus the optional working directory the repository-controlled guard is evaluated against. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteControlStatusResult +internal sealed class PluginsEnableRequest { - /// State of the runtime-managed remote-control singleton. - [JsonPropertyName("status")] - public RemoteControlStatus Status { get => field ??= new(); set; } + /// 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; } + + /// Working directory whose repository `enabledPlugins` overlay decides whether this mutation is repository-controlled. Hosts that serve sessions across several repositories (the SDK server) should pass the session's directory; otherwise the guard is evaluated against the server process's own working directory, which may belong to a different repository. Defaults to the server's current working directory. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// Reattach to an existing MC session without creating a new one. +/// Plugin names (or specs) to disable, plus the optional working directory the repository-controlled guard is evaluated against. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteControlConfigExistingMcSession +internal sealed class PluginsDisableRequest { - /// Existing MC session ID to reattach to. - [JsonPropertyName("mcSessionId")] - public string McSessionId { get; set; } = string.Empty; + /// 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; } - /// Existing MC task ID for the reattached session. - [JsonPropertyName("mcTaskId")] - public string McTaskId { get; set; } = string.Empty; + /// Working directory whose repository `enabledPlugins` overlay decides whether this mutation is repository-controlled. Hosts that serve sessions across several repositories (the SDK server) should pass the session's directory; otherwise the guard is evaluated against the server process's own working directory, which may belong to a different repository. Defaults to the server's current working directory. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// Configuration for the runtime-managed remote-control singleton. +/// Trusted built-in plugin directories to use for this runtime process. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteControlConfig +internal sealed class PluginsBuiltinSetRequest { - /// Reattach to an existing MC session without creating a new one. - [JsonPropertyName("existingMcSession")] - public RemoteControlConfigExistingMcSession? ExistingMcSession { get; set; } + /// Complete replacement set of trusted built-in plugin directories. Every entry must be an absolute local filesystem path no longer than 4096 characters. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } +} - /// Whether the user explicitly requested remote (vs. implicit session-sync). Controls warning surfacing for missing-repo cases. - [JsonPropertyName("explicit")] - public bool Explicit { get; set; } +/// Registered marketplace summary. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MarketplaceInfo +{ + /// Whether the managed marketplace currently resolved into the runtime marketplace registry. Set to false when the desired managed entry is retained for governance visibility after loading or reconciliation failed. + [JsonPropertyName("available")] + public bool? Available { get; set; } - /// Whether remote export should be enabled. - [JsonPropertyName("remote")] - public bool Remote { get; set; } + /// True when this is a default marketplace shipped with the runtime. Defaults are not removable. + [JsonPropertyName("isDefault")] + public bool? IsDefault { get; set; } - /// When true, suppresses timeline messages on successful setup. - [JsonPropertyName("silent")] - public bool Silent { get; set; } + /// Whether enterprise managed settings provide and control this marketplace entry. + [JsonPropertyName("managed")] + public bool? Managed { get; set; } - /// Whether the MC session may steer the local session (write mode). - [JsonPropertyName("steerable")] - public bool Steerable { get; set; } + /// Marketplace name (matches the @marketplace suffix in plugin specs). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Existing Mission Control task ID to attach the exported session to. - [JsonPropertyName("taskId")] - public string? TaskId { get; set; } + /// Human-readable description of where the marketplace data is fetched from (e.g. "GitHub: owner/repo"). + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; } -/// Parameters for attaching the remote-control singleton to a session. +/// All registered marketplaces, including built-in defaults. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsStartRemoteControlRequest +public sealed class MarketplaceListResult { - /// Configuration for the runtime-managed remote-control singleton. - [JsonPropertyName("config")] - public RemoteControlConfig Config { get => field ??= new(); set; } - - /// Local session id to attach remote control to. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Registered marketplaces. + [JsonPropertyName("marketplaces")] + public IList Marketplaces { get => field ??= []; set; } } -/// Outcome of a transferRemoteControl call. +/// Result of registering a new marketplace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteControlTransferResult +public sealed class MarketplaceAddResult { - /// State of the runtime-managed remote-control singleton. - [JsonPropertyName("status")] - public RemoteControlStatus Status { get => field ??= new(); set; } - - /// Whether the rebinding actually happened. - [JsonPropertyName("transferred")] - public bool Transferred { get; set; } + /// Final name of the marketplace as resolved from its manifest. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Parameters for atomically rebinding the remote-control singleton. +/// Marketplace source and optional working directory for relative-path resolution. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsTransferRemoteControlRequest +internal sealed class PluginsMarketplacesAddRequest { - /// When provided, the transfer is rejected unless the singleton currently points at this session id (compare-and-swap semantics to avoid clobbering newer state). - [JsonPropertyName("expectedFromSessionId")] - public string? ExpectedFromSessionId { get; set; } + /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; - /// Local session id to point remote control at. - [JsonPropertyName("toSessionId")] - public string ToSessionId { 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; } } -/// Patch for the singleton's steering state. +/// Outcome of the remove attempt, including dependent-plugin info when applicable. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsSetRemoteControlSteeringRequest +public sealed class MarketplaceRemoveResult { - /// Target steering state. Today only `true` is actionable on the underlying exporter; `false` is reserved for future use. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Names of installed plugins that prevented removal. Populated only when `removed=false`. + [JsonPropertyName("dependentPlugins")] + public IList? DependentPlugins { get; set; } + + /// True when the marketplace was actually removed. False when removal was skipped because the marketplace has dependent plugins and `force` was not set. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// Outcome of a stopRemoteControl call. +/// Name of the marketplace to remove and an optional force flag. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteControlStopResult +internal sealed class PluginsMarketplacesRemoveRequest { - /// State of the runtime-managed remote-control singleton. - [JsonPropertyName("status")] - public RemoteControlStatus Status { get => field ??= new(); set; } + /// When true, also uninstall every plugin sourced from this marketplace. When false (default), removal is a no-op if any plugin from this marketplace is installed and the dependent plugin names are returned in the result. + [JsonPropertyName("force")] + public bool? Force { get; set; } - /// Whether the singleton was actually torn down by this call. - [JsonPropertyName("stopped")] - public bool Stopped { get; set; } + /// Marketplace name to remove. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// RPC data type for SessionsStopRemoteControl operations. +/// Plugin entry advertised by a marketplace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionsStopRemoteControlRequest +public sealed class MarketplacePluginInfo { - /// When provided, the stop is rejected unless the singleton currently points at this session id (compare-and-swap semantics). - [JsonPropertyName("expectedSessionId")] - public string? ExpectedSessionId { get; set; } + /// Short description from the marketplace catalog, when present. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// When true, the singleton is unconditionally torn down regardless of `expectedSessionId`. Use during shutdown or explicit `/remote off`. - [JsonPropertyName("force")] - public bool? Force { get; set; } + /// Plugin name as listed in the marketplace catalog. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Params to attach or detach an in-process ExtensionController delegate. +/// Plugins advertised by the marketplace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConfigureSessionExtensionsParams +public sealed class MarketplaceBrowseResult { - /// Session to attach the extension controller delegate to. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Plugins advertised by the marketplace. + [JsonPropertyName("plugins")] + public IList Plugins { get => field ??= []; set; } } -/// Outcome of an agentRegistry.spawn call. -/// Polymorphic base type discriminated by kind. +/// Name of the marketplace whose plugin catalog to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AgentRegistrySpawnResultSpawned), "spawned")] -[JsonDerivedType(typeof(AgentRegistrySpawnResultSpawnError), "spawn-error")] -[JsonDerivedType(typeof(AgentRegistrySpawnResultRegistryTimeout), "registry-timeout")] -[JsonDerivedType(typeof(AgentRegistrySpawnResultValidationError), "validation-error")] -public partial class AgentRegistrySpawnResult +internal sealed class PluginsMarketplacesBrowseRequest { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// Marketplace name to browse. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } - -/// Full registry entry for the spawned child. Lets the controller call `handleLiveTargetSelected(entry)` directly without re-reading the registry (avoids a TOCTOU window). +/// Per-marketplace refresh result, including marketplace name, success flag, and optional failure error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentRegistryLiveTargetEntry +public sealed class MarketplaceRefreshEntry { - /// Kind of attention required when status === "attention". Meaningful only when status === "attention". - [JsonPropertyName("attentionKind")] - public AgentRegistryLiveTargetEntryAttentionKind? AttentionKind { get; set; } + /// Error message (failure only). + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Git branch of the session (when known). - [JsonPropertyName("branch")] - public string? Branch { get; set; } + /// Marketplace name that was refreshed. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Copilot CLI version that wrote the entry. - [JsonPropertyName("copilotVersion")] - public string CopilotVersion { get; set; } = string.Empty; + /// Whether the refresh succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Working directory of the session (when known). - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } +/// Result of refreshing one or more marketplace catalogs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MarketplaceRefreshResult +{ + /// Per-marketplace refresh results in deterministic order. + [JsonPropertyName("results")] + public IList Results { get => field ??= []; set; } +} - /// Bind host for the entry's JSON-RPC server. - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; +/// RPC data type for PluginsMarketplacesRefresh operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PluginsMarketplacesRefreshRequest +{ + /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. + [JsonPropertyName("name")] + public string? Name { get; set; } +} - /// Process kind tag for the registry entry. - [JsonPropertyName("kind")] - public AgentRegistryLiveTargetEntryKind Kind { get; set; } +/// Agent metadata, including identifiers, display details, source, tools, model, models, reasoning effort, MCP servers, skills, and file path. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AgentInfo +{ + /// Description of the agent's purpose. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Wall-clock milliseconds since the watcher last observed this entry (heartbeat freshness). - [JsonPropertyName("lastSeenMs")] - public long LastSeenMs { get; set; } + /// Whether model-driven invocation is disabled for this agent. + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } - /// How the most recent turn ended (clean vs aborted). Lets the renderer distinguish done from done_cancelled. - [JsonPropertyName("lastTerminalEvent")] - public AgentRegistryLiveTargetEntryLastTerminalEvent? LastTerminalEvent { get; set; } + /// Human-readable display name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Model identifier currently selected for the session. + /// Stable identifier for selection. For most agents this is the same as `name`; for plugin/builtin agents it may differ. Always populated; defaults to `name` when no distinct id was assigned. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonPropertyName("mcpServers")] + public IDictionary? McpServers { get; set; } + + /// Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. [JsonPropertyName("model")] public string? Model { get; set; } - /// Operating-system pid of the process owning this entry. - [JsonPropertyName("pid")] - public long Pid { get; set; } - - /// TCP port the entry's JSON-RPC server is listening on. - [JsonPropertyName("port")] - public long Port { get; set; } + /// Whether authored models are preferences or required constraints. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } - /// Registry entry schema version (1 = ui-server, 2 = managed-server). - [JsonPropertyName("schemaVersion")] - public long SchemaVersion { get; set; } + /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + [JsonPropertyName("models")] + public IList? Models { get; set; } - /// Session ID of the foreground session for this entry. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + /// Name of the agent. Use `id` as the stable selection identifier. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Friendly session name (when set). - [JsonPropertyName("sessionName")] - public string? SessionName { get; set; } + /// Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. + [JsonPropertyName("path")] + public string? Path { get; set; } - /// ISO 8601 timestamp captured at registration. - [JsonPropertyName("startedAt")] - public string StartedAt { get; set; } = string.Empty; + /// Authored base prompt for the agent. Runtime prompt assembly may add dynamic context at invocation time. Omitted from `session.agent.list` unless `includePrompt` is true. + [JsonPropertyName("prompt")] + public string? Prompt { get; set; } - /// Coarse lifecycle status of the foreground session. - [JsonPropertyName("status")] - public AgentRegistryLiveTargetEntryStatus? Status { get; set; } + /// Authored reasoning effort for this agent. Applied on selection to models that support it; omitted means no authored preference. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } - /// Monotonic per-publisher revision counter incremented on every status update. Lets watchers detect transient flips. - [JsonPropertyName("statusRevision")] - public long? StatusRevision { get; set; } + /// Skill names preloaded into this agent's context. Omitted means none. + [JsonPropertyName("skills")] + public IList? Skills { get; set; } - /// Connection token (null when the target is unauthenticated). - [JsonInclude] - [JsonPropertyName("token")] - internal string? Token { get; set; } + /// Where the agent definition was loaded from. + [JsonPropertyName("source")] + public AgentInfoSource? Source { get; set; } + + /// Allowed tool names for this agent. Empty array means none; omitted means inherit defaults. + [JsonPropertyName("tools")] + public IList? Tools { get; set; } + + /// Whether the agent can be selected directly by the user. Agents marked `false` are subagent-only. + [JsonPropertyName("userInvocable")] + public bool? UserInvocable { get; set; } } -/// Canonical process-log discovery outcome; populated from spawnLiveTarget. +/// Agents discovered across user, project, plugin, and remote sources. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentRegistryLogCapture +public sealed class ServerAgentList { - /// Whether a canonical process log was discovered for this managed spawn. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } - - /// Why no canonical process log could be opened for this managed spawn (set only when enabled is false). - [JsonPropertyName("openError")] - public string? OpenError { get; set; } + /// All discovered agents across all sources. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } +} - /// Categorized reason no canonical process log could be opened (set only when enabled is false). - [JsonPropertyName("openErrorReason")] - public AgentRegistryLogCaptureOpenErrorReason? OpenErrorReason { get; set; } +/// Optional project paths to include in agent discovery. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsDiscoverRequest +{ + /// When true, omit the host's agents (the user-level agent directory and all plugin agents), leaving only project and remote agents. For multitenant deployments. + [JsonPropertyName("excludeHostAgents")] + public bool? ExcludeHostAgents { get; set; } - /// Absolute path to the managed spawn's process-<timestamp>-<pid>.log file (only set when enabled). - [JsonPropertyName("path")] - public string? Path { get; set; } + /// Optional list of project directory paths to scan for project-scoped agents. When omitted or empty, only user/plugin/remote-independent agents are returned (no project scan). + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } } -/// Managed-server child was spawned and registered successfully. -/// The spawned variant of . +/// Canonical directory where custom agents can be discovered or created, with scope, preference, and optional project path. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AgentRegistrySpawnResultSpawned : AgentRegistrySpawnResult +public sealed class AgentDiscoveryPath { - /// - [JsonIgnore] - public override string Kind => "spawned"; - - /// Full registry entry for the spawned child. Lets the controller call `handleLiveTargetSelected(entry)` directly without re-reading the registry (avoids a TOCTOU window). - [JsonPropertyName("entry")] - public required AgentRegistryLiveTargetEntry Entry { get; set; } + /// Absolute path of the search/create directory (may not exist on disk yet). + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// If the delegate attempted to send the initial prompt and failed, the categorized error message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("initialPromptError")] - public string? InitialPromptError { get; set; } + /// Whether this is the canonical directory to create a new agent in its tier. At most one entry per tier is preferred. + [JsonPropertyName("preferredForCreation")] + public bool PreferredForCreation { get; set; } - /// Whether the delegate already sent the initial prompt. Always omitted in the current wiring: the controller sends the prompt post-attach via the standard LocalRpcSession.send path. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("initialPromptSent")] - public bool? InitialPromptSent { get; set; } + /// The input project path this directory was derived from (only for project scope). + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } - /// Per-spawn log-capture outcome; populated from spawnLiveTarget. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("logCapture")] - public AgentRegistryLogCapture? LogCapture { get; set; } + /// Which tier this directory belongs to. + [JsonPropertyName("scope")] + public AgentDiscoveryPathScope Scope { get; set; } } -/// `child_process.spawn` itself failed before the child entered the registry. -/// The spawn-error variant of . +/// Canonical locations where custom agents can be created so the runtime will recognize them. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AgentRegistrySpawnResultSpawnError : AgentRegistrySpawnResult +public sealed class AgentDiscoveryPathList { - /// - [JsonIgnore] - public override string Kind => "spawn-error"; - - /// Underlying errno code (e.g. ENOENT, EACCES) when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("code")] - public string? Code { get; set; } - - /// Human-readable error message. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Canonical agent create/discovery directories, in priority order. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } } -/// Spawn succeeded but the child did not publish a matching managed-server entry within the timeout. -/// The registry-timeout variant of . +/// Optional project paths to include when enumerating agent discovery directories. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AgentRegistrySpawnResultRegistryTimeout : AgentRegistrySpawnResult +internal sealed class AgentsGetDiscoveryPathsRequest { - /// - [JsonIgnore] - public override string Kind => "registry-timeout"; - - /// Process ID of the orphaned child (so the caller can offer 'kill the pid' guidance). - [JsonPropertyName("childPid")] - public required long ChildPid { get; set; } + /// When true, omit the host's user-level agent directory, leaving only project directories. For multitenant deployments (mirrors `discover`'s `excludeHostAgents`). + [JsonPropertyName("excludeHostAgents")] + public bool? ExcludeHostAgents { get; set; } - /// Per-spawn log-capture outcome; populated from spawnLiveTarget. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("logCapture")] - public AgentRegistryLogCapture? LogCapture { get; set; } + /// Optional list of project directory paths. When omitted or empty, only the user-level directory is returned. + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } } -/// Synchronous pre-validation rejected the spawn request. -/// The validation-error variant of . +/// The agents this runtime ships, named so a consumer can tell them apart from authored ones. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AgentRegistrySpawnResultValidationError : AgentRegistrySpawnResult +internal sealed class AgentsGetBuiltinsResult { - /// - [JsonIgnore] - public override string Kind => "validation-error"; + /// The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. + [JsonPropertyName("disableableNames")] + public IList DisableableNames { get => field ??= []; set; } - /// Which parameter field was invalid. Omitted when the rejection is not field-specific. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("field")] - public AgentRegistrySpawnValidationErrorField? Field { get; set; } - - /// Human-readable explanation; safe to surface in the UI banner. Never logged to unrestricted telemetry. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Every agent name this runtime ships. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } - /// Categorized reason for the rejection. Low-cardinality enum so telemetry can aggregate by reason without leaking raw paths or agent/model names. - [JsonPropertyName("reason")] - public required AgentRegistrySpawnValidationErrorReason Reason { get; set; } + /// The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. + [JsonPropertyName("yamlBasedNames")] + public IList YamlBasedNames { get => field ??= []; set; } } -/// Inputs to spawn a managed-server child via the controller's spawn delegate. +/// A shipped agent, named and described. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentRegistrySpawnRequest +internal sealed class BuiltinAgentSummary { - /// Custom or built-in agent name (e.g. 'explore'). When omitted, the child uses its own default. - [JsonPropertyName("agentName")] - public string? AgentName { get; set; } + /// One-line description of what the agent does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Working directory for the spawned child (must be an existing directory). - [JsonPropertyName("cwd")] - public string Cwd { get; set; } = string.Empty; + /// The agent name, as it appears in `getBuiltins`. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} - /// Optional first user message. Forwarded to the caller (the CLI's spawn wrapper sends it post-attach via the standard LocalRpcSession.send path). - [JsonPropertyName("initialPrompt")] - public string? InitialPrompt { get; set; } +/// The shipped agents available under the requested flags. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetAvailableBuiltinsResult +{ + /// Available shipped agents, in the runtime's own order. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } +} - /// Model identifier to apply to the new session. - [JsonPropertyName("model")] - public string? Model { get; set; } +/// The feature flags to evaluate shipped agents against. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetAvailableBuiltinsRequest +{ + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + [JsonPropertyName("context")] + public string? Context { get; set; } - /// Friendly session name. Must satisfy validateSessionName: non-empty, no leading/trailing whitespace, <=100 chars, no control chars, no double quotes. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + [JsonPropertyName("featureFlags")] + public IDictionary? FeatureFlags { get; set; } - /// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. - [JsonPropertyName("permissionMode")] - public AgentRegistrySpawnPermissionMode? PermissionMode { get; set; } + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + [JsonPropertyName("overrides")] + public IDictionary? Overrides { get; set; } } -/// Identifies the target session. +/// One shipped agent's definition. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSuspendRequest +internal sealed class AgentsGetBuiltinDefinitionResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. + [JsonPropertyName("definitionJson")] + public string DefinitionJson { get; set; } = string.Empty; } -/// Result of sending a user message. +/// The shipped agent whose definition to load. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SendResult +internal sealed class AgentsGetBuiltinDefinitionRequest { - /// Unique identifier assigned to the message. - [JsonPropertyName("messageId")] - public string MessageId { get; set; } = string.Empty; + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Provider-native structured output format. JSON Schema is forwarded without rewriting or validating the schema or the generated output. -/// Polymorphic base type discriminated by type. +/// One shipped agent, projected for a listing. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ResponseFormatJsonSchema), "json_schema")] -public partial class ResponseFormat +internal sealed class AgentsGetBuiltinListingDefinitionResult { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + /// The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. + [JsonPropertyName("definitionJson")] + public string DefinitionJson { get; set; } = string.Empty; } - -/// A JSON Schema output contract. OpenAI receives the name, description, schema and strict setting; Anthropic receives the schema in output_config.format and always uses its native strict enforcement. +/// The shipped agent whose listing entry to load. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class JsonSchemaResponseFormat +internal sealed class AgentsGetBuiltinListingDefinitionRequest { - /// Optional description passed to OpenAI providers. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// Name of the output schema, subject to the provider's naming restrictions. + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - - /// 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 and schema restrictions are determined by the provider. - [JsonPropertyName("schema")] - public JsonElement Schema { get; set; } - - /// Optional strict enforcement setting for OpenAI providers. Omitted uses the provider default. Anthropic always enforces its supported schema subset. - [JsonPropertyName("strict")] - public bool? Strict { get; set; } } -/// The json_schema variant of . +/// The model to switch to, and the warning to show when the agent's preference could not be met. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ResponseFormatJsonSchema : ResponseFormat +internal sealed class AgentsCustomAgentInitialModelDecisionResult { - /// - [JsonIgnore] - public override string Type => "json_schema"; + /// The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } - /// JSON Schema and provider options for the turn's output. - [JsonPropertyName("jsonSchema")] - public required JsonSchemaResponseFormat JsonSchema { get; set; } + /// The first available model that matches the agent's preferences. Absent when none of the requested models is available. + [JsonPropertyName("targetModel")] + public string? TargetModel { get; set; } + + /// What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Parameters for sending a user message to the session. +/// The models a custom agent asks for, and the models actually available. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SendRequest +internal sealed class AgentsCustomAgentInitialModelDecisionParams { - /// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. - [JsonPropertyName("agentMode")] - public SendAgentMode? AgentMode { get; set; } - - /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the message. - [JsonPropertyName("attachments")] - public IList? Attachments { get; set; } - - /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. - [JsonPropertyName("billable")] - public bool? Billable { get; set; } + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + [JsonPropertyName("agentModelsJson")] + public string AgentModelsJson { get; set; } = string.Empty; - /// If provided, this is shown in the timeline instead of `prompt`. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// The models available to this session, serialized in the shape the model list carries. + [JsonPropertyName("availableModelsJson")] + public string AvailableModelsJson { get; set; } = string.Empty; +} - /// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. - [JsonPropertyName("mode")] - public SendMode? Mode { get; set; } +/// Loaded instruction source for a session, including path, content, category, location, applicability, and optional description. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class InstructionSource +{ + /// Glob pattern(s) from frontmatter — when set, this instruction applies only to matching files. + [JsonPropertyName("applyTo")] + public IList? ApplyTo { get; set; } - /// If true, adds the message to the front of the queue instead of the end. - [JsonPropertyName("prepend")] - public bool? Prepend { get; set; } + /// Raw content of the instruction file. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// The user message text. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// When true, this source starts disabled and must be toggled on by the user. + [JsonPropertyName("defaultDisabled")] + public bool? DefaultDisabled { get; set; } - /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. - [JsonPropertyName("requestHeaders")] - public IDictionary? RequestHeaders { get; set; } + /// Short description (body after frontmatter) for use in instruction tables. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange. - [JsonPropertyName("requiredTool")] - public string? RequiredTool { get; set; } + /// Unique identifier for this source (used for toggling). + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Provider-native output format for this turn, including all tool-call iterations. Not inherited by later turns or subagents. Ordinary steering inherits the active format; specifying responseFormat with mode: immediate is an error, even while idle. Returned assistant content remains text; the runtime does not parse or validate it. Unsupported models or schemas produce provider errors. - [JsonPropertyName("responseFormat")] - public ResponseFormat? ResponseFormat { get; set; } + /// Human-readable label. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Where this source lives — used for UI grouping. + [JsonPropertyName("location")] + public InstructionSourceLocation Location { get; set; } - /// Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-<command-id>` for command-originated messages, `schedule-<numeric-id>` for scheduled prompts, or `agent-<agent-id>` for prompts sent by another agent. - [RegularExpression("^(user|system|command-.*|schedule-\\d+|agent-.+)$")] - [JsonInclude] - [JsonPropertyName("source")] - internal string? Source { get; set; } + /// The project path this source was discovered from. Only set by sessionless discovery for repository, working-directory, and project-scoped plugin sources, where it disambiguates sources across multiple workspace roots. The session-scoped getSources leaves it unset. + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } - /// W3C Trace Context traceparent header for distributed tracing of this agent turn. - [JsonPropertyName("traceparent")] - public string? Traceparent { get; set; } + /// File path relative to repo or absolute for home. + [JsonPropertyName("sourcePath")] + public string SourcePath { get; set; } = string.Empty; - /// W3C Trace Context tracestate header for distributed tracing. - [JsonPropertyName("tracestate")] - public string? Tracestate { get; set; } + /// Category of instruction source — used for merge logic. + [JsonPropertyName("type")] + public InstructionSourceType Type { get; set; } +} - /// If true, await completion of the agentic loop for this message before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageId`; the caller can rely on the agent having processed the message before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. - [JsonPropertyName("wait")] - public bool? Wait { get; set; } +/// Instruction sources discovered across user, repository, and plugin sources. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerInstructionSourceList +{ + /// All discovered instruction sources. + [JsonPropertyName("sources")] + public IList Sources { get => field ??= []; set; } } -/// Result of sending zero or more user messages. +/// Optional project paths to include in instruction discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SendMessagesResult +internal sealed class InstructionsDiscoverRequest { - /// Unique identifiers assigned to the messages, one per provided message in order. For a batch that starts a run, assistant messages use the final ID as originatingMessageId throughout that run, including tool iterations and stop-hook corrections. Immediate steering does not replace the active run's origin. Empty when no messages were provided; that run has no originatingMessageId. - [JsonPropertyName("messageIds")] - public IList MessageIds { get => field ??= []; set; } + /// When true, omit the host's instruction sources (user/home-level files and plugin rules), leaving only repository and working-directory sources. For multitenant deployments. + [JsonPropertyName("excludeHostInstructions")] + public bool? ExcludeHostInstructions { get; set; } + + /// Optional list of project directory paths to scan for repository/working-directory instruction sources. When omitted or empty, only user-level and plugin instruction sources are returned (no project scan). + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } } -/// A single user message to append to the session as part of a `session.sendMessages` turn. +/// Canonical file or directory where custom instructions can be discovered or created, with location, kind, preference, and project path. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SendMessageItem +public sealed class InstructionDiscoveryPath { - /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with this message. - [JsonPropertyName("attachments")] - public IList? Attachments { get; set; } + /// Whether the target is a single file or a directory of instruction files. + [JsonPropertyName("kind")] + public InstructionDiscoveryPathKind Kind { get; set; } - /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. - [JsonInclude] - [JsonPropertyName("billable")] - internal bool? Billable { get; set; } + /// Which tier this target belongs to. + [JsonPropertyName("location")] + public InstructionDiscoveryPathLocation Location { get; set; } - /// If provided, this is shown in the timeline instead of `prompt`. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// Absolute path of the file or directory (may not exist on disk yet). + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// The user message text. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Whether this is the canonical target to create new instructions in its tier. At most one entry per tier is preferred. + [JsonPropertyName("preferredForCreation")] + public bool PreferredForCreation { get; set; } - /// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange. - [JsonPropertyName("requiredTool")] - public string? RequiredTool { get; set; } + /// The input project path this target was derived from (only for repository targets). + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } +} - /// Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-<command-id>` for command-originated messages, `schedule-<numeric-id>` for scheduled prompts, or `agent-<agent-id>` for prompts sent by another agent. - [RegularExpression("^(user|system|command-.*|schedule-\\d+|agent-.+)$")] - [JsonInclude] - [JsonPropertyName("source")] - internal string? Source { get; set; } +/// Canonical files and directories where custom instructions can be created so the runtime will recognize them. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class InstructionDiscoveryPathList +{ + /// Canonical instruction create/discovery files and directories, in priority order. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } } -/// 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. +/// Optional project paths to include when enumerating instruction discovery targets. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SendMessagesRequest +internal sealed class InstructionsGetDiscoveryPathsRequest { - /// The UI mode the agent was in when these messages were sent. Defaults to the session's current mode. - [JsonPropertyName("agentMode")] - public SendAgentMode? AgentMode { get; set; } + /// When true, omit the host's user-level instruction targets, leaving only repository targets. For multitenant deployments (mirrors `discover`'s `excludeHostInstructions`). + [JsonPropertyName("excludeHostInstructions")] + public bool? ExcludeHostInstructions { get; set; } - /// The user messages to append to the conversation, in order, before running one agent loop. When the batch starts a run, its final message is the primary initiating message; earlier messages provide context, not separate runs or replies. May be empty, in which case a single turn runs over the existing history with no new user message or originatingMessageId. - [JsonPropertyName("messages")] - public IList Messages { get => field ??= []; set; } + /// Optional list of project directory paths. When omitted or empty, only the user-level targets are returned. + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } +} - /// How to deliver the messages. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. - [JsonPropertyName("mode")] - public SendMode? Mode { get; set; } +/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class InstalledPlugin +{ + /// Path where the plugin is cached locally. + [JsonPropertyName("cache_path")] + public string? CachePath { get; set; } - /// If true, adds the messages to the front of the queue instead of the end. - [JsonPropertyName("prepend")] - public bool? Prepend { get; set; } + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. - [JsonPropertyName("requestHeaders")] - public IDictionary? RequestHeaders { get; set; } + /// Installation timestamp. + [JsonPropertyName("installed_at")] + public string InstalledAt { get; set; } = string.Empty; - /// Provider-native output format for the whole turn, including an empty message batch and all tool-call iterations. Not inherited by later turns or subagents. Ordinary steering inherits the active format; specifying responseFormat with mode: immediate is an error, even while idle. Returned assistant content remains text; the runtime does not parse or validate it. Unsupported models or schemas produce provider errors. - [JsonPropertyName("responseFormat")] - public ResponseFormat? ResponseFormat { 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Marketplace the plugin came from (empty string for direct repo installs). + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; - /// W3C Trace Context traceparent header for distributed tracing of this agent turn. - [JsonPropertyName("traceparent")] - public string? Traceparent { get; set; } + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// W3C Trace Context tracestate header for distributed tracing. - [JsonPropertyName("tracestate")] - public string? Tracestate { get; set; } + /// Source for direct repo installs (when marketplace is empty). + [JsonPropertyName("source")] + public JsonElement? Source { get; set; } - /// If true, await completion of the agentic loop for this turn before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageIds`; the caller can rely on the agent having processed the messages before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. - [JsonPropertyName("wait")] - public bool? Wait { get; set; } + /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. + [JsonPropertyName("source_sha")] + public string? SourceSha { get; set; } + + /// Version installed (if available). + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Internal request for sending a system notification. +/// An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SendSystemNotificationRequest +internal sealed class LoggedInUser { - /// Optional structured notification kind. - [JsonPropertyName("kind")] - public JsonElement? Kind { get; set; } + /// Source account this account was derived from, when one was recorded. + [JsonPropertyName("derivedFrom")] + public string? DerivedFrom { get; set; } - /// Notification text to deliver to the model. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Host the account belongs to, such as `github.com` or an Enterprise server. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Internal delivery options, including passive policy. - [JsonPropertyName("options")] - public JsonElement? Options { get; set; } + /// Account kind, when the host recorded one. Consumers must tolerate new strings. + [JsonPropertyName("kind")] + public string? Kind { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Account login on that host. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; } -/// Result of aborting the current turn. +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AbortResult +internal sealed class GlobalStateLoadResult { - /// Error message if the abort failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Whether the user has answered the prompt suggesting they install the desktop app. + [JsonPropertyName("appInstallNudgeResponded")] + public bool? AppInstallNudgeResponded { get; set; } - /// Whether the abort completed successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Whether the app tip has been shown. + [JsonPropertyName("appTipShown")] + public bool? AppTipShown { get; set; } -/// Parameters for aborting the current turn. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AbortRequest -{ - /// Finite reason code describing why the current turn was aborted. - [JsonPropertyName("reason")] - public AbortReason? Reason { get; set; } + /// Terminals the user has already been asked to set up, so the host does not ask twice. + [JsonPropertyName("askedSetupTerminals")] + public IList? AskedSetupTerminals { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + [JsonPropertyName("autoFeedbackLastPromptedAt")] + public string? AutoFeedbackLastPromptedAt { get; set; } + + /// When the host first ran on this machine. + [JsonPropertyName("firstLaunchAt")] + public string? FirstLaunchAt { get; set; } + + /// Plugins installed on this machine. + [JsonPropertyName("installedPlugins")] + public IList? InstalledPlugins { get; set; } + + /// Account used for the most recent sign-in. + [JsonPropertyName("lastLoggedInUser")] + public LoggedInUser? LastLoggedInUser { get; set; } + + /// Every account the host has signed in to on this machine. + [JsonPropertyName("loggedInUsers")] + public IList? LoggedInUsers { get; set; } + + /// Whether the one-off cleanup of stored reasoning summaries has run. + [JsonPropertyName("reasoningSummariesCleanupDone")] + public bool? ReasoningSummariesCleanupDone { get; set; } + + /// Models the user selected recently, most recent first. + [JsonPropertyName("recentModelIds")] + public IList? RecentModelIds { get; set; } + + /// Whether the user declined to trust the sandbox credential proxy CA. + [JsonPropertyName("sandboxCredentialProxyCaDeclined")] + public bool? SandboxCredentialProxyCaDeclined { get; set; } + + /// Whether the sandbox onboarding has been shown. + [JsonPropertyName("sandboxOnboardingShown")] + public bool? SandboxOnboardingShown { get; set; } + + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + [JsonPropertyName("staff")] + public bool? Staff { get; set; } + + /// Whether the user was recognized as GitHub staff. + [JsonPropertyName("staffGithub")] + public bool? StaffGitHub { get; set; } + + /// When the staff-only log level migration last ran. + [JsonPropertyName("staffLogLevelMigrationAt")] + public string? StaffLogLevelMigrationAt { get; set; } + + /// Whether the user was recognized as Microsoft staff. + [JsonPropertyName("staffMicrosoft")] + public bool? StaffMicrosoft { get; set; } + + /// When the staff-only model reset last ran. + [JsonPropertyName("staffModelResetAt")] + public string? StaffModelResetAt { get; set; } + + /// When the staff-only update channel migration last ran. + [JsonPropertyName("staffUpdateChannelMigrationAt")] + public string? StaffUpdateChannelMigrationAt { get; set; } + + /// Folders where the user declined the init prompt, so it stays hidden there. + [JsonPropertyName("suppressInitFolders")] + public IList? SuppressInitFolders { get; set; } + + /// Folders the user has marked as trusted. + [JsonPropertyName("trustedFolders")] + public IList? TrustedFolders { get; set; } } -/// Result of interrupting the main agent turn. +/// Selects the configuration directory whose machine-wide state to read. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InterruptMainTurnResult +internal sealed class GlobalStateLoadForConfigDirRequest { - /// Whether an in-flight main agent turn was interrupted. False when the main loop was not processing. - [JsonPropertyName("interrupted")] - public bool Interrupted { get; set; } + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } } -/// Parameters for interrupting the main agent turn. +/// A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class InterruptMainTurnRequest +internal sealed class GlobalStateWriteKeyRequest { - /// When true, the user's queued prompts are preserved and run as the next turn once the interrupted turn unwinds; when false (the default), the queue is cleared like a plain abort. - [JsonPropertyName("flushQueued")] - public bool? FlushQueued { get; set; } + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + [JsonPropertyName("value")] + public JsonElement? Value { get; set; } } -/// Identifies the target session. +/// A literal choice the command input accepts, with a human-facing description. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCancelAllBackgroundAgentsRequest +public sealed class SlashCommandInputChoice { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Human-readable description shown alongside the choice. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// The literal choice value (e.g. 'on', 'off', 'show'). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Parameters for shutting down the session. +/// Optional unstructured input hint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShutdownRequest +public sealed class SlashCommandInput { - /// Dispatch deferred sessionEnd hooks in the background with their full per-hook timeoutSec instead of awaiting them under the short shared shutdown budget. Set this when the host process keeps running after the session closes (for example the CLI's /clear), so a slow hook neither blocks the close nor is aborted. Hooks still detached when the process later exits are terminated with it. Defaults to false. - [JsonPropertyName("detachSessionEndHooks")] - public bool? DetachSessionEndHooks { get; set; } + /// Optional literal choices the input accepts, each with a human-facing description; clients may render these as selectable options. + [JsonPropertyName("choices")] + public IList? Choices { get; set; } - /// Optional human-readable reason. Typically the message of the error that triggered shutdown when type is 'error'. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). + [JsonPropertyName("completion")] + public SlashCommandInputCompletion? Completion { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Hint to display when command input has not been provided. + [JsonPropertyName("hint")] + public string Hint { get; set; } = string.Empty; - /// Why the session is being shut down. Defaults to "routine" when omitted. - [JsonPropertyName("type")] - public ShutdownType? Type { get; set; } -} + /// When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace. + [JsonPropertyName("preserveMultilineInput")] + public bool? PreserveMultilineInput { get; set; } -/// Identifier of the session event that was emitted for the log message. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LogResult -{ - /// The unique identifier of the emitted session event. - [JsonPropertyName("eventId")] - public Guid EventId { get; set; } + /// When true, the command requires non-empty input; clients should render the input hint as required. + [JsonPropertyName("required")] + public bool? Required { get; set; } } -/// Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. +/// Slash-command metadata with name, aliases, description, kind, input hint, execution allowance, and schedulability. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class LogRequest +public sealed class SlashCommandInfo { - /// When true, the message is transient and not persisted to the session event log on disk. - [JsonPropertyName("ephemeral")] - public bool? Ephemeral { get; set; } + /// Canonical aliases without leading slashes. + [JsonPropertyName("aliases")] + public IList? Aliases { get; set; } - /// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". - [JsonPropertyName("level")] - public SessionLogLevel? Level { get; set; } + /// Whether the command may run while an agent turn is active. + [JsonPropertyName("allowDuringAgentExecution")] + public bool AllowDuringAgentExecution { get; set; } - /// Human-readable message. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Human-readable command description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the command is experimental. + [JsonPropertyName("experimental")] + public bool? Experimental { get; set; } - /// Optional actionable tip displayed alongside the message. Only honored on `level: "info"`. - [JsonPropertyName("tip")] - public string? Tip { get; set; } + /// Optional unstructured input hint. + [JsonPropertyName("input")] + public SlashCommandInput? Input { get; set; } - /// Domain category for this log entry (e.g., "mcp", "subscription", "policy", "model"). Maps to `infoType`/`warningType`/`errorType` on the emitted event. Defaults to "notification". - [JsonPropertyName("type")] - public string? Type { get; set; } + /// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. + [JsonPropertyName("kind")] + public SlashCommandKind Kind { get; set; } - /// Optional URL the user can open in their browser for more details. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("url")] - public string? Url { get; set; } + /// Canonical command name without a leading slash. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt. + [JsonPropertyName("schedulable")] + public bool? Schedulable { get; set; } } -/// Managed sandbox enforcement state for a session. +/// Slash commands available in the session, after applying any include/exclude filters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxEnforcementStatus +public sealed class CommandList { - /// Whether an enforcement failure has permanently blocked the session. - [JsonPropertyName("blocked")] - public bool Blocked { get; set; } + /// Commands available in this session. + [JsonPropertyName("commands")] + public IList Commands { get => field ??= []; set; } +} - /// The first sandbox enforcement failure that blocked the session. - [JsonPropertyName("reason")] - public string? Reason { get; set; } +/// A single user setting's effective value alongside its default, so consumers can render settings left at their default. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UserSettingMetadata +{ + /// The centrally-known default for this setting (null when no default is registered). + [JsonPropertyName("default")] + public JsonElement Default { get; set; } - /// Whether the effective managed policy requires an available sandbox backend. - [JsonPropertyName("required")] - public bool Required { get; set; } + /// True when the user has not set an explicit value for this setting (i.e. it is left at its default). Reflects whether the user has overridden the key, not whether the effective value happens to equal the default — a key explicitly set to a value identical to the default still reports false. + [JsonPropertyName("isDefault")] + public bool IsDefault { get; set; } + + /// The effective value: the user's value if set, otherwise the default. + [JsonPropertyName("value")] + public JsonElement Value { get; set; } } -/// Identifies the target session. +/// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSandboxGetEnforcementStatusRequest +public sealed class UserSettingsGetResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Every known user setting keyed by setting name, each with its effective value, default, and whether it is at the default. + [JsonPropertyName("settings")] + public IDictionary Settings { get => field ??= new Dictionary(); set; } } -/// Result of attempting to disable sandboxing for the current session. +/// Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxDisableForSessionResult +internal sealed class UserSettingsSetRequest { - /// 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; } + /// Partial user settings to write, as a free-form object keyed by setting name. + [JsonPropertyName("settings")] + public JsonElement Settings { get; set; } } -/// Optional informational context describing how and where the permission decision was made. This does not affect permission behavior. +/// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionDecisionContext +public sealed class GitHubRepositoryIdentity { - /// Disposition of the permission request as observed by the responding client. - [JsonPropertyName("outcome")] - public PermissionDecisionOutcome Outcome { get; set; } + /// Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + [JsonPropertyName("host")] + public string Host { 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; } + /// Repository name, without the owner prefix or the `.git` suffix. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Controlled reason or actor responsible for the response. - [JsonPropertyName("source")] - public PermissionDecisionSource Source { get; set; } + /// Repository owner login (user or organization). + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; +} - /// Client surface that submitted the response. - [JsonPropertyName("surface")] - public PermissionDecisionSurface Surface { get; set; } +/// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubRepositoryAtPathResult +{ + /// Resolved repository identity, or null when the selected remote resolves to no GitHub host. + [JsonPropertyName("repository")] + public GitHubRepositoryIdentity? Repository { get; set; } } -/// Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. +/// Working-tree path whose owning GitHub repository should be resolved. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SandboxDisableForSessionRequest +internal sealed class GitHubRepositoryAtPathRequest { - /// Optional attribution for the permission decision. - [JsonPropertyName("decisionContext")] - public PermissionDecisionContext? DecisionContext { get; set; } + /// Absolute path to a directory inside the git working tree to resolve. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} - /// Identifier of the exact pending sandbox-bypass permission request that authorized the session opt-out. +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersRequestIdResult +{ + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + public long RequestId { get; set; } } -/// Authentication status and account metadata for the session. +/// A GitHub login the authenticated user may act as: their own account, or an organization they belong to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionAuthStatus +internal sealed class GitHubOwnerOption { - /// Authentication type. - [JsonPropertyName("authType")] - public AuthInfoType? AuthType { get; set; } + /// The owner's GitHub login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Copilot plan tier (e.g., individual_pro, business). - [JsonPropertyName("copilotPlan")] - public string? CopilotPlan { get; set; } + /// Which kind of owner this is. The authenticated user's own account is always reported as `user`. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; +} - /// Authentication host URL. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("host")] - public string? Host { get; set; } +/// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersListResult +{ + /// Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Whether the session has resolved authentication. - [JsonPropertyName("isAuthenticated")] - public bool IsAuthenticated { get; set; } + /// The owners, on success: the authenticated user first, then the organizations they belong to. + [JsonPropertyName("owners")] + public IList? Owners { get; set; } - /// Authenticated login/username, if available. - [JsonPropertyName("login")] - public string? Login { get; set; } + /// A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. + [JsonPropertyName("throwError")] + public string? ThrowError { get; set; } - /// Human-readable authentication status description. - [JsonPropertyName("statusMessage")] - public string? StatusMessage { get; set; } + /// A line the caller should log. Present only alongside `message`, and only for failures worth recording. + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Identifies the target session. +/// Credential to list owners under, and the request id that makes the listing cancellable. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetStatusRequest +internal sealed class GitHubOwnersListRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + [JsonPropertyName("authInfo")] + public JsonElement AuthInfo { get; set; } + + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } } -/// Indicates whether the credential update succeeded. +/// Whether the id named a running owner listing. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSetCredentialsResult +internal sealed class GitHubOwnersCancelResult { - /// 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; } - - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. + [JsonPropertyName("canceled")] + public bool Canceled { get; set; } } -/// Authentication credentials accepted by session.gitHubAuth.setCredentials. Session-owned token-provider identities cannot be installed through this method. -/// Polymorphic base type discriminated by type. +/// The owner listing to abandon. [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 +internal sealed class GitHubOwnersCancelRequest { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + /// Request id the listing was started with. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } } - -/// Authentication-info input variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret. -/// The hmac variant of . +/// The remote the checked-out branch tracks. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoHmac : SettableAuthInfo +internal sealed class GitCurrentBranchRemoteResult { - /// - [JsonIgnore] - public override string Type => "hmac"; - - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } - - /// HMAC secret used to sign requests. - [JsonPropertyName("hmac")] - public required string Hmac { get; set; } + /// Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("remote")] + public string Remote { get; set; } = string.Empty; +} - /// Authentication host. HMAC auth always targets the public GitHub host. - [JsonPropertyName("host")] - public required string Host { get; set; } +/// Working-tree path a git query applies to. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitCwdRequest +{ + /// Absolute path to a directory inside the git working tree to query. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; } -/// 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 . +/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoEnv : SettableAuthInfo +public sealed class SessionWorkingDirectoryContext { - /// - [JsonIgnore] - public override string Type => "env"; + /// Merge-base commit SHA (fork point from the remote default branch). + [JsonPropertyName("baseCommit")] + public string? BaseCommit { 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; } + /// Current git branch name. + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// Name of the environment variable the token was sourced from. - [JsonPropertyName("envVar")] - public required string EnvVar { get; set; } + /// Current working directory path. + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; - /// Authentication host (e.g. https://github.com or a GHES host). - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Root directory of the git repository, resolved via git rev-parse. + [JsonPropertyName("gitRoot")] + public string? GitRoot { 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; } + /// Head commit of the current git branch. + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } - /// The token value itself. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { get; set; } + /// Hosting platform type of the repository. + [JsonPropertyName("hostType")] + public SessionWorkingDirectoryContextHostType? HostType { get; set; } + + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com"). + [JsonPropertyName("repositoryHost")] + public string? RepositoryHost { get; set; } } -/// Token authentication accepted by session.gitHubAuth.setCredentials. -/// The token variant of . +/// A GitHub repository one of a working tree's remotes points at. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoToken : SettableAuthInfo +internal sealed class GitRemoteRepository { - /// - [JsonIgnore] - public override string Type => "token"; + /// GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } + /// Repository name, without the owner. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Account or organization owning the repository. + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; - /// The token value itself. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { get; set; } + /// Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. + [JsonPropertyName("remoteName")] + public string RemoteName { get; set; } = string.Empty; } -/// Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host. -/// The copilot-api-token variant of . +/// The GitHub repositories a working tree's remotes point at. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoCopilotApiToken : SettableAuthInfo +internal sealed class GitReposFromRemotesResult { - /// - [JsonIgnore] - public override string Type => "copilot-api-token"; - - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } - - /// Authentication host (always the public GitHub host). - [JsonPropertyName("host")] - public required string Host { get; set; } + /// One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + [JsonPropertyName("repositories")] + public IList Repositories { get => field ??= []; set; } } -/// Authentication-info variant for OAuth user auth, with host and login; the token remains in the runtime secret store. -/// The user variant of . +/// Git working tree whose GitHub remotes should be listed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoUser : SettableAuthInfo +internal sealed class GitReposFromRemotesRequest { - /// - [JsonIgnore] - public override string Type => "user"; - - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } + /// Absolute path to the root of the git working tree. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("gitRoot")] + public string GitRoot { get; set; } = string.Empty; +} - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } +/// Validated device-managed settings discovered before a session exists. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsReadResult +{ + /// Discovery or validation error text when managed settings could not be read safely. + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } - /// OAuth user login. - [JsonPropertyName("login")] - public required string Login { get; set; } + /// Validated, canonical managed-settings JSON. Omitted when no managed settings were discovered or when discovered settings failed validation. + [JsonPropertyName("settingsJson")] + public JsonElement? SettingsJson { get; set; } } -/// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. -/// The gh-cli variant of . +/// One validation finding for a managed-settings document. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoGhCli : SettableAuthInfo +public sealed class ManagedSettingsDiagnostic { - /// - [JsonIgnore] - public override string Type => "gh-cli"; + /// Human-readable description of the finding. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } + /// 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; - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Whether the finding rejects the document. + [JsonPropertyName("severity")] + public ManagedSettingsDiagnosticSeverity Severity { get; set; } +} - /// User login as reported by `gh auth status`. - [JsonPropertyName("login")] - public required string Login { 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; } - /// The token returned by `gh auth token`. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { 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; } -/// 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 . +/// Lock state and provenance of one managed setting. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoApiKey : SettableAuthInfo +public sealed class ManagedSettingMeta { - /// - [JsonIgnore] - public override string Type => "api-key"; + /// Whether users and repositories may choose a different value. `false` means policy locks the value. + [JsonPropertyName("overridable")] + public bool Overridable { get; set; } - /// The API key. Treat as a secret. - [JsonPropertyName("apiKey")] - public required string ApiKey { 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; +} - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } - - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } -} - -/// New auth credentials to install on the session. Omit to leave credentials unchanged. +/// 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)] -internal sealed class SessionSetCredentialsParams +public sealed class ManagedSettingsMeta { - /// 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; } + /// Lock state and provenance of `values.autoTier`. + [JsonPropertyName("autoTier")] + public ManagedSettingMeta? AutoTier { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Lock state and provenance of `values.model`. + [JsonPropertyName("model")] + public ManagedSettingMeta? Model { get; set; } } -/// Credential-free authentication identity safe to expose to hosts and user interfaces. +/// 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 AuthIdentity +public sealed class ManagedSettingsResolvedData { - /// Snapshot of the authenticated user's Copilot subscription info, if known. - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } + /// 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; } - /// Name of the environment variable that supplied the credential, when applicable. - [JsonPropertyName("envVar")] - public string? EnvVar { get; set; } + /// Whether a session-local permissions layer injected by the SDK host was present. + [JsonPropertyName("clientManaged")] + public bool? ClientManaged { get; set; } - /// Authentication host. - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; + /// Whether an actual device MDM/plist/registry/file managed-settings layer was present. + [JsonPropertyName("deviceManaged")] + public bool DeviceManaged { get; set; } - /// Authenticated login, when available. - [JsonPropertyName("login")] - public string? Login { 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; } - /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. - [JsonPropertyName("registrationId")] - public string? RegistrationId { 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; } - /// Authentication type. - [JsonPropertyName("type")] - public AuthInfoType Type { 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; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetCurrentAuthInfoRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// 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; } -/// 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; + /// 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; } } -/// Identifies the target session. +/// 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)] -internal sealed class SessionGitHubAuthRefreshCopilotUserRequest +public sealed class ManagedSettingsValues { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Internal GitHub login parameters. +/// Effective enterprise managed settings for an account, resolved without a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAuthLoginRequest +public sealed class ManagedSettingsResolveResult { - /// GitHub host URL. - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; + /// 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; } - /// GitHub login. - [JsonPropertyName("login")] - public string Login { get; set; } = string.Empty; + /// 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; } - /// Whether to persist the token after login. - [JsonPropertyName("persist")] - public bool? Persist { get; 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Per-key lock state and provenance for the entries in `values`, using the same key names. + [JsonPropertyName("meta")] + public ManagedSettingsMeta? Meta { get; set; } - /// GitHub authentication token. - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; + /// 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; } } -/// Parameters for switching the session's active authentication. +/// RPC data type for ManagedSettingsResolve operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAuthSwitchRequest +internal sealed class ManagedSettingsResolveRequest { - /// Authentication information to activate. - [JsonPropertyName("authInfo")] - public AuthInfo AuthInfo { get => field ??= new(); set; } + /// Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } - /// Optional token paired with the authentication information. - [JsonPropertyName("token")] - public string? Token { 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; } } -/// Identifies the target session. +/// The authoring JSON schema for managed settings recognized by this runtime. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthLogoutRequest +public sealed class ManagedSettingsSchemaResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Parameters identifying a GitHub authentication to log out. +/// Result of validating a managed-settings document. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAuthLogoutUserRequest +public sealed class ManagedSettingsValidateResult { - /// Authentication information to log out. - [JsonPropertyName("authInfo")] - public AuthInfo AuthInfo { get => field ??= new(); set; } + /// Errors that reject the document and warnings about content the runtime ignores. + [JsonPropertyName("diagnostics")] + public IList Diagnostics { get => field ??= []; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Validation error from an authentication attempt. +/// A candidate managed-settings document to validate without applying it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthValidationError +internal sealed class ManagedSettingsValidateRequest { - /// Optional message returned by GitHub. - [JsonPropertyName("githubMessage")] - public string? GitHubMessage { get; set; } + /// 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; } - /// Authentication validation error message. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// 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; } } -/// Identifies the target session. +/// 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)] -internal sealed class SessionGitHubAuthLastAuthErrorsRequest +public sealed class ManagedSettingsComposeResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// A file included in the session debug bundle. +/// One candidate channel; absent settings represents a channel that delivered no document. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsCollectedEntry +public sealed class ManagedSettingsComposeLayer { - /// Relative path of the file in the staged bundle/archive. - [JsonPropertyName("bundlePath")] - public string BundlePath { get; set; } = string.Empty; - - /// Redacted output size in bytes. - [JsonPropertyName("sizeBytes")] - public long SizeBytes { get; set; } + /// Candidate managed-settings document. Omit when the channel delivered none, as in resolve output. + [JsonPropertyName("settings")] + public JsonElement? Settings { get; set; } - /// Source category for this entry. + /// The channel whose candidate document is being supplied. [JsonPropertyName("source")] - public DebugCollectLogsSource Source { get; set; } + public ManagedSettingsChannel Source { get; set; } } -/// An optional debug bundle entry that could not be included. +/// Candidate managed-settings documents to merge without applying them. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsSkippedEntry +internal sealed class ManagedSettingsComposeRequest { - /// Relative path requested for this bundle entry. - [JsonPropertyName("bundlePath")] - public string BundlePath { get; set; } = string.Empty; - - /// Server-local source path that could not be read. - [JsonPropertyName("path")] - public string? Path { get; set; } - - /// Reason the entry was skipped. - [JsonPropertyName("reason")] - public string Reason { get; set; } = string.Empty; + /// 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; } } -/// Result of collecting a session debug bundle. +/// Indicates whether the calling client was registered as the session filesystem provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsResult +public sealed class SessionFsSetProviderResult { - /// Files included in the bundle. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } - - /// Destination kind that was written. - [JsonPropertyName("kind")] - public DebugCollectLogsResultKind Kind { get; set; } + /// Whether the provider was set successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Actual archive path or staging directory path written. This may differ from the requested path when no-overwrite suffixing or fallback-to-temp-directory was needed. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; +/// Optional capabilities declared by the provider. +[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; } - /// Optional files or directories that could not be included. - [JsonPropertyName("skippedEntries")] - public IList? SkippedEntries { get; set; } + /// Whether the provider supports SQLite query/exists operations. + [JsonPropertyName("sqlite")] + public bool? Sqlite { get; set; } } -/// A caller-provided server-local file or directory to include in the debug bundle. +/// 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. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsEntry +internal sealed class SessionFsSetProviderRequest { - /// 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; } + /// Optional capabilities declared by the provider. + [JsonPropertyName("capabilities")] + public SessionFsSetProviderCapabilities? Capabilities { get; set; } - /// Server-local source path to read. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Path conventions used by this filesystem. + [JsonPropertyName("conventions")] + public SessionFsSetProviderConventions Conventions { 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; } + /// Absolute initial working directory for sessions. Registering the provider establishes this path as the root of its virtual namespace; the runtime does not require the provider to materialize or stat it before creating a session. + [JsonPropertyName("initialCwd")] + public string InitialCwd { 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; } + /// Path within each session's SessionFs where the runtime stores files for that session. + [JsonPropertyName("sessionStatePath")] + public string SessionStatePath { get; set; } = string.Empty; } -/// Destination for the session debug bundle. -/// Polymorphic base type discriminated by kind. +/// Indicates whether the calling client was registered as the LLM inference provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] -[JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] -public partial class DebugCollectLogsDestination +public sealed class LlmInferenceSetProviderResult { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// Whether the provider was set successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } } +/// Whether the start frame was accepted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class LlmInferenceHttpResponseStartResult +{ + /// True when the response start was matched to a pending request; false when unknown. + [JsonPropertyName("accepted")] + public bool Accepted { get; set; } +} -/// The archive variant of . +/// Response head. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class DebugCollectLogsDestinationArchive : DebugCollectLogsDestination +internal sealed class LlmInferenceHttpResponseStartRequest { - /// - [JsonIgnore] - public override string Kind => "archive"; + /// HTTP response headers, preserving multiple values per name. + [JsonPropertyName("headers")] + public IDictionary> Headers { get => field ??= new Dictionary>(); 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; } + /// Matches the requestId from the originating httpRequestStart frame. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Absolute or server-relative path for the .tgz archive to create. - [JsonPropertyName("outputPath")] - public required string OutputPath { get; set; } + /// HTTP status code. + [JsonPropertyName("status")] + public long Status { get; set; } + + /// Optional HTTP status reason phrase. + [JsonPropertyName("statusText")] + public string? StatusText { get; set; } } -/// The directory variant of . +/// Whether the chunk was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class DebugCollectLogsDestinationDirectory : DebugCollectLogsDestination +public sealed class LlmInferenceHttpResponseChunkResult { - /// - [JsonIgnore] - public override string Kind => "directory"; - - /// Directory where files should be staged. The directory is created if needed. - [JsonPropertyName("outputDirectory")] - public required string OutputDirectory { get; set; } + /// True when the chunk was matched to a pending request; false when unknown. + [JsonPropertyName("accepted")] + public bool Accepted { get; set; } } -/// Built-in session diagnostics to include in the bundle. Omitted fields default to true. +/// Set to terminate the response with a transport-level failure. Implies end-of-stream; any further chunks for this requestId are ignored. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsInclude +public sealed class LlmInferenceHttpResponseChunkError { - /// 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; } + /// Optional machine-readable error code. + [JsonPropertyName("code")] + public string? Code { get; set; } - /// Include the session event log (`events.jsonl`). Defaults to true. - [JsonPropertyName("events")] - public bool? Events { get; set; } + /// Human-readable failure description. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} - /// 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; } +/// A response body chunk or terminal error. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class LlmInferenceHttpResponseChunkRequest +{ + /// When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text. + [JsonPropertyName("binary")] + public bool? Binary { get; set; } - /// Maximum number of previous process logs to include. Defaults to 5. - [JsonPropertyName("previousProcessLogLimit")] - public long? PreviousProcessLogLimit { get; set; } + /// Body byte range. UTF-8 text when `binary` is absent or false; base64-encoded bytes when `binary` is true. May be empty (e.g. when the response body is empty: send a single chunk with empty data and end=true). + [JsonPropertyName("data")] + public string Data { get; set; } = string.Empty; - /// 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; } + /// When true, this is the final body chunk for the response. The runtime treats the response body as complete after receiving an end-marked chunk. + [JsonPropertyName("end")] + public bool? End { get; set; } - /// Include process logs for the session. Defaults to true. - [JsonPropertyName("processLogs")] - public bool? ProcessLogs { get; set; } + /// Set to terminate the response with a transport-level failure. Implies end-of-stream; any further chunks for this requestId are ignored. + [JsonPropertyName("error")] + public LlmInferenceHttpResponseChunkError? Error { get; set; } - /// Include interactive shell logs written under the session's `shell-logs` directory. Defaults to true. - [JsonPropertyName("shellLogs")] - public bool? ShellLogs { get; set; } + /// Matches the requestId from the originating httpRequestStart frame. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; } -/// Options for collecting a session debug bundle with configurable redaction. +/// Pre-resolved working-directory context for session startup. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DebugCollectLogsRequest +public sealed class SessionContext { - /// 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; } + /// Active git branch. + [JsonPropertyName("branch")] + public string? Branch { 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; } + /// Most recent working directory for this session. + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; - /// Which built-in session diagnostics to include. Omitted fields default to true. - [JsonPropertyName("include")] - public DebugCollectLogsInclude? Include { get; set; } + /// Git repository root, if the cwd was inside a git repo. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Repository host type. + [JsonPropertyName("hostType")] + public SessionContextHostType? HostType { get; set; } + + /// Repository slug in `owner/name` form, when known. + [JsonPropertyName("repository")] + public string? Repository { 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. +/// GitHub repository the remote session belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasAction +public sealed class RemoteSessionMetadataRepository { - /// Description of the action. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// JSON Schema for the action input. - [JsonPropertyName("inputSchema")] - public JsonElement? InputSchema { get; set; } + /// Branch associated with the remote session. + [JsonPropertyName("branch")] + public string Branch { get; set; } = string.Empty; - /// Action name exposed by the canvas provider. + /// Repository name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + + /// Repository owner. + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; } -/// Canvas available in the current session. +/// Remote session metadata for the session to hand off (typically obtained from `sessions.list` with `source: "remote"`). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiscoveredCanvas +public sealed class RemoteSessionMetadataValue { - /// Actions the agent or host may invoke on an open instance. - [JsonPropertyName("actions")] - public IList? Actions { get; set; } + /// Most recent working directory context. + [JsonPropertyName("context")] + public SessionContext? Context { get; set; } - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; + /// Host-supplied human description of what the session is doing right now ("running tests", "waiting for approval"). Optional in the protocol and absent on hosts that do not publish it, so never rely on it -- it enriches `hostStatus`, it does not replace it. + [JsonPropertyName("hostActivity")] + public string? HostActivity { 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; + /// Live status as the owning host reports it in its session listing, so a row for a session running elsewhere can show that it is running. Absent for hosts that publish no such status (the cloud task managers), which read as idle. + [JsonPropertyName("hostStatus")] + public RemoteSessionHostStatus? HostStatus { get; set; } - /// Human-readable canvas name. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; + /// Always true for remote sessions. + [JsonPropertyName("isRemote")] + public bool IsRemote { get; set; } - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Last-modified time as an ISO 8601 timestamp. + [JsonPropertyName("modifiedTime")] + public string ModifiedTime { get; set; } = string.Empty; - /// Owning extension display name, when available. - [JsonPropertyName("extensionName")] - public string? ExtensionName { get; set; } + /// Optional human-friendly name set via /rename. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Host-local PNG path for the canvas icon, when supplied. - [JsonPropertyName("icon")] - public string? Icon { get; set; } + /// Pull request number associated with the session. + [JsonPropertyName("pullRequestNumber")] + public long? PullRequestNumber { get; set; } - /// JSON Schema for canvas open input. - [JsonPropertyName("inputSchema")] - public JsonElement? InputSchema { get; set; } -} + /// Backing remote session IDs (most recent first). + [JsonPropertyName("remoteSessionIds")] + public IList RemoteSessionIds { get => field ??= []; 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; } -} + /// GitHub repository the remote session belongs to. + [JsonPropertyName("repository")] + public RemoteSessionMetadataRepository Repository { get => field ??= new(); set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCanvasListRequest -{ - /// Target session identifier. + /// Original remote resource identifier (task ID or PR node ID). + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + + /// Stable 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; + /// Deadline (ISO 8601) at which a CLI remote session becomes stale without further heartbeats. + [JsonPropertyName("staleAt")] + public string? StaleAt { get; set; } - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Session creation time as an ISO 8601 timestamp. + [JsonPropertyName("startTime")] + public string StartTime { get; set; } = string.Empty; - /// Owning extension display name, when available. - [JsonPropertyName("extensionName")] - public string? ExtensionName { get; set; } + /// Server-side task state returned by GitHub. + [JsonPropertyName("state")] + public string? State { get; set; } - /// Host-local PNG path for the canvas icon, when supplied. - [JsonPropertyName("icon")] - public string? Icon { get; set; } + /// Short summary of the session, when one has been derived. + [JsonPropertyName("summary")] + public string? Summary { get; set; } - /// Input supplied when the instance was opened. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// Whether the remote task originated from CCA or CLI `--remote`. + [JsonPropertyName("taskType")] + public RemoteSessionMetadataTaskType? TaskType { get; set; } +} - /// Stable caller-supplied canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; +/// `sessions.open` handoff progress update with step, status, and optional message. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionsOpenProgress +{ + /// Optional step message. + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Provider-supplied status text. + /// Step status. [JsonPropertyName("status")] - public string? Status { get; set; } - - /// Rendered title. - [JsonPropertyName("title")] - public string? Title { get; set; } + public SessionsOpenProgressStatus Status { get; set; } - /// URL for web-rendered canvases. - [JsonPropertyName("url")] - public string? Url { get; set; } + /// Handoff step. + [JsonPropertyName("step")] + public SessionsOpenProgressStep Step { get; set; } } -/// Live open-canvas snapshot. +/// Result of opening a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasListOpenResult +public sealed class SessionOpenResult { - /// Currently open canvas instances. - [JsonPropertyName("openCanvases")] - public IList OpenCanvases { get => field ??= []; set; } -} + /// Remote session metadata, present when status is `connected`. + [JsonPropertyName("metadata")] + public RemoteSessionMetadataValue? Metadata { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCanvasListOpenRequest -{ - /// Target session identifier. + /// Handoff progress steps, present when status is `handed_off`. + [JsonPropertyName("progress")] + public IList? Progress { get; set; } + + /// Remote session ID, present when status is `connected`. + [JsonPropertyName("remoteSessionId")] + public string? RemoteSessionId { get; set; } + + /// Opened session ID. Omitted when status is `not_found`. [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + public string? SessionId { get; set; } + + /// Startup prompts queued by user-level hook configs at session creation. Only populated when status is `created`; resumed sessions return an empty array. + [JsonPropertyName("startupPrompts")] + public IList? StartupPrompts { get; set; } + + /// Outcome of the open request. + [JsonPropertyName("status")] + public SessionsOpenStatus Status { get; set; } } -/// Canvas open parameters. +/// Identifier and optional friendly name assigned to the newly forked session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasOpenRequest +public sealed class SessionsForkResult { - /// 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; + /// Friendly name assigned to the forked session, if any. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Target session identifier. + /// The new forked session's ID. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Canvas close parameters. +/// Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasCloseRequest +internal sealed class SessionsForkRequest { - /// Open canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Optional friendly name to assign to the forked session. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Target session identifier. + /// Source session ID to fork from. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Optional event ID boundary. When provided, the fork includes only events before this ID (exclusive). When omitted, all events are included. + [JsonPropertyName("toEventId")] + public string? ToEventId { get; set; } } -/// Canvas action invocation result. +/// Repository associated with the connected remote session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasActionInvokeResult +public sealed class ConnectedRemoteSessionMetadataRepository { - /// Provider-supplied action result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Branch associated with the remote session. + [JsonPropertyName("branch")] + public string Branch { get; set; } = string.Empty; + + /// Repository name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Repository owner or organization login. + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; } -/// Canvas action invocation parameters. +/// Metadata for a connected remote session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasActionInvokeRequest +public sealed class ConnectedRemoteSessionMetadata { - /// Action name to invoke. - [JsonPropertyName("actionName")] - public string ActionName { get; set; } = string.Empty; + /// Neutral SDK discriminator for the connected remote session kind. + [JsonPropertyName("kind")] + public ConnectedRemoteSessionMetadataKind Kind { get; set; } - /// Action input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// Last session update time as an ISO 8601 string. + [JsonPropertyName("modifiedTime")] + public DateTimeOffset ModifiedTime { get; set; } - /// Open canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Optional friendly session name. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Target session identifier. + /// Pull request number associated with the session. + [JsonPropertyName("pullRequestNumber")] + public long? PullRequestNumber { get; set; } + + /// Repository associated with the connected remote session. + [JsonPropertyName("repository")] + public ConnectedRemoteSessionMetadataRepository Repository { get => field ??= new(); set; } + + /// Original remote resource identifier. + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + + /// SDK session ID for the connected remote session. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; -} -/// Internal canvas provider registration parameters. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasProviderRegisterRequest -{ - /// Canvas contributions supplied by the provider. - [JsonPropertyName("canvases")] - public IList Canvases { get => field ??= []; set; } + /// Remote session staleness deadline as an ISO 8601 string. + [JsonPropertyName("staleAt")] + public DateTimeOffset? StaleAt { get; set; } - /// Connection identifier for callback routing. - [JsonPropertyName("connectionId")] - public string ConnectionId { get; set; } = string.Empty; + /// Session start time as an ISO 8601 string. + [JsonPropertyName("startTime")] + public DateTimeOffset StartTime { get; set; } - /// Provider metadata supplied by the host. - [JsonPropertyName("info")] - public JsonElement Info { get; set; } + /// Remote session state returned by the backing service. + [JsonPropertyName("state")] + public string? State { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Optional session summary. + [JsonPropertyName("summary")] + public string? Summary { get; set; } } -/// Internal canvas provider unregistration parameters. +/// Remote session connection result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasProviderUnregisterRequest +public sealed class RemoteSessionConnectionResult { - /// Connection identifier to unregister. - [JsonPropertyName("connectionId")] - public string ConnectionId { get; set; } = string.Empty; + /// Metadata for a connected remote session. + [JsonPropertyName("metadata")] + public ConnectedRemoteSessionMetadata Metadata { get => field ??= new(); set; } - /// Target session identifier. + /// SDK session ID for the connected remote session. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Machine-readable factory run failure. -/// Polymorphic base type discriminated by type. +/// Remote session connection parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(FactoryRunFailureFactoryLimitReached), "factory_limit_reached")] -[JsonDerivedType(typeof(FactoryRunFailureFactoryResumeDeclined), "factory_resume_declined")] -[JsonDerivedType(typeof(FactoryRunFailureFactoryDurableFailure), "factory_durable_failure")] -[JsonDerivedType(typeof(FactoryRunFailureFactoryAccountingIncomplete), "factory_accounting_incomplete")] -[JsonDerivedType(typeof(FactoryRunFailureFactoryProviderDisconnected), "factory_provider_disconnected")] -public partial class FactoryRunFailure +internal sealed class ConnectRemoteSessionParams { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + /// Session ID to connect to. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } - -/// The factory_limit_reached variant of . +/// Local or remote session metadata entry. Narrow on `isRemote` to access source-specific fields. +/// Data type discriminated by isRemote. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class FactoryRunFailureFactoryLimitReached : FactoryRunFailure +public partial class SessionListEntry { - /// - [JsonIgnore] - public override string Type => "factory_limit_reached"; + /// The boolean discriminator. + [JsonPropertyName("isRemote")] + public bool IsRemote { get; set; } - /// Resource ceiling that stopped the run. - [JsonPropertyName("kind")] - public required FactoryRunFailureKind Kind { get; set; } + /// Runtime client name that created/last resumed this session. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Factory run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Pre-resolved working-directory context for session startup. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("context")] + public SessionContext? Context { get; set; } - /// Suggested larger ceiling when the runtime can derive one safely. + /// Host-supplied human description of what the session is doing right now ("running tests", "waiting for approval"). Optional in the protocol and absent on hosts that do not publish it, so never rely on it -- it enriches `hostStatus`, it does not replace it. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("suggestedValue")] - public double? SuggestedValue { get; set; } + [JsonPropertyName("hostActivity")] + public string? HostActivity { get; set; } - /// Approved effective ceiling that was reached. - [JsonPropertyName("value")] - public required double Value { get; set; } + /// Live status as the owning host reports it in its session listing, so a row for a session running elsewhere can show that it is running. Absent for hosts that publish no such status (the cloud task managers), which read as idle. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hostStatus")] + public RemoteSessionHostStatus? HostStatus { get; set; } + + /// True for detached maintenance sessions that should be hidden from normal resume lists. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("isDetached")] + public bool? IsDetached { get; set; } + + /// GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mcTaskId")] + public string? McTaskId { get; set; } + + /// Last-modified time of the session's persisted state, as ISO 8601. + [JsonPropertyName("modifiedTime")] + public required string ModifiedTime { get; set; } + + /// Optional human-friendly name set via /rename. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Pull request number associated with the session. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pullRequestNumber")] + public long? PullRequestNumber { get; set; } + + /// Backing remote session IDs (most recent first). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remoteSessionIds")] + public IList? RemoteSessionIds { get; set; } + + /// GitHub repository the remote session belongs to. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("repository")] + public RemoteSessionMetadataRepository? Repository { get; set; } + + /// Original remote resource identifier (task ID or PR node ID). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + + /// Stable session identifier. + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } + + /// Deadline (ISO 8601) at which a CLI remote session becomes stale without further heartbeats. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("staleAt")] + public string? StaleAt { get; set; } + + /// Session creation time as an ISO 8601 timestamp. + [JsonPropertyName("startTime")] + public required string StartTime { get; set; } + + /// Server-side task state returned by GitHub. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("state")] + public string? State { get; set; } + + /// Short summary of the session, when one has been derived. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("summary")] + public string? Summary { get; set; } + + /// Whether the remote task originated from CCA or CLI `--remote`. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("taskType")] + public RemoteSessionMetadataTaskType? TaskType { get; set; } } -/// The factory_resume_declined variant of . +/// Sessions matching the filter, ordered most-recently-modified first. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class FactoryRunFailureFactoryResumeDeclined : FactoryRunFailure +public sealed class SessionList { - /// - [JsonIgnore] - public override string Type => "factory_resume_declined"; + /// Sessions ordered most-recently-modified first. Discriminated by `isRemote`. + [JsonPropertyName("sessions")] + public IList Sessions { get => field ??= []; set; } +} - /// Human-readable reason the resume did not proceed. - [JsonPropertyName("reason")] - public required string Reason { get; set; } +/// Optional filter applied to the returned sessions. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionListFilter +{ + /// Match sessions whose context.branch equals this value. + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// Factory run identifier whose changed limits were declined. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Match sessions whose context.cwd equals this value. + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } + + /// Match sessions whose context.gitRoot equals this value. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } + + /// Match sessions whose context.repository equals this value. + [JsonPropertyName("repository")] + public string? Repository { get; set; } } -/// The factory_durable_failure variant of . +/// Optional source filter, metadata-load limit, and context filter applied to the returned sessions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class FactoryRunFailureFactoryDurableFailure : FactoryRunFailure +internal sealed class SessionsListRequest { - /// - [JsonIgnore] - public override string Type => "factory_durable_failure"; + /// Optional filter applied to the returned sessions. + [JsonPropertyName("filter")] + public SessionListFilter? Filter { get; set; } - /// Stable failure code. - [JsonPropertyName("code")] - public required string Code { get; set; } + /// When true, include detached maintenance sessions. Defaults to false for user-facing session lists. + [JsonPropertyName("includeDetached")] + public bool? IncludeDetached { get; set; } - /// Execution-critical durable operation that failed. - [JsonPropertyName("operation")] - public required FactoryDurableOperation Operation { get; set; } + /// When provided, only the first N local sessions (sorted by modification time, newest first) load full metadata; remaining sessions return basic info only. Use 0 to return only basic info for every local session. Has no effect on remote entries (which always carry their full shape). + [JsonPropertyName("metadataLimit")] + public long? MetadataLimit { get; set; } - /// Factory run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Which session sources to include. Defaults to `local` for backward compatibility. + [JsonPropertyName("source")] + public SessionSource? Source { get; set; } + + /// Only meaningful when `source` includes remote. When true, propagates errors from the remote service instead of silently returning an empty remote list. Defaults to false. + [JsonPropertyName("throwOnError")] + public bool? ThrowOnError { get; set; } } -/// The run stopped because its usage accounting could not be completed. -/// The factory_accounting_incomplete variant of . +/// Persisted local session metadata, including identifiers, timestamps, summary/name, client, context, detached state, and task ID. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class FactoryRunFailureFactoryAccountingIncomplete : FactoryRunFailure +public sealed class LocalSessionMetadataValue { - /// - [JsonIgnore] - public override string Type => "factory_accounting_incomplete"; + /// Runtime client name that created/last resumed this session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Confirmed usage in nano-AIU, representing the floor of what the run spent. - [JsonPropertyName("drainedNanoAiu")] - public required long DrainedNanoAiu { get; set; } + /// Pre-resolved working-directory context for session startup. + [JsonPropertyName("context")] + public SessionContext? Context { get; set; } - /// Factory run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// True for detached maintenance sessions that should be hidden from normal resume lists. + [JsonPropertyName("isDetached")] + public bool? IsDetached { get; set; } + + /// Always false for local sessions. + [JsonPropertyName("isRemote")] + public bool IsRemote { get; set; } + + /// GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control. + [JsonPropertyName("mcTaskId")] + public string? McTaskId { get; set; } + + /// Last-modified time of the session's persisted state, as ISO 8601. + [JsonPropertyName("modifiedTime")] + public string ModifiedTime { get; set; } = string.Empty; + + /// Optional human-friendly name set via /rename. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Stable session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Session creation time as an ISO 8601 timestamp. + [JsonPropertyName("startTime")] + public string StartTime { get; set; } = string.Empty; + + /// Short summary of the session, when one has been derived. + [JsonPropertyName("summary")] + public string? Summary { get; set; } } -/// The extension that owns the factory 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 factory_provider_disconnected variant of . +/// Persisted local session metadata when the session exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class FactoryRunFailureFactoryProviderDisconnected : FactoryRunFailure +internal sealed class SessionsGetMetadataResult { - /// - [JsonIgnore] - public override string Type => "factory_provider_disconnected"; + /// Local session metadata, omitted when the session does not exist. + [JsonPropertyName("session")] + public LocalSessionMetadataValue? Session { get; set; } +} - /// Factory run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } +/// Session ID whose persisted metadata should be read. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetMetadataRequest +{ + /// Session ID to inspect. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Durable metadata describing who initiated a factory pause. -/// Polymorphic base type discriminated by type. +/// Client metadata outcome for one requested local session. +/// Polymorphic base type discriminated by status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", + TypeDiscriminatorPropertyName = "status", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(FactoryPauseInfoUser), "user")] -[JsonDerivedType(typeof(FactoryPauseInfoCheckpoint), "checkpoint")] -public partial class FactoryPauseInfo +[JsonDerivedType(typeof(SessionsClientMetadataEntryOk), "ok")] +[JsonDerivedType(typeof(SessionsClientMetadataEntryNotFound), "notFound")] +[JsonDerivedType(typeof(SessionsClientMetadataEntryCorrupt), "corrupt")] +[JsonDerivedType(typeof(SessionsClientMetadataEntryUnsupportedVersion), "unsupportedVersion")] +[JsonDerivedType(typeof(SessionsClientMetadataEntryUnavailable), "unavailable")] +public partial class SessionsClientMetadataEntry { /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + [JsonPropertyName("status")] + public virtual string Status { get; set; } = string.Empty; } -/// The user variant of . +/// The ok variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class FactoryPauseInfoUser : FactoryPauseInfo +public partial class SessionsClientMetadataEntryOk : SessionsClientMetadataEntry { /// [JsonIgnore] - public override string Type => "user"; + public override string Status => "ok"; + + /// Validated client metadata, possibly empty or projected to requested keys. + [JsonPropertyName("metadata")] + public required IDictionary Metadata { get; set; } + + /// Requested session ID. + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } } -/// The checkpoint variant of . +/// The notFound variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class FactoryPauseInfoCheckpoint : FactoryPauseInfo +public partial class SessionsClientMetadataEntryNotFound : SessionsClientMetadataEntry { /// [JsonIgnore] - public override string Type => "checkpoint"; + public override string Status => "notFound"; - /// Stable author-defined checkpoint key that initiated the pause. - [JsonPropertyName("key")] - public required string Key { get; set; } + /// Requested session ID. + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } } -/// Complete current or terminal factory run envelope. +/// The corrupt variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryRunResult +public partial class SessionsClientMetadataEntryCorrupt : SessionsClientMetadataEntry { - /// 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; } + /// + [JsonIgnore] + public override string Status => "corrupt"; - /// Error message for an errored run. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Requested session ID. + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } +} - /// Machine-readable failure details for a halted or errored run. - [JsonPropertyName("failure")] - public FactoryRunFailure? Failure { get; set; } - - /// Structured pause initiator metadata for a paused attempt. - [JsonPropertyName("pauseInfo")] - public FactoryPauseInfo? PauseInfo { get; set; } +/// The unsupportedVersion variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SessionsClientMetadataEntryUnsupportedVersion : SessionsClientMetadataEntry +{ + /// + [JsonIgnore] + public override string Status => "unsupportedVersion"; - /// Reason for a halted or cancelled run. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Requested session ID. + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } +} - /// Completed factory result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } +/// The unavailable variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SessionsClientMetadataEntryUnavailable : SessionsClientMetadataEntry +{ + /// + [JsonIgnore] + public override string Status => "unavailable"; - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Filesystem or provider error code. Clients should not assume every provider uses operating-system error codes. + [JsonPropertyName("code")] + public required string Code { get; set; } - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - [JsonPropertyName("snapshot")] - public JsonElement? Snapshot { get; set; } + /// Human-readable diagnostic message. Not stable for programmatic matching. + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Current or terminal factory run status. - [JsonPropertyName("status")] - public FactoryRunStatus Status { get; set; } + /// Requested session ID. + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } } -/// Wire-only per-invocation factory resource ceiling overrides. +/// Bounded batch request for client-owned metadata from persisted local sessions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryRunLimits +internal sealed class SessionsGetClientMetadataRequest { - /// Maximum AI credits consumed by factory subagents and their descendants. The post-paid ceiling is soft: parallel turns can settle beyond it before the run stops. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; set; } - - /// Maximum number of factory subagents that may run concurrently. - [JsonPropertyName("maxConcurrentSubagents")] - public long? MaxConcurrentSubagents { get; set; } - - /// Maximum total number of factory subagents that may be admitted. - [JsonPropertyName("maxTotalSubagents")] - public long? MaxTotalSubagents { get; set; } + /// Case-sensitive keys to project from each valid bag. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. Omit to return every entry. + [JsonPropertyName("keys")] + public IList? Keys { 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; } + /// Session IDs to inspect. Results preserve this order. + [JsonPropertyName("sessionIds")] + public IList SessionIds { get => field ??= []; set; } } -/// Options controlling factory invocation. +/// Batch of session events returned by a read, with cursor and continuation metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RunOptions +public sealed class EventsReadResult { - /// Per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public FactoryRunLimits? Limits { get; set; } + /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). + [JsonPropertyName("cursor")] + public string Cursor { get; set; } = string.Empty; - /// Whether to emit factory phase names to the session transcript. - [JsonPropertyName("logPhaseNames")] - public bool? LogPhaseNames { get; set; } + /// Cursor status: 'ok' means the cursor was applied successfully. For session.eventLog.read, 'expired' means the cursor referred to an event that no longer exists in active history and the read fell back to a boundary of the remaining history: the beginning for a forward read or the newest window for a backward read. That fallback may overlap already rendered events, so active-session consumers should reset, rebase, or deduplicate before continuing. sessions.readPersistedEvents has stricter snapshot semantics: 'expired' returns an empty terminal page and never switches to a replacement journal generation. Other persisted-read I/O failures are RPC errors with diagnostics, not cursor expiry. + [JsonPropertyName("cursorStatus")] + public EventsCursorStatus CursorStatus { get; set; } - /// Whether to notify the originating session when the factory completes. - [JsonPropertyName("notifyOnComplete")] - public bool? NotifyOnComplete { get; set; } + /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. + [JsonPropertyName("events")] + public IList Events { get => field ??= []; set; } - /// Run identifier whose journal and progress should seed this resumed run. - [JsonPropertyName("resumeFromRunId")] - public string? ResumeFromRunId { get; set; } + /// True when more events are available in the read's direction. For a backward read, true means older persisted events remain before the returned window. A persisted-event page may contain fewer than `max` events because of its byte budget while still reporting hasMore true; continue according to this flag rather than the event count. + [JsonPropertyName("hasMore")] + public bool HasMore { get; set; } } -/// Parameters for invoking a registered factory. +/// Pagination options for reading an inactive or active local session's persisted event journal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryRunRequest +internal sealed class SessionsReadPersistedEventsRequest { - /// Factory input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + /// Opaque, process-local, single-use cursor returned by the previous persisted-event read. Omit on the first call and issue continuations sequentially; reusing the same cursor returns an expired terminal page. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Registered factory name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. This selects the initial read only; a continuation always uses the direction bound into its cursor. + [JsonPropertyName("direction")] + public EventsReadDirection? Direction { get; set; } - /// Factory invocation options. - [JsonPropertyName("options")] - public RunOptions? Options { get; set; } + /// Maximum number of events to return in this batch (1–1000, default 200). Pages may contain fewer events to keep the serialized event array within a soft 1 MiB budget including resolved binary assets; one oversized event is returned alone to guarantee progress. + [JsonPropertyName("max")] + public long? Max { get; set; } - /// Target session identifier. + /// Session ID whose persisted event journal should be read. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Resolved persisted factory identity and resumed run envelope. +/// Recent local session IDs that contain user-visible history. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryResumeResult +internal sealed class SessionsListNonEmptySessionIdsResult { - /// Persisted factory name resolved for the resumed run. - [JsonPropertyName("factoryName")] - public string FactoryName { get; set; } = string.Empty; - - /// Terminal resumed run envelope. - [JsonPropertyName("run")] - public FactoryRunResult Run { get => field ??= new(); set; } + /// Session IDs ordered newest-first. + [JsonPropertyName("sessionIds")] + public IList SessionIds { get => field ??= []; set; } } -/// Parameters for resuming a factory run from its persisted identity. +/// Limit for non-empty local session IDs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryResumeRequest +internal sealed class SessionsListNonEmptySessionIdsRequest { - /// Optional per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public FactoryRunLimits? Limits { get; set; } - - /// Whether to emit factory phase names to the session transcript. - [JsonPropertyName("logPhaseNames")] - public bool? LogPhaseNames { get; set; } + /// Maximum number of session IDs to return. + [JsonPropertyName("limit")] + public long? Limit { get; set; } +} - /// Whether to notify the originating session when the factory completes. - [JsonPropertyName("notifyOnComplete")] - public bool? NotifyOnComplete { get; set; } +/// ID of the local session bound to the given GitHub task, or omitted when none. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionsFindByTaskIDResult +{ + /// Omitted when no local session is bound to that GitHub task. + [JsonPropertyName("sessionId")] + public string? SessionId { get; set; } +} - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +/// GitHub task ID to look up. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsFindByTaskIDRequest +{ + /// GitHub task ID to look up. + [JsonPropertyName("taskId")] + public string TaskId { get; set; } = string.Empty; +} - /// Target session identifier. +/// Session ID matching the prefix, omitted when no unique match exists. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionsFindByPrefixResult +{ + /// Omitted when no unique session matches the prefix (no match or ambiguous). [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + public string? SessionId { get; set; } } -/// Options for an internal tool-originated factory invocation. +/// UUID prefix to resolve to a unique session ID. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryToolRunOptions +internal sealed class SessionsFindByPrefixRequest { - /// Per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public FactoryRunLimits? Limits { get; set; } - - /// Run identifier whose journal and progress should seed this resumed run. - [JsonPropertyName("resumeFromRunId")] - public string? ResumeFromRunId { get; set; } + /// UUID prefix (>=7 hex chars, <36 chars). Returns the unique session ID, or undefined when there is no match or the prefix matches multiple sessions. + [JsonPropertyName("prefix")] + public string Prefix { get; set; } = string.Empty; } -/// Internal parameters for invoking a registered factory from a tool. +/// Most-relevant session ID for the supplied context, or omitted when no sessions exist. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryToolRunRequest +public sealed class SessionsGetLastForContextResult { - /// Factory input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + /// Most-relevant session ID for the supplied context, or omitted when no sessions exist. + [JsonPropertyName("sessionId")] + public string? SessionId { get; set; } +} - /// Registered factory name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// Optional working-directory context used to score session relevance. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetLastForContextRequest +{ + /// Optional working-directory context used to score session relevance. When omitted the most-recently-modified session wins. + [JsonPropertyName("context")] + public SessionContext? Context { get; set; } +} - /// Tool-originated factory invocation options. - [JsonPropertyName("options")] - public FactoryToolRunOptions? Options { get; set; } +/// Absolute path to the session's events.jsonl file on disk. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetEventFilePathResult +{ + /// Absolute path to the session's events.jsonl file. + [JsonPropertyName("filePath")] + public string FilePath { get; set; } = string.Empty; +} - /// Target session identifier. +/// Session ID whose event-log file path to compute. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetEventFilePathRequest +{ + /// Session ID whose event-log file path to compute. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// Opaque identifier of the originating tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } +/// Map of sessionId -> on-disk size in bytes for each session's workspace directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSizes +{ + /// Map of sessionId -> on-disk size in bytes for the session's workspace directory. + [JsonPropertyName("sizes")] + public IDictionary Sizes { get => field ??= new Dictionary(); set; } } -/// Internal parameters for resuming a factory run from a tool. +/// Session IDs from the input set that are currently in use by another process. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryToolResumeRequest +public sealed class SessionsCheckInUseResult { - /// Optional per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public FactoryRunLimits? Limits { get; set; } + /// Session IDs from the input set that are currently held by another running process via an alive lock file. + [JsonPropertyName("inUse")] + public IList InUse { get => field ??= []; set; } +} - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +/// Session IDs to test for live in-use locks. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsCheckInUseRequest +{ + /// Session IDs to test for live in-use locks. + [JsonPropertyName("sessionIds")] + public IList SessionIds { get => field ??= []; set; } +} - /// Target session identifier. +/// The session's persisted remote-steerable flag, or omitted when no value has been persisted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetPersistedRemoteSteerableResult +{ + /// The session's persisted remote-steerable flag if recorded; omitted when no value has been persisted. + [JsonPropertyName("remoteSteerable")] + public bool? RemoteSteerable { get; set; } +} + +/// Session ID to look up the persisted remote-steerable flag for. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetPersistedRemoteSteerableRequest +{ + /// Session ID to look up the persisted remote-steerable flag for. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Opaque identifier of the originating tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } } -/// Parameters for retrieving a factory run. +/// Closes a session: emits shutdown, flushes pending events to disk, releases the in-use lock, disposes the active session. Idempotent: succeeds even if the session is not currently active. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryGetRunRequest +public sealed class SessionsCloseResult { - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +} - /// Target session identifier. +/// Session ID to close. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsCloseRequest +{ + /// Session ID to close. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Declared or approved factory resource ceilings. +/// Map of sessionId -> bytes freed by removing the session's workspace directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryDeclaredLimits +public sealed class SessionBulkDeleteResult { - /// 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; } + /// Map of sessionId -> bytes freed by removing the session's workspace directory. Sessions whose deletion failed are omitted from this map (failures are logged on the server but not surfaced per-id; check the map for absent IDs to detect them). + [JsonPropertyName("freedBytes")] + public IDictionary FreedBytes { get => field ??= new Dictionary(); set; } } -/// Durable factory resource consumption. +/// Session IDs to close, deactivate, and delete from disk. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryRunConsumed +internal sealed class SessionsBulkDeleteRequest { - /// 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; } - - /// Total subagents spawned by the run. - [JsonPropertyName("subagents")] - public long Subagents { get; set; } + /// Session IDs to close, deactivate, and delete from disk. + [JsonPropertyName("sessionIds")] + public IList SessionIds { get => field ??= []; set; } } -/// Current factory phase identity. +/// Session ID to delete from disk. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryCurrentPhase +internal sealed class SessionsDeleteRequest { - /// Current phase identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Session ID to delete. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Zero-based declared phase ordinal, or null for an undeclared phase. - [JsonPropertyName("ordinal")] - public long? Ordinal { get; set; } + /// Internal resolved session directory path to delete. + [JsonPropertyName("sessionPath")] + public string? SessionPath { get; set; } } -/// Prompt-safe terminal factory outcome. +/// Outcome of the prune operation: deleted IDs, dry-run candidates, skipped IDs, total bytes freed, and the dry-run flag. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryRunTerminal +public sealed class SessionPruneResult { - /// Human-readable terminal error. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Session IDs that would be deleted in dry-run mode (always empty otherwise). + [JsonPropertyName("candidates")] + public IList Candidates { get => field ??= []; set; } - /// Machine-readable terminal failure. - [JsonPropertyName("failure")] - public FactoryRunFailure? Failure { get; set; } + /// Session IDs that were deleted (always empty in dry-run mode). + [JsonPropertyName("deleted")] + public IList Deleted { get => field ??= []; set; } - /// Pause initiator metadata, or null when the run did not pause. - [JsonPropertyName("pauseInfo")] - public FactoryPauseInfo? PauseInfo { get; set; } + /// True when no deletions were actually performed. + [JsonPropertyName("dryRun")] + public bool DryRun { get; set; } - /// Human-readable terminal reason. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Total bytes freed (actual when not dry-run, projected when dry-run). + [JsonPropertyName("freedBytes")] + public long FreedBytes { get; set; } - /// Prompt-safe preview of the completed result. - [JsonPropertyName("resultPreview")] - public string? ResultPreview { get; set; } + /// Session IDs that were skipped (e.g., named sessions). + [JsonPropertyName("skipped")] + public IList Skipped { get => field ??= []; set; } } -/// Durable factory run summary with read-time live overlays. +/// Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryRunSummary +internal sealed class SessionsPruneOldRequest { - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { get; set; } + /// When true, only report what would be deleted without performing any deletion. + [JsonPropertyName("dryRun")] + public bool? DryRun { get; set; } - /// Approved effective resource ceilings, or null until approved. - [JsonPropertyName("approved")] - public FactoryDeclaredLimits? Approved { get; set; } + /// Session IDs that should never be considered for pruning. + [JsonPropertyName("excludeSessionIds")] + public IList? ExcludeSessionIds { get; set; } - /// Whether the durable run state currently passes runtime resume eligibility checks. - [JsonPropertyName("canResume")] - public bool CanResume { get; set; } + /// When true, named sessions (set via /rename) are also eligible for pruning. + [JsonPropertyName("includeNamed")] + public bool? IncludeNamed { get; set; } - /// Epoch milliseconds when the run completed, or null while nonterminal. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } + /// Delete sessions whose modifiedTime is at least this many days old. + [JsonPropertyName("olderThanDays")] + public long OlderThanDays { get; set; } +} - /// Durable resource consumption. - [JsonPropertyName("consumed")] - public FactoryRunConsumed Consumed { get => field ??= new(); set; } +/// Flush a session's pending events to disk. No-op when no writer exists for the session (e.g., already closed). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionsSaveResult +{ +} - /// Epoch milliseconds when the run was created. - [JsonPropertyName("createdAt")] - public long CreatedAt { get; set; } +/// Session ID whose pending events should be flushed to disk. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsSaveRequest +{ + /// Session ID whose pending events should be flushed to disk. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Current phase identity, or null before any phase is entered. - [JsonPropertyName("currentPhase")] - public FactoryCurrentPhase? CurrentPhase { get; set; } +/// Release the in-use lock held by this process for the given session. No-op when this process does not currently hold a lock for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionsReleaseLockResult +{ +} - /// Resource ceilings declared by the factory. - [JsonPropertyName("declaredLimits")] - public FactoryDeclaredLimits DeclaredLimits { get => field ??= new(); set; } +/// Session ID whose in-use lock should be released. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsReleaseLockRequest +{ + /// Session ID whose in-use lock should be released. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Number of phases declared by the factory. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } +/// The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionEnrichMetadataResult +{ + /// Enriched records, with summary and context backfilled. Sessions confirmed empty and unnamed may be omitted. + [JsonPropertyName("sessions")] + public IList Sessions { get => field ??= []; set; } +} - /// Human-readable factory description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; +/// Session metadata records to enrich with summary and context information. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsEnrichMetadataRequest +{ + /// Session metadata records to enrich. Records that already have summary and context are returned unchanged. + [JsonPropertyName("sessions")] + public IList Sessions { get => field ??= []; set; } +} - /// Registered factory name. - [JsonPropertyName("factoryName")] - public string FactoryName { get; set; } = string.Empty; +/// The workspace record that was written. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsCreateWorkspaceResult +{ + /// The created workspace record, as JSON. + [JsonPropertyName("workspaceJson")] + public string WorkspaceJson { get; set; } = string.Empty; +} - /// Number of direct factory agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } +/// A working-directory context together with the client that produced it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionWorkingDirectoryContextWithClient +{ + /// Merge-base commit SHA. + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } + /// Current git branch name. + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Name of the client that created the session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Current working directory path. + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Root directory of the git repository. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } - /// Current factory run status. - [JsonPropertyName("status")] - public FactoryRunStatus Status { get; set; } + /// Head commit of the current git branch. + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } - /// Terminal run outcome, or null while nonterminal. - [JsonPropertyName("terminal")] - public FactoryRunTerminal? Terminal { get; set; } + /// Hosting platform type of the repository. + [JsonPropertyName("hostType")] + public string? HostType { get; set; } - /// Total direct factory agents spawned across all attempts. - [JsonPropertyName("totalSpawnedAgentCount")] - public long TotalSpawnedAgentCount { get; set; } + /// Repository identifier derived from the git remote URL. + [JsonPropertyName("repository")] + public string? Repository { get; set; } - /// Epoch milliseconds when the durable run was last updated. - [JsonPropertyName("updatedAt")] - public long UpdatedAt { get; set; } + /// Raw host string from the git remote URL. + [JsonPropertyName("repositoryHost")] + public string? RepositoryHost { get; set; } } -/// A page of factory runs in durable creation order. +/// Identity, state location and starting context for a workspace record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryListRunsResult +internal sealed class SessionsCreateWorkspaceRequest { - /// Whether terminal runs newer than this page exist. - [JsonPropertyName("hasMoreNewer")] - public bool? HasMoreNewer { get; set; } + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + [JsonPropertyName("context")] + public SessionWorkingDirectoryContextWithClient? Context { get; set; } - /// Newest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("newestSeq")] - public long? NewestSeq { get; set; } + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + [JsonPropertyName("convention")] + public string Convention { get; set; } = string.Empty; - /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("oldestSeq")] - public long? OldestSeq { get; set; } + /// User-supplied display name for the workspace. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Number of terminal runs older than this page. - [JsonPropertyName("omittedOlder")] - public long? OmittedOlder { get; set; } + /// Session ID the workspace record belongs to. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Factory run summaries in durable creation order. - [JsonPropertyName("runs")] - public IList Runs { get => field ??= []; set; } + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + [JsonPropertyName("sessionStatePath")] + public string SessionStatePath { get; set; } = string.Empty; } -/// Parameters for paging factory runs. +/// The workspace record on disk, omitted when the session has none. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryListRunsRequest +internal sealed class SessionsLoadWorkspaceResult { - /// Exclusive forward cursor. - [JsonPropertyName("afterSeq")] - public long? AfterSeq { get; set; } - - /// Exclusive backward cursor. - [JsonPropertyName("beforeSeq")] - public long? BeforeSeq { get; set; } - - /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. - [JsonPropertyName("limit")] - public int? Limit { get; set; } + /// The workspace record, as JSON. Omitted when the record does not exist. + [JsonPropertyName("workspaceJson")] + public string? WorkspaceJson { get; set; } +} - /// Target session identifier. +/// Where the session's state lives, as a root directory and the session ID under it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsLoadWorkspaceRequest +{ + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Root directory every session's state directory sits under. + [JsonPropertyName("sessionsHome")] + public string SessionsHome { get; set; } = string.Empty; } -/// Prompt-safe durable identity and live status for a direct factory agent. +/// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryAgentSummary +internal sealed class SessionsUpdateWorkspaceFieldsResult { - /// 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; } +/// Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsUpdateWorkspaceFieldsRequest +{ + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + [JsonPropertyName("fieldsJson")] + public string FieldsJson { get; set; } = string.Empty; - /// Stable direct-agent identifier. - [JsonPropertyName("agentId")] - public string AgentId { get; set; } = string.Empty; + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Registered agent type. - [JsonPropertyName("agentType")] - public string AgentType { get; set; } = string.Empty; - - /// Epoch milliseconds when the agent completed. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } - - /// Friendly, non-unique name intended for display. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } - - /// Friendly, non-unique name intended for display. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; - - /// Phase identifier active when the agent was launched, or null. - [JsonPropertyName("phaseId")] - public string? PhaseId { get; set; } - - /// Model requested when the agent was launched. - [JsonPropertyName("requestedModel")] - public string? RequestedModel { get; set; } - - /// Concrete model resolved for the agent. - [JsonPropertyName("resolvedModel")] - public string? ResolvedModel { get; set; } - - /// Owning factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Epoch milliseconds when the agent started. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } - - /// Current durable or live agent status. - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - - /// Tool-call identifier that launched the agent. - [JsonPropertyName("toolCallId")] - public string ToolCallId { get; set; } = string.Empty; + /// Root directory every session's state directory sits under. + [JsonPropertyName("sessionsHome")] + public string SessionsHome { get; set; } = string.Empty; } -/// Durable lifecycle and timing for one factory phase. +/// Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryPhaseObservation +public sealed class SessionsReloadPluginHooksResult { - /// 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; } - - /// 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; } = string.Empty; - - /// Most recent run attempt that entered this phase, or `0` if the phase has never been entered. - [JsonPropertyName("lastEnteredRunAttempt")] - public long LastEnteredRunAttempt { get; set; } - - /// 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 FactoryPhaseStatus Status { get; set; } - - /// Human-readable phase title. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; - - /// Total direct agents associated with this phase. - [JsonPropertyName("totalAgentCount")] - public long TotalAgentCount { get; set; } } -/// One durable factory progress record. +/// Active session ID and an optional flag for deferring repo-level hooks until folder trust. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryProgressLine +internal sealed class SessionsReloadPluginHooksRequest { - /// Resume attempt that emitted this record. - [JsonPropertyName("attempt")] - public long Attempt { get; set; } - - /// Progress record kind. - [JsonPropertyName("kind")] - public FactoryLogLineKind Kind { get; set; } - - /// 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; } + /// When true, skip repo-level hooks. Use before folder trust is confirmed; loadDeferredRepoHooks loads them post-trust. + [JsonPropertyName("deferRepoHooks")] + public bool? DeferRepoHooks { get; set; } - /// Prompt-safe progress text. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// Active session ID to reload hooks for. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// A bidirectional page of factory progress. +/// Queued repo-level startup prompts and the total hook command count after loading. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryProgressPage +public sealed class SessionLoadDeferredRepoHooksResult { - /// 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; } - - /// 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; } + /// Total hook command count (user + plugin + repo) loaded for the session by this call. Captured atomically with startupPrompts so callers don't need to read a separate counter. + [JsonPropertyName("hookCount")] + public long HookCount { get; set; } - /// Run revision reflected by this page. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Repo-level startup prompts queued from repo hook configs. Empty on resume, when no repo configs were pending, or when disableAllHooks is set. + [JsonPropertyName("startupPrompts")] + public IList StartupPrompts { get => field ??= []; set; } } -/// Full factory run observability detail. +/// Active session ID whose deferred repo-level hooks should be loaded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryRunDetail +internal sealed class SessionsLoadDeferredRepoHooksRequest { - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { get; set; } - - /// Durable identities and live statuses for direct factory agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } + /// Active session ID whose deferred repo-level hooks should be loaded. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Approved effective resource ceilings, or null until approved. - [JsonPropertyName("approved")] - public FactoryDeclaredLimits? Approved { get; set; } +/// Replace the manager-wide additional plugins. New session creations and subsequent hook reloads see the new set; already-running sessions keep their existing hook installation until the next reload. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionsSetAdditionalPluginsResult +{ +} - /// Whether the durable run state currently passes runtime resume eligibility checks. - [JsonPropertyName("canResume")] - public bool CanResume { get; set; } +/// Manager-wide additional plugins to register; replaces any previously-configured set. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsSetAdditionalPluginsRequest +{ + /// Manager-wide additional plugins to register. Replaces any previously-configured set. Pass an empty array to clear. + [JsonPropertyName("plugins")] + public IList Plugins { get => field ??= []; set; } +} - /// Epoch milliseconds when the run completed, or null while nonterminal. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } +/// Dynamic-context board entry count, when available. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetBoardEntryCountResult +{ + /// Board entry count, when available. + [JsonPropertyName("count")] + public long? Count { get; set; } +} - /// Durable resource consumption. - [JsonPropertyName("consumed")] - public FactoryRunConsumed Consumed { get => field ??= new(); set; } +/// Session ID whose board entry count should be returned. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsGetBoardEntryCountRequest +{ + /// Session ID whose board entry count should be returned. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Epoch milliseconds when the run was created. - [JsonPropertyName("createdAt")] - public long CreatedAt { get; set; } +/// State of the runtime-managed remote-control singleton. +/// Polymorphic base type discriminated by state. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "state", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(RemoteControlStatusOff), "off")] +[JsonDerivedType(typeof(RemoteControlStatusConnecting), "connecting")] +[JsonDerivedType(typeof(RemoteControlStatusActive), "active")] +[JsonDerivedType(typeof(RemoteControlStatusError), "error")] +public partial class RemoteControlStatus +{ + /// The type discriminator. + [JsonPropertyName("state")] + public virtual string State { get; set; } = string.Empty; +} - /// Current phase identity, or null before any phase is entered. - [JsonPropertyName("currentPhase")] - public FactoryCurrentPhase? CurrentPhase { get; set; } - /// Resource ceilings declared by the factory. - [JsonPropertyName("declaredLimits")] - public FactoryDeclaredLimits DeclaredLimits { get => field ??= new(); set; } +/// Remote control is not connected. +/// The off variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class RemoteControlStatusOff : RemoteControlStatus +{ + /// + [JsonIgnore] + public override string State => "off"; +} - /// Number of phases declared by the factory. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } +/// Remote control is in the middle of initial setup. +/// The connecting variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class RemoteControlStatusConnecting : RemoteControlStatus +{ + /// + [JsonIgnore] + public override string State => "connecting"; - /// Human-readable factory description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Session id the connection is attaching to. + [JsonPropertyName("attachedSessionId")] + public required string AttachedSessionId { get; set; } +} - /// Registered factory name. - [JsonPropertyName("factoryName")] - public string FactoryName { get; set; } = string.Empty; +/// Remote control is connected to a local session. +/// The active variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class RemoteControlStatusActive : RemoteControlStatus +{ + /// + [JsonIgnore] + public override string State => "active"; - /// Number of direct factory agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } + /// Session id remote control is pointed at. + [JsonPropertyName("attachedSessionId")] + public required string AttachedSessionId { get; set; } - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } + /// True while a read-only/session-sync export is deferred, awaiting the first `user.message` before its MC session exists. Marked internal: this field is excluded from the public SDK surface and is populated only on the CLI in-process path. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("awaitingFirstMessage")] + internal bool? AwaitingFirstMessage { get; set; } - /// Lifecycle and timing observations for each factory phase. - [JsonPropertyName("phases")] - public IList Phases { get => field ??= []; set; } + /// MC frontend URL for this session, when known. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("frontendUrl")] + public string? FrontendUrl { get; set; } - /// Bidirectional page of durable factory progress. - [JsonPropertyName("progress")] - public FactoryProgressPage Progress { get => field ??= new(); set; } + /// Whether the MC session may steer this session. + [JsonPropertyName("isSteerable")] + public required bool IsSteerable { get; set; } +} - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } +/// The last setup attempt failed. The singleton is otherwise off. +/// The error variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class RemoteControlStatusError : RemoteControlStatus +{ + /// + [JsonIgnore] + public override string State => "error"; - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Session id the failing setup attempt targeted, when known. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("attachedSessionId")] + public string? AttachedSessionId { get; set; } - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Human-readable error message from the last setup attempt. + [JsonPropertyName("error")] + public required string Error { get; set; } +} - /// Current factory run status. +/// Wrapper for the singleton's current status. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class RemoteControlStatusResult +{ + /// State of the runtime-managed remote-control singleton. [JsonPropertyName("status")] - public FactoryRunStatus Status { get; set; } - - /// Terminal run outcome, or null while nonterminal. - [JsonPropertyName("terminal")] - public FactoryRunTerminal? Terminal { get; set; } + public RemoteControlStatus Status { get => field ??= new(); set; } +} - /// Total direct factory agents spawned across all attempts. - [JsonPropertyName("totalSpawnedAgentCount")] - public long TotalSpawnedAgentCount { get; set; } +/// Reattach to an existing MC session without creating a new one. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class RemoteControlConfigExistingMcSession +{ + /// Existing MC session ID to reattach to. + [JsonPropertyName("mcSessionId")] + public string McSessionId { get; set; } = string.Empty; - /// Epoch milliseconds when the durable run was last updated. - [JsonPropertyName("updatedAt")] - public long UpdatedAt { get; set; } + /// Existing MC task ID for the reattached session. + [JsonPropertyName("mcTaskId")] + public string McTaskId { get; set; } = string.Empty; } -/// Parameters for paging factory progress. +/// Configuration for the runtime-managed remote-control singleton. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryGetRunProgressRequest +public sealed class RemoteControlConfig { - /// Exclusive forward cursor. - [JsonPropertyName("afterSeq")] - public long? AfterSeq { get; set; } + /// Reattach to an existing MC session without creating a new one. + [JsonPropertyName("existingMcSession")] + public RemoteControlConfigExistingMcSession? ExistingMcSession { get; set; } - /// Exclusive backward cursor. - [JsonPropertyName("beforeSeq")] - public long? BeforeSeq { get; set; } + /// Whether the user explicitly requested remote (vs. implicit session-sync). Controls warning surfacing for missing-repo cases. + [JsonPropertyName("explicit")] + public bool Explicit { get; set; } - /// Maximum records to return. Defaults to 200 and is capped at 500. - [JsonPropertyName("limit")] - public int? Limit { get; set; } + /// Whether remote export should be enabled. + [JsonPropertyName("remote")] + public bool Remote { get; set; } - /// Optional phase identifier used to scope records and cursors. - [JsonPropertyName("phaseId")] - public string? PhaseId { get; set; } + /// When true, suppresses timeline messages on successful setup. + [JsonPropertyName("silent")] + public bool Silent { get; set; } - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Whether the MC session may steer the local session (write mode). + [JsonPropertyName("steerable")] + public bool Steerable { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Existing Mission Control task ID to attach the exported session to. + [JsonPropertyName("taskId")] + public string? TaskId { get; set; } } -/// Parameters for cancelling a factory run. +/// Parameters for attaching the remote-control singleton to a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryCancelRequest +internal sealed class SessionsStartRemoteControlRequest { - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Configuration for the runtime-managed remote-control singleton. + [JsonPropertyName("config")] + public RemoteControlConfig Config { get => field ??= new(); set; } - /// Target session identifier. + /// Local session id to attach remote control to. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Parameters for pausing a running factory. +/// Outcome of a transferRemoteControl call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryPauseRequest +public sealed class RemoteControlTransferResult { - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// State of the runtime-managed remote-control singleton. + [JsonPropertyName("status")] + public RemoteControlStatus Status { get => field ??= new(); set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the rebinding actually happened. + [JsonPropertyName("transferred")] + public bool Transferred { get; set; } } -/// RPC data type for SessionFactoryPauseAtCheckpoint operations. +/// Parameters for atomically rebinding the remote-control singleton. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionFactoryPauseAtCheckpointResult +internal sealed class SessionsTransferRemoteControlRequest { - /// Whether this execution attempt must pause or may continue. - [JsonPropertyName("action")] - public FactoryPauseCheckpointAction Action { get; set; } + /// When provided, the transfer is rejected unless the singleton currently points at this session id (compare-and-swap semantics to avoid clobbering newer state). + [JsonPropertyName("expectedFromSessionId")] + public string? ExpectedFromSessionId { get; set; } + + /// Local session id to point remote control at. + [JsonPropertyName("toSessionId")] + public string ToSessionId { get; set; } = string.Empty; } -/// Parameters for an owned durable pause checkpoint. +/// Patch for the singleton's steering state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryPauseCheckpointRequest +internal sealed class SessionsSetRemoteControlSteeringRequest { - /// 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; - - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Target steering state. Today only `true` is actionable on the underlying exporter; `false` is reserved for future use. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } } -/// Acknowledgement that a factory request was accepted. +/// Outcome of a stopRemoteControl call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryAckResult +public sealed class RemoteControlStopResult { + /// State of the runtime-managed remote-control singleton. + [JsonPropertyName("status")] + public RemoteControlStatus Status { get => field ??= new(); set; } + + /// Whether the singleton was actually torn down by this call. + [JsonPropertyName("stopped")] + public bool Stopped { get; set; } } -/// One ordered factory progress line. +/// RPC data type for SessionsStopRemoteControl operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryLogLine +internal sealed class SessionsStopRemoteControlRequest { - /// Progress line kind. - [JsonPropertyName("kind")] - public FactoryLogLineKind Kind { get; set; } - - /// Monotonic sequence number within the factory run. - [JsonPropertyName("seq")] - public long Seq { get; set; } + /// When provided, the stop is rejected unless the singleton currently points at this session id (compare-and-swap semantics). + [JsonPropertyName("expectedSessionId")] + public string? ExpectedSessionId { get; set; } - /// Progress text. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// When true, the singleton is unconditionally torn down regardless of `expectedSessionId`. Use during shutdown or explicit `/remote off`. + [JsonPropertyName("force")] + public bool? Force { get; set; } } -/// Parameters for recording factory progress. +/// Params to attach or detach an in-process ExtensionController delegate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryLogRequest +internal sealed class ConfigureSessionExtensionsParams { - /// Opaque token identifying the current factory execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; - - /// Ordered progress lines to append. - [JsonPropertyName("lines")] - public IList Lines { get => field ??= []; set; } - - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Target session identifier. + /// Session to attach the extension controller delegate to. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of one factory-scoped subagent call. +/// Outcome of an agentRegistry.spawn call. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryAgentResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AgentRegistrySpawnResultSpawned), "spawned")] +[JsonDerivedType(typeof(AgentRegistrySpawnResultSpawnError), "spawn-error")] +[JsonDerivedType(typeof(AgentRegistrySpawnResultRegistryTimeout), "registry-timeout")] +[JsonDerivedType(typeof(AgentRegistrySpawnResultValidationError), "validation-error")] +public partial class AgentRegistrySpawnResult { - /// Agent result, omitted when the agent produced no result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Options for one factory-scoped subagent call. + +/// Full registry entry for the spawned child. Lets the controller call `handleLiveTargetSelected(entry)` directly without re-reading the registry (avoids a TOCTOU window). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryAgentOptions +public sealed class AgentRegistryLiveTargetEntry { - /// Optional built-in or custom agent name whose definition configures the subagent. - [JsonPropertyName("agent")] - public string? Agent { get; set; } + /// Kind of attention required when status === "attention". Meaningful only when status === "attention". + [JsonPropertyName("attentionKind")] + public AgentRegistryLiveTargetEntryAttentionKind? AttentionKind { get; set; } - /// Optional context tier override for the subagent. - [JsonPropertyName("contextTier")] - public ContextTier? ContextTier { get; set; } + /// Git branch of the session (when known). + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// Optional label distinguishing otherwise identical memoized agent calls. - [JsonPropertyName("label")] - public string? Label { get; set; } + /// Copilot CLI version that wrote the entry. + [JsonPropertyName("copilotVersion")] + public string CopilotVersion { get; set; } = string.Empty; - /// Optional model identifier for the subagent. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Working directory of the session (when known). + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } - /// Optional reasoning effort override for the subagent. - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + /// Bind host for the entry's JSON-RPC server. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Optional JSON Schema for structured agent output. - [JsonPropertyName("schema")] - public JsonElement? Schema { get; set; } -} + /// Process kind tag for the registry entry. + [JsonPropertyName("kind")] + public AgentRegistryLiveTargetEntryKind Kind { get; set; } -/// Parameters for one factory-scoped subagent call. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryAgentRequest -{ - /// Opaque token identifying the current factory execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Wall-clock milliseconds since the watcher last observed this entry (heartbeat freshness). + [JsonPropertyName("lastSeenMs")] + public long LastSeenMs { get; set; } - /// Factory run identifier that owns the subagent. - [JsonPropertyName("factoryRunId")] - public string FactoryRunId { get; set; } = string.Empty; + /// How the most recent turn ended (clean vs aborted). Lets the renderer distinguish done from done_cancelled. + [JsonPropertyName("lastTerminalEvent")] + public AgentRegistryLiveTargetEntryLastTerminalEvent? LastTerminalEvent { get; set; } - /// Subagent execution options. - [JsonPropertyName("opts")] - public FactoryAgentOptions Opts { get => field ??= new(); set; } + /// Model identifier currently selected for the session. + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Prompt to send to the subagent. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Operating-system pid of the process owning this entry. + [JsonPropertyName("pid")] + public long Pid { get; set; } - /// Target session identifier. + /// TCP port the entry's JSON-RPC server is listening on. + [JsonPropertyName("port")] + public long Port { get; set; } + + /// Registry entry schema version (1 = ui-server, 2 = managed-server). + [JsonPropertyName("schemaVersion")] + public long SchemaVersion { get; set; } + + /// Session ID of the foreground session for this entry. [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + public string? SessionId { get; set; } -/// Result of reading a factory journal entry. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryJournalGetResult -{ - /// Whether the journal contained the requested key. - [JsonPropertyName("hit")] - public bool Hit { get; set; } + /// Friendly session name (when set). + [JsonPropertyName("sessionName")] + public string? SessionName { get; set; } - /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. - [JsonPropertyName("resultJson")] - public JsonElement? ResultJson { get; set; } + /// ISO 8601 timestamp captured at registration. + [JsonPropertyName("startedAt")] + public string StartedAt { get; set; } = string.Empty; + + /// Coarse lifecycle status of the foreground session. + [JsonPropertyName("status")] + public AgentRegistryLiveTargetEntryStatus? Status { get; set; } + + /// Monotonic per-publisher revision counter incremented on every status update. Lets watchers detect transient flips. + [JsonPropertyName("statusRevision")] + public long? StatusRevision { get; set; } + + /// Connection token (null when the target is unauthenticated). + [JsonInclude] + [JsonPropertyName("token")] + internal string? Token { get; set; } } -/// Parameters for reading a factory journal entry. +/// Canonical process-log discovery outcome; populated from spawnLiveTarget. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryJournalGetRequest +public sealed class AgentRegistryLogCapture { - /// Opaque token identifying the current factory execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Whether a canonical process log was discovered for this managed spawn. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Namespaced journal key. - [JsonPropertyName("key")] - public string Key { get; set; } = string.Empty; + /// Why no canonical process log could be opened for this managed spawn (set only when enabled is false). + [JsonPropertyName("openError")] + public string? OpenError { get; set; } - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Categorized reason no canonical process log could be opened (set only when enabled is false). + [JsonPropertyName("openErrorReason")] + public AgentRegistryLogCaptureOpenErrorReason? OpenErrorReason { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Absolute path to the managed spawn's process-<timestamp>-<pid>.log file (only set when enabled). + [JsonPropertyName("path")] + public string? Path { get; set; } } -/// Parameters for storing a factory journal entry. +/// Managed-server child was spawned and registered successfully. +/// The spawned variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FactoryJournalPutRequest +public partial class AgentRegistrySpawnResultSpawned : AgentRegistrySpawnResult { - /// Opaque token identifying the current factory 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 Kind => "spawned"; - /// JSON result to memoize. - [JsonPropertyName("resultJson")] - public JsonElement ResultJson { get; set; } + /// Full registry entry for the spawned child. Lets the controller call `handleLiveTargetSelected(entry)` directly without re-reading the registry (avoids a TOCTOU window). + [JsonPropertyName("entry")] + public required AgentRegistryLiveTargetEntry Entry { get; set; } - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// If the delegate attempted to send the initial prompt and failed, the categorized error message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("initialPromptError")] + public string? InitialPromptError { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether the delegate already sent the initial prompt. Always omitted in the current wiring: the controller sends the prompt post-attach via the standard LocalRpcSession.send path. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("initialPromptSent")] + public bool? InitialPromptSent { get; set; } -/// Machine-readable workflow run failure. -/// Polymorphic base type discriminated by type. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowLimitReached), "workflow_limit_reached")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowResumeDeclined), "workflow_resume_declined")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowDurableFailure), "workflow_durable_failure")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowAccountingIncomplete), "workflow_accounting_incomplete")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowProviderDisconnected), "workflow_provider_disconnected")] -public partial class WorkflowRunFailure -{ - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + /// Per-spawn log-capture outcome; populated from spawnLiveTarget. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("logCapture")] + public AgentRegistryLogCapture? LogCapture { get; set; } } - -/// The workflow_limit_reached variant of . +/// `child_process.spawn` itself failed before the child entered the registry. +/// The spawn-error variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowLimitReached : WorkflowRunFailure +public partial class AgentRegistrySpawnResultSpawnError : AgentRegistrySpawnResult { /// [JsonIgnore] - public override string Type => "workflow_limit_reached"; - - /// Resource ceiling that stopped the run. - [JsonPropertyName("kind")] - public required WorkflowRunFailureKind Kind { get; set; } - - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + public override string Kind => "spawn-error"; - /// Suggested larger ceiling when the runtime can derive one safely. + /// Underlying errno code (e.g. ENOENT, EACCES) when available. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("suggestedValue")] - public double? SuggestedValue { get; set; } + [JsonPropertyName("code")] + public string? Code { get; set; } - /// Approved effective ceiling that was reached. - [JsonPropertyName("value")] - public required double Value { get; set; } + /// Human-readable error message. + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// The workflow_resume_declined variant of . +/// Spawn succeeded but the child did not publish a matching managed-server entry within the timeout. +/// The registry-timeout variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowResumeDeclined : WorkflowRunFailure +public partial class AgentRegistrySpawnResultRegistryTimeout : AgentRegistrySpawnResult { /// [JsonIgnore] - public override string Type => "workflow_resume_declined"; + public override string Kind => "registry-timeout"; - /// Human-readable reason the resume did not proceed. - [JsonPropertyName("reason")] - public required string Reason { get; set; } + /// Process ID of the orphaned child (so the caller can offer 'kill the pid' guidance). + [JsonPropertyName("childPid")] + public required long ChildPid { get; set; } - /// Workflow run identifier whose changed limits were declined. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Per-spawn log-capture outcome; populated from spawnLiveTarget. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("logCapture")] + public AgentRegistryLogCapture? LogCapture { get; set; } } -/// The workflow_durable_failure variant of . +/// Synchronous pre-validation rejected the spawn request. +/// The validation-error variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowDurableFailure : WorkflowRunFailure +public partial class AgentRegistrySpawnResultValidationError : AgentRegistrySpawnResult { /// [JsonIgnore] - public override string Type => "workflow_durable_failure"; + public override string Kind => "validation-error"; - /// Stable failure code. - [JsonPropertyName("code")] - public required string Code { get; set; } + /// Which parameter field was invalid. Omitted when the rejection is not field-specific. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("field")] + public AgentRegistrySpawnValidationErrorField? Field { get; set; } - /// Execution-critical durable operation that failed. - [JsonPropertyName("operation")] - public required WorkflowDurableOperation Operation { get; set; } + /// Human-readable explanation; safe to surface in the UI banner. Never logged to unrestricted telemetry. + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Categorized reason for the rejection. Low-cardinality enum so telemetry can aggregate by reason without leaking raw paths or agent/model names. + [JsonPropertyName("reason")] + public required AgentRegistrySpawnValidationErrorReason Reason { get; set; } } -/// The run stopped because its usage accounting could not be completed. -/// The workflow_accounting_incomplete variant of . +/// Inputs to spawn a managed-server child via the controller's spawn delegate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowAccountingIncomplete : WorkflowRunFailure +internal sealed class AgentRegistrySpawnRequest { - /// - [JsonIgnore] - public override string Type => "workflow_accounting_incomplete"; + /// Custom or built-in agent name (e.g. 'explore'). When omitted, the child uses its own default. + [JsonPropertyName("agentName")] + public string? AgentName { get; set; } - /// Confirmed usage in nano-AIU, representing the floor of what the run spent. - [JsonPropertyName("drainedNanoAiu")] - public required long DrainedNanoAiu { get; set; } + /// Working directory for the spawned child (must be an existing directory). + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Optional first user message. Forwarded to the caller (the CLI's spawn wrapper sends it post-attach via the standard LocalRpcSession.send path). + [JsonPropertyName("initialPrompt")] + public string? InitialPrompt { get; set; } + + /// Model identifier to apply to the new session. + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// Friendly session name. Must satisfy validateSessionName: non-empty, no leading/trailing whitespace, <=100 chars, no control chars, no double quotes. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. + [JsonPropertyName("permissionMode")] + public AgentRegistrySpawnPermissionMode? PermissionMode { get; set; } } -/// 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 . +/// Feature availability. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowProviderDisconnected : WorkflowRunFailure +public sealed class ConnectorDiscoveryCapabilities { - /// - [JsonIgnore] - public override string Type => "workflow_provider_disconnected"; + /// API version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Availability. + [JsonPropertyName("availability")] + public ConnectorDiscoveryAvailability Availability { get; set; } + + /// Whether results are cached. + [JsonPropertyName("conditionalCache")] + public bool ConditionalCache { get; set; } + + /// Whether accounts are selected by opaque ID. + [JsonPropertyName("opaqueAccountSelection")] + public bool OpaqueAccountSelection { get; set; } } -/// Durable metadata describing who initiated a workflow pause. -/// Polymorphic base type discriminated by type. +/// Account metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(WorkflowPauseInfoUser), "user")] -[JsonDerivedType(typeof(WorkflowPauseInfoCheckpoint), "checkpoint")] -public partial class WorkflowPauseInfo +public sealed class ConnectorDiscoveryAuthInfo { - /// The type discriminator. + /// Host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Authentication type. [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + public AuthInfoType Type { get; set; } } - -/// The user variant of . +/// Eligible account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowPauseInfoUser : WorkflowPauseInfo +public sealed class ConnectorDiscoveryAccount { - /// - [JsonIgnore] - public override string Type => "user"; + /// Opaque account ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Account metadata. + [JsonPropertyName("authInfo")] + public ConnectorDiscoveryAuthInfo AuthInfo { get => field ??= new(); set; } } -/// The checkpoint variant of . +/// Eligible accounts. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowPauseInfoCheckpoint : WorkflowPauseInfo +public sealed class ConnectorDiscoveryAccountList { - /// - [JsonIgnore] - public override string Type => "checkpoint"; + /// Eligible accounts. + [JsonPropertyName("accounts")] + public IList Accounts { get => field ??= []; set; } - /// Stable author-defined checkpoint key that initiated the pause. - [JsonPropertyName("key")] - public required string Key { get; set; } + /// Availability. + [JsonPropertyName("availability")] + public ConnectorDiscoveryAvailability Availability { get; set; } } -/// Complete current or terminal workflow run envelope. +/// Entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunResult +public sealed class ConnectorDiscoveryCatalogEntry { - /// 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; } + /// Description. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Reason for a halted or cancelled run. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Display name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Completed workflow result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Logo. + [JsonPropertyName("logo")] + public string? Logo { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - [JsonPropertyName("snapshot")] - public JsonElement? Snapshot { get; set; } + /// Release tag. + [JsonPropertyName("releaseTag")] + public string? ReleaseTag { get; set; } - /// Current or terminal workflow run status. + /// Status. [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + public ConnectorCatalogStatus Status { get; set; } + + /// Tier. + [JsonPropertyName("tier")] + public string? Tier { get; set; } } -/// Wire-only per-invocation workflow resource ceiling overrides. +/// Entries for the selected account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunLimits +public sealed class ConnectorDiscoveryCatalogResult { - /// 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; } + /// Opaque account ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// Maximum number of workflow subagents that may run concurrently. - [JsonPropertyName("maxConcurrentSubagents")] - public long? MaxConcurrentSubagents { get; set; } + /// Entries. + [JsonPropertyName("connectors")] + public IList Connectors { get => field ??= []; set; } - /// Maximum total number of workflow subagents that may be admitted. - [JsonPropertyName("maxTotalSubagents")] - public long? MaxTotalSubagents { get; set; } + /// Refresh time in Unix epoch milliseconds. + [JsonPropertyName("refreshedAtMs")] + public long RefreshedAtMs { 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; } + /// Revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } } -/// Options controlling workflow invocation. +/// Selected account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunOptions +internal sealed class ConnectorDiscoveryAccountRequest { - /// 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; } - - /// Run identifier whose journal and progress should seed this resumed run. - [JsonPropertyName("resumeFromRunId")] - public string? ResumeFromRunId { get; set; } + /// Opaque account ID. + [RegularExpression("^\\S+$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(2048)] + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; } -/// Parameters for invoking a registered workflow. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowRunRequest +internal sealed class SessionSuspendRequest { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } - - /// Registered workflow name. - [JsonPropertyName("name")] - public string Name { 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; } -/// Resolved persisted workflow identity and resumed run envelope. +/// Result of sending a user message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowResumeResult +public sealed class SendResult { - /// 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; + /// Unique identifier assigned to the message. + [JsonPropertyName("messageId")] + public string MessageId { get; set; } = string.Empty; } -/// Parameters for resuming a workflow run from its persisted identity. +/// Provider-native structured output format. JSON Schema is forwarded without rewriting or validating the schema or the generated output. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowResumeRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ResponseFormatJsonSchema), "json_schema")] +public partial class ResponseFormat { - /// 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; } - - /// 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 type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Options for an internal tool-originated workflow invocation. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolRunOptions -{ - /// Per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } - - /// Run identifier whose journal and progress should seed this resumed run. - [JsonPropertyName("resumeFromRunId")] - public string? ResumeFromRunId { get; set; } -} -/// Internal parameters for invoking a registered workflow from a tool. +/// A JSON Schema output contract. OpenAI receives the name, description, schema and strict setting; Anthropic receives the schema in output_config.format and always uses its native strict enforcement. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolRunRequest +public sealed class JsonSchemaResponseFormat { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + /// Optional description passed to OpenAI providers. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Registered workflow name. + /// Name of the output schema, subject to the provider's naming restrictions. [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; + /// 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 and schema restrictions are determined by the provider. + [JsonPropertyName("schema")] + public JsonElement Schema { get; set; } - /// Opaque identifier of the originating tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } + /// Optional strict enforcement setting for OpenAI providers. Omitted uses the provider default. Anthropic always enforces its supported schema subset. + [JsonPropertyName("strict")] + public bool? Strict { get; set; } } -/// Internal parameters for resuming a workflow run from a tool. +/// The json_schema variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolResumeRequest +public partial class ResponseFormatJsonSchema : ResponseFormat { - /// Optional per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { 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; + /// + [JsonIgnore] + public override string Type => "json_schema"; - /// Opaque identifier of the originating tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } + /// JSON Schema and provider options for the turn's output. + [JsonPropertyName("jsonSchema")] + public required JsonSchemaResponseFormat JsonSchema { get; set; } } -/// Parameters for retrieving a workflow run. +/// Parameters for sending a user message to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowGetRunRequest +internal sealed class SendRequest { - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. + [JsonPropertyName("agentMode")] + public SendAgentMode? AgentMode { get; set; } + + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the message. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } + + /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. + [JsonPropertyName("billable")] + public bool? Billable { get; set; } + + /// If provided, this is shown in the timeline instead of `prompt`. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + [JsonPropertyName("mode")] + public SendMode? Mode { get; set; } + + /// If true, adds the message to the front of the queue instead of the end. + [JsonPropertyName("prepend")] + public bool? Prepend { get; set; } + + /// The user message text. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. + [JsonPropertyName("requestHeaders")] + public IDictionary? RequestHeaders { get; set; } + + /// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange. + [JsonPropertyName("requiredTool")] + public string? RequiredTool { get; set; } + + /// Provider-native output format for this turn, including all tool-call iterations. Not inherited by later turns or subagents. Ordinary steering inherits the active format; specifying responseFormat with mode: immediate is an error, even while idle. Returned assistant content remains text; the runtime does not parse or validate it. Unsupported models or schemas produce provider errors. + [JsonPropertyName("responseFormat")] + public ResponseFormat? ResponseFormat { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-<command-id>` for command-originated messages, `schedule-<numeric-id>` for scheduled prompts, or `agent-<agent-id>` for prompts sent by another agent. + [RegularExpression("^(user|system|command-.*|schedule-\\d+|agent-.+)$")] + [JsonInclude] + [JsonPropertyName("source")] + internal string? Source { get; set; } + + /// W3C Trace Context traceparent header for distributed tracing of this agent turn. + [JsonPropertyName("traceparent")] + public string? Traceparent { get; set; } + + /// W3C Trace Context tracestate header for distributed tracing. + [JsonPropertyName("tracestate")] + public string? Tracestate { get; set; } + + /// If true, await completion of the agentic loop for this message before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageId`; the caller can rely on the agent having processed the message before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } } -/// Declared or approved workflow resource ceilings. +/// Result of sending zero or more user messages. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowDeclaredLimits +public sealed class SendMessagesResult { - /// Maximum AI credits consumed by subagents and descendants. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; set; } + /// Unique identifiers assigned to the messages, one per provided message in order. For a batch that starts a run, assistant messages use the final ID as originatingMessageId throughout that run, including tool iterations and stop-hook corrections. Immediate steering does not replace the active run's origin. Empty when no messages were provided; that run has no originatingMessageId. + [JsonPropertyName("messageIds")] + public IList MessageIds { get => field ??= []; set; } +} - /// Maximum concurrently active subagents. - [JsonPropertyName("maxConcurrentSubagents")] - public long? MaxConcurrentSubagents { get; set; } +/// A single user message to append to the session as part of a `session.sendMessages` turn. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SendMessageItem +{ + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with this message. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } - /// Maximum total subagents spawned by the run. - [JsonPropertyName("maxTotalSubagents")] - public long? MaxTotalSubagents { get; set; } + /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. + [JsonInclude] + [JsonPropertyName("billable")] + internal bool? Billable { get; set; } - /// Maximum accumulated active execution time in seconds. - [JsonPropertyName("timeoutSeconds")] - public double? TimeoutSeconds { get; set; } + /// If provided, this is shown in the timeline instead of `prompt`. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// The user message text. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange. + [JsonPropertyName("requiredTool")] + public string? RequiredTool { get; set; } + + /// Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-<command-id>` for command-originated messages, `schedule-<numeric-id>` for scheduled prompts, or `agent-<agent-id>` for prompts sent by another agent. + [RegularExpression("^(user|system|command-.*|schedule-\\d+|agent-.+)$")] + [JsonInclude] + [JsonPropertyName("source")] + internal string? Source { get; set; } } -/// Durable workflow resource consumption. +/// 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. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunConsumed +internal sealed class SendMessagesRequest { - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeMs")] - public long ActiveMs { get; set; } + /// The UI mode the agent was in when these messages were sent. Defaults to the session's current mode. + [JsonPropertyName("agentMode")] + public SendAgentMode? AgentMode { get; set; } - /// AI usage consumed by the run in nano-AIU. - [JsonPropertyName("nanoAiu")] - public long NanoAiu { get; set; } + /// The user messages to append to the conversation, in order, before running one agent loop. When the batch starts a run, its final message is the primary initiating message; earlier messages provide context, not separate runs or replies. May be empty, in which case a single turn runs over the existing history with no new user message or originatingMessageId. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; set; } - /// Total subagents spawned by the run. - [JsonPropertyName("subagents")] - public long Subagents { get; set; } + /// How to deliver the messages. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + [JsonPropertyName("mode")] + public SendMode? Mode { get; set; } + + /// If true, adds the messages to the front of the queue instead of the end. + [JsonPropertyName("prepend")] + public bool? Prepend { get; set; } + + /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. + [JsonPropertyName("requestHeaders")] + public IDictionary? RequestHeaders { get; set; } + + /// Provider-native output format for the whole turn, including an empty message batch and all tool-call iterations. Not inherited by later turns or subagents. Ordinary steering inherits the active format; specifying responseFormat with mode: immediate is an error, even while idle. Returned assistant content remains text; the runtime does not parse or validate it. Unsupported models or schemas produce provider errors. + [JsonPropertyName("responseFormat")] + public ResponseFormat? ResponseFormat { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// W3C Trace Context traceparent header for distributed tracing of this agent turn. + [JsonPropertyName("traceparent")] + public string? Traceparent { get; set; } + + /// W3C Trace Context tracestate header for distributed tracing. + [JsonPropertyName("tracestate")] + public string? Tracestate { get; set; } + + /// If true, await completion of the agentic loop for this turn before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageIds`; the caller can rely on the agent having processed the messages before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } } -/// Current workflow phase identity. +/// Internal request for sending a system notification. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowCurrentPhase +internal sealed class SendSystemNotificationRequest { - /// Current phase identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Optional structured notification kind. + [JsonPropertyName("kind")] + public JsonElement? Kind { get; set; } - /// Zero-based declared phase ordinal, or null for an undeclared phase. - [JsonPropertyName("ordinal")] - public long? Ordinal { get; set; } + /// Notification text to deliver to the model. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// Internal delivery options, including passive policy. + [JsonPropertyName("options")] + public JsonElement? Options { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Prompt-safe terminal workflow outcome. +/// Result of aborting the current turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunTerminal +public sealed class AbortResult { - /// Human-readable terminal error. + /// Error message if the abort failed. [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; } + /// Whether the abort completed successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Human-readable terminal reason. +/// Parameters for aborting the current turn. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AbortRequest +{ + /// Finite reason code describing why the current turn was aborted. [JsonPropertyName("reason")] - public string? Reason { get; set; } + public AbortReason? Reason { get; set; } - /// Prompt-safe preview of the completed result. - [JsonPropertyName("resultPreview")] - public string? ResultPreview { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Durable workflow run summary with read-time live overlays. +/// Result of interrupting the main agent turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunSummary +public sealed class InterruptMainTurnResult { - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { get; set; } + /// Whether an in-flight main agent turn was interrupted. False when the main loop was not processing. + [JsonPropertyName("interrupted")] + public bool Interrupted { get; set; } +} - /// Approved effective resource ceilings, or null until approved. - [JsonPropertyName("approved")] - public WorkflowDeclaredLimits? Approved { get; set; } +/// Parameters for interrupting the main agent turn. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class InterruptMainTurnRequest +{ + /// When true, the user's queued prompts are preserved and run as the next turn once the interrupted turn unwinds; when false (the default), the queue is cleared like a plain abort. + [JsonPropertyName("flushQueued")] + public bool? FlushQueued { get; set; } - /// Whether the durable run state currently passes runtime resume eligibility checks. - [JsonPropertyName("canResume")] - public bool CanResume { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Epoch milliseconds when the run completed, or null while nonterminal. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionCancelAllBackgroundAgentsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Durable resource consumption. - [JsonPropertyName("consumed")] - public WorkflowRunConsumed Consumed { get => field ??= new(); set; } +/// Parameters for shutting down the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ShutdownRequest +{ + /// Dispatch deferred sessionEnd hooks in the background with their full per-hook timeoutSec instead of awaiting them under the short shared shutdown budget. Set this when the host process keeps running after the session closes (for example the CLI's /clear), so a slow hook neither blocks the close nor is aborted. Hooks still detached when the process later exits are terminated with it. Defaults to false. + [JsonPropertyName("detachSessionEndHooks")] + public bool? DetachSessionEndHooks { get; set; } - /// Epoch milliseconds when the run was created. - [JsonPropertyName("createdAt")] - public long CreatedAt { get; set; } + /// Optional human-readable reason. Typically the message of the error that triggered shutdown when type is 'error'. + [JsonPropertyName("reason")] + public string? Reason { get; set; } - /// Current phase identity, or null before any phase is entered. - [JsonPropertyName("currentPhase")] - public WorkflowCurrentPhase? CurrentPhase { get; set; } + /// 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; } + /// Why the session is being shut down. Defaults to "routine" when omitted. + [JsonPropertyName("type")] + public ShutdownType? Type { get; set; } +} - /// Number of phases declared by the workflow. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } +/// Identifier of the session event that was emitted for the log message. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class LogResult +{ + /// The unique identifier of the emitted session event. + [JsonPropertyName("eventId")] + public Guid EventId { get; set; } +} - /// Human-readable workflow description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; +/// Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class LogRequest +{ + /// When true, the message is transient and not persisted to the session event log on disk. + [JsonPropertyName("ephemeral")] + public bool? Ephemeral { get; set; } - /// Number of direct workflow agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } + /// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". + [JsonPropertyName("level")] + public SessionLogLevel? Level { get; set; } - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } + /// Human-readable message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Optional actionable tip displayed alongside the message. Only honored on `level: "info"`. + [JsonPropertyName("tip")] + public string? Tip { get; set; } - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Domain category for this log entry (e.g., "mcp", "subscription", "policy", "model"). Maps to `infoType`/`warningType`/`errorType` on the emitted event. Defaults to "notification". + [JsonPropertyName("type")] + public string? Type { get; set; } - /// Current workflow run status. - [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + /// Optional URL the user can open in their browser for more details. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("url")] + public string? Url { get; set; } +} - /// Terminal run outcome, or null while nonterminal. - [JsonPropertyName("terminal")] - public WorkflowRunTerminal? Terminal { get; set; } +/// Adapter-declared policy that tells clients whether discovery may run automatically. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAutomaticDiscoveryPolicy +{ + /// Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + [JsonPropertyName("mode")] + public ModelProviderAutomaticDiscoveryMode Mode { get; set; } - /// Total direct workflow agents spawned across all attempts. - [JsonPropertyName("totalSpawnedAgentCount")] - public long TotalSpawnedAgentCount { get; set; } + /// Maximum network scope used by this adapter during discovery. + [JsonPropertyName("networkScope")] + public ModelProviderDiscoveryNetworkScope NetworkScope { get; set; } - /// Epoch milliseconds when the durable run was last updated. - [JsonPropertyName("updatedAt")] - public long UpdatedAt { 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; } - /// Registered workflow name. - [JsonPropertyName("workflowName")] - public string WorkflowName { get; set; } = string.Empty; + /// True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. + [JsonPropertyName("requiresTrust")] + public bool RequiresTrust { get; set; } } -/// A page of workflow runs in durable creation order. +/// An operation supported by a model-provider adapter. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowListRunsResult +public sealed class ModelProviderAdapterOperationDescriptor { - /// Whether terminal runs newer than this page exist. - [JsonPropertyName("hasMoreNewer")] - public bool? HasMoreNewer { get; set; } + /// Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported on discover. No external references are resolved. Omitted or null input selects defaults when requiresInput is false. Without a schema, the adapter validates supplied input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } - /// Newest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("newestSeq")] - public long? NewestSeq { 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; +} - /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("oldestSeq")] - public long? OldestSeq { get; set; } +/// Contributor attribution, independent of routing identity and authorization. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAttribution +{ + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { get; set; } - /// Number of terminal runs older than this page. - [JsonPropertyName("omittedOlder")] - public long? OmittedOlder { 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; } - /// Workflow run summaries in durable creation order. - [JsonPropertyName("runs")] - public IList Runs { get => field ??= []; set; } + /// Kind of component that supplied the adapter. Attribution does not confer authority. + [JsonPropertyName("source")] + public ModelProviderProvenanceSource Source { get; set; } } -/// Parameters for paging workflow runs. +/// A normalized model-provider adapter in the session's effective catalog. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowListRunsRequest +public sealed class ModelProviderAdapterDescriptor { - /// Exclusive forward cursor. - [JsonPropertyName("afterSeq")] - public long? AfterSeq { 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; - /// Exclusive backward cursor. - [JsonPropertyName("beforeSeq")] - public long? BeforeSeq { get; set; } + /// Adapter-declared policy for passive and automatic discovery. + [JsonPropertyName("automaticDiscovery")] + public ModelProviderAutomaticDiscoveryPolicy AutomaticDiscovery { get => field ??= new(); set; } - /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. - [JsonPropertyName("limit")] - public int? Limit { get; set; } + /// Human-readable provider name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// 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; +} + +/// Normalized model-provider adapter definitions available to the session, not discovered instances. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAdapterCatalog +{ + /// 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; } -/// Prompt-safe durable identity and live status for a direct workflow agent. +/// Attribution for the adapter that produced a provider row. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentSummary +public sealed class ModelProviderProvenance { - /// Accumulated active agent time in milliseconds. - [JsonPropertyName("activeMs")] - public long ActiveMs { 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; - /// Prompt-safe live activity text. - [JsonPropertyName("activity")] - public string? Activity { get; set; } + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { get; set; } - /// Stable direct-agent identifier. - [JsonPropertyName("agentId")] - public string AgentId { get; set; } = string.Empty; + /// Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. + [JsonPropertyName("ownerId")] + public string? OwnerId { get; set; } - /// Registered agent type. - [JsonPropertyName("agentType")] - public string AgentType { get; set; } = string.Empty; + /// Descriptive provider family that produced this row; not a routing key. + [JsonPropertyName("providerKind")] + public string ProviderKind { get; set; } = string.Empty; - /// Epoch milliseconds when the agent completed. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } + /// Kind of component that supplied the adapter. + [JsonPropertyName("source")] + public ModelProviderProvenanceSource Source { get; set; } +} - /// Friendly, non-unique name intended for display. +/// Serializable reference to a discovered provider instance. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderInstanceReference +{ + /// 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; + + /// Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + [JsonPropertyName("id")] + public string Id { 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; +} + +/// A normalized model-provider instance discovered by the runtime. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderInstance +{ + /// Human-readable instance name. [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + public string DisplayName { get; set; } = string.Empty; - /// Friendly, non-unique name intended for display. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; + /// Inference API endpoint when the provider exposes one separately from its management endpoint. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("inferenceEndpoint")] + public string? InferenceEndpoint { get; set; } - /// Phase identifier active when the agent was launched, or null. - [JsonPropertyName("phaseId")] - public string? PhaseId { get; set; } + /// Transport to use for inference against this instance. + [JsonPropertyName("inferenceTransport")] + public ProviderEndpointTransport? InferenceTransport { get; set; } - /// Model requested when the agent was launched. - [JsonPropertyName("requestedModel")] - public string? RequestedModel { get; set; } + /// Provider family to use for inference against this instance. + [JsonPropertyName("inferenceType")] + public ProviderEndpointType? InferenceType { get; set; } - /// Concrete model resolved for the agent. - [JsonPropertyName("resolvedModel")] - public string? ResolvedModel { get; set; } + /// Wire API to use for inference against this instance, when required by the provider family. + [JsonPropertyName("inferenceWireApi")] + public ProviderEndpointWireApi? InferenceWireApi { get; set; } - /// Owning workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Attribution for the adapter that produced this instance. + [JsonPropertyName("provenance")] + public ModelProviderProvenance Provenance { get => field ??= new(); set; } - /// Epoch milliseconds when the agent started. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Self-contained reference for subsequent provider operations. + [JsonPropertyName("reference")] + public ModelProviderInstanceReference Reference { get => field ??= new(); set; } +} - /// Current durable or live agent status. - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; +/// 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 ModelProviderOperationOutcome +{ + /// Machine-readable operation outcome. + [JsonPropertyName("code")] + public ModelProviderOperationOutcomeCode Code { get; set; } - /// Tool-call identifier that launched the agent. - [JsonPropertyName("toolCallId")] - public string ToolCallId { get; set; } = string.Empty; + /// Human-readable detail for non-success outcomes. + [JsonPropertyName("message")] + public string? Message { get; set; } } -/// Durable lifecycle and timing for one workflow phase. +/// Provider instances found by a discovery operation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowPhaseObservation +public sealed class ModelProviderDiscoverResult { - /// Completed active time accumulated by this phase in milliseconds. - [JsonPropertyName("accumulatedActiveMs")] - public long AccumulatedActiveMs { get; set; } + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + [JsonPropertyName("instances")] + public IList Instances { get => field ??= []; 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; } + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } +} - /// Current live active time for this phase in milliseconds. - [JsonPropertyName("currentActiveMs")] - public long CurrentActiveMs { get; set; } +/// Provider discovery parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelProviderDiscoverRequest +{ + /// Opaque adapter identity returned by `session.providers.getCatalog`. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; - /// Optional human-readable phase detail. - [JsonPropertyName("detail")] - public string? Detail { 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; } - /// Number of times execution entered this phase. - [JsonPropertyName("entryCount")] - public long EntryCount { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Phase identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; +/// Current health information for a provider instance. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderStatus +{ + /// Normalized provider instance. + [JsonPropertyName("instance")] + public ModelProviderInstance Instance { get => field ??= new(); 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; } + /// Typed operation outcome. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); 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 WorkflowPhaseStatus Status { get; set; } - - /// Human-readable phase title. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; - - /// Total direct agents associated with this phase. - [JsonPropertyName("totalAgentCount")] - public long TotalAgentCount { get; set; } -} - -/// One durable workflow progress record. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowProgressLine -{ - /// Resume attempt that emitted this record. - [JsonPropertyName("attempt")] - public long Attempt { get; set; } - - /// Progress record kind. - [JsonPropertyName("kind")] - public WorkflowLogLineKind Kind { get; set; } - - /// 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; -} - -/// A bidirectional page of workflow progress. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowProgressPage -{ - /// 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; } - - /// 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; } -} - -/// Full workflow run observability detail. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunDetail -{ - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { get; set; } - - /// Durable identities and live statuses for direct workflow agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; 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; } - - /// Number of phases declared by the workflow. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } - - /// Human-readable workflow description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// Number of direct workflow agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } - - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } - - /// Lifecycle and timing observations for each workflow phase. - [JsonPropertyName("phases")] - public IList Phases { get => field ??= []; set; } - - /// Bidirectional page of durable workflow progress. - [JsonPropertyName("progress")] - public WorkflowProgressPage Progress { get => field ??= new(); set; } - - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } - - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } - - /// Current workflow run status. + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. [JsonPropertyName("status")] - public WorkflowRunStatus Status { 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; -} - -/// Parameters for paging workflow progress. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -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; -} - -/// Parameters for cancelling a workflow run. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowCancelRequest -{ - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} - -/// Parameters for pausing a running workflow. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowPauseRequest -{ - /// 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 string Status { get; set; } = string.Empty; -/// RPC data type for SessionWorkflowPauseAtCheckpoint operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkflowPauseAtCheckpointResult -{ - /// Whether this execution attempt must pause or may continue. - [JsonPropertyName("action")] - public WorkflowPauseCheckpointAction Action { get; set; } + /// Provider-reported version. + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Parameters for an owned durable pause checkpoint. +/// Provider status request parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowPauseCheckpointRequest +internal sealed class ModelProviderGetStatusRequest { - /// 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; + /// 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; } -/// Acknowledgement that a workflow request was accepted. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAckResult -{ -} - -/// One ordered workflow progress line. +/// Provider-reported model artifact metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowLogLine +public sealed class ModelArtifactDetails { - /// Progress line kind. - [JsonPropertyName("kind")] - public WorkflowLogLineKind Kind { get; set; } - - /// Monotonic sequence number within the workflow run. - [JsonPropertyName("seq")] - public long Seq { get; set; } + /// Provider-reported model architecture. + [JsonPropertyName("architecture")] + public string? Architecture { get; set; } - /// Progress text. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; -} + /// Provider-reported model families. + [JsonPropertyName("families")] + public IList? Families { get; set; } -/// Parameters for recording workflow progress. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowLogRequest -{ - /// Opaque token identifying the current workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Primary model family. + [JsonPropertyName("family")] + public string? Family { get; set; } - /// Ordered progress lines to append. - [JsonPropertyName("lines")] - public IList Lines { get => field ??= []; set; } + /// Artifact format, such as `gguf`. + [JsonPropertyName("format")] + public string? Format { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Provider-reported parameter count label. + [JsonPropertyName("parameterSize")] + public string? ParameterSize { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Provider-reported quantization label. + [JsonPropertyName("quantization")] + public string? Quantization { get; set; } -/// Result of one workflow-scoped subagent call. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentResult -{ - /// Agent result, omitted when the agent produced no result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Provider-reported tokenizer. + [JsonPropertyName("tokenizer")] + public string? Tokenizer { get; set; } } -/// Options for one workflow-scoped subagent call. +/// A non-fatal provider observation warning. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentOptions +public sealed class ModelProviderWarning { - /// Optional built-in or custom agent name whose definition configures the subagent. - [JsonPropertyName("agent")] - public string? Agent { get; set; } - - /// Optional context tier override for the subagent. - [JsonPropertyName("contextTier")] - public ContextTier? ContextTier { get; set; } - - /// Optional label distinguishing otherwise identical memoized agent calls. - [JsonPropertyName("label")] - public string? Label { get; set; } - - /// 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; } + /// Machine-readable warning code. + [JsonPropertyName("code")] + public string Code { get; set; } = string.Empty; - /// Optional JSON Schema for structured agent output. - [JsonPropertyName("schema")] - public JsonElement? Schema { get; set; } + /// Human-readable warning message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; } -/// Parameters for one workflow-scoped subagent call. +/// 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)] -internal sealed class WorkflowAgentRequest +public sealed class DiscoveredModel { - /// 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; + /// Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. + [JsonPropertyName("capabilities")] + public ModelCapabilities Capabilities { get => field ??= new(); set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Provider-reported model artifact details. + [JsonPropertyName("details")] + public ModelArtifactDetails Details { get => field ??= new(); set; } - /// Workflow run identifier that owns the subagent. - [JsonPropertyName("workflowRunId")] - public string WorkflowRunId { get; set; } = string.Empty; -} + /// Provider-reported artifact digest. + [JsonPropertyName("digest")] + public string? Digest { get; set; } -/// Result of reading a workflow journal entry. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowJournalGetResult -{ - /// Whether the journal contained the requested key. - [JsonPropertyName("hit")] - public bool Hit { get; set; } + /// Provider-native model identifier. + [JsonPropertyName("id")] + public string Id { 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; } -} + /// Provider-reported last-modified timestamp. + [JsonPropertyName("modifiedAt")] + public DateTimeOffset? ModifiedAt { get; set; } -/// 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; + /// Provider-reported display name. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Namespaced journal key. - [JsonPropertyName("key")] - public string Key { get; set; } = string.Empty; + /// Attribution for the adapter that produced this model row. + [JsonPropertyName("provenance")] + public ModelProviderProvenance Provenance { get => field ??= new(); set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Provider-reported artifact size in bytes. + [JsonPropertyName("sizeBytes")] + public long? SizeBytes { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Non-fatal warnings encountered while enriching this model. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// Parameters for storing a workflow journal entry. +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowJournalPutRequest +public sealed class DiscoveredModelList { - /// 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; + /// Provider-native models in provider order. + [JsonPropertyName("models")] + public IList Models { get => field ??= []; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Typed operation outcome. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); 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. +/// Provider model inventory request parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CurrentModel +internal sealed class ModelProviderModelsListRequest { - /// 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; } - - /// 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; } -} + /// Provider instance reference returned by discovery. + [JsonPropertyName("instance")] + public ModelProviderInstanceReference Instance { get => field ??= new(); set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModelGetCurrentRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// RPC data type for ModelSwitchConfirmation operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchConfirmation -{ - /// 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; -} - -/// The model identifier active on the session after the switch. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchToResult -{ - /// 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; } - - /// User-facing warning produced while applying the model switch. - [JsonPropertyName("warning")] - public string? Warning { get; set; } -} - /// Vision-specific limits. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ModelCapabilitiesOverrideLimitsVision @@ -9783,511 +10419,869 @@ public sealed class ModelCapabilitiesOverride public ModelCapabilitiesOverrideSupports? Supports { get; set; } } -/// Environment variables consulted while resolving model-picker settings. -public sealed class ModelPickerSettingsContextEnvironment -{ -} - -/// Filesystem and environment context used to resolve model-picker settings. +/// RPC data type for ProtocolSystemMessageAppendConfig operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelPickerSettingsContext +public sealed class ProtocolSystemMessageAppendConfig { - /// Optional Copilot configuration directory containing persisted settings. - [JsonPropertyName("configDir")] - public string? ConfigDir { get; set; } - - /// Environment variables consulted while resolving model-picker settings. - [JsonPropertyName("environment")] - public ModelPickerSettingsContextEnvironment Environment { get => field ??= new(); set; } + /// Text appended to the standard system prompt. + [JsonPropertyName("content")] + public string? Content { get; set; } - /// User home directory used when resolving persisted settings. - [JsonPropertyName("homeDirectory")] - public string HomeDirectory { get; set; } = string.Empty; + /// Append-mode discriminator. Omission also selects append mode. + [JsonPropertyName("mode")] + public ProtocolAppendMode? Mode { get; set; } } -/// RPC data type for ModelPickerPersistence operations. +/// RPC data type for SystemMessageBlock operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelPickerPersistenceRequest +public sealed class SystemMessageBlock { - /// Whether context tier was explicitly selected and should be persisted. - [JsonPropertyName("contextTierExplicit")] - public bool? ContextTierExplicit { get; set; } + /// Whether providers with explicit prompt caching should place a cache breakpoint after this block. + [JsonPropertyName("cacheBreakpoint")] + public bool? CacheBreakpoint { get; set; } - /// Whether reasoning effort was explicitly selected and should be persisted. - [JsonPropertyName("reasoningEffortExplicit")] - public bool? ReasoningEffortExplicit { get; set; } + /// Text content for this system-message block. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Filesystem and environment context used to resolve settings persistence. - [JsonPropertyName("settingsContext")] - public ModelPickerSettingsContext SettingsContext { get => field ??= new(); set; } + /// Whether the block is static and may be cached independently of dynamic prompt content. + [JsonPropertyName("isStatic")] + public bool? IsStatic { get; set; } } -/// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. +/// RPC data type for ProtocolSystemMessageReplaceConfig operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSwitchToRequest +public sealed class ProtocolSystemMessageReplaceConfig { - /// 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; } + /// Complete replacement system-message text. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. - [JsonPropertyName("contextTier")] - public ContextTier? ContextTier { get; set; } + /// Optional structured blocks corresponding to the replacement content. + [JsonPropertyName("contentBlocks")] + public IList? ContentBlocks { 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; } + /// Replace-mode discriminator. + [JsonPropertyName("mode")] + public ProtocolReplaceMode Mode { get; set; } +} - /// Override individual model capabilities resolved by the runtime. - [JsonPropertyName("modelCapabilities")] - public ModelCapabilitiesOverride? ModelCapabilities { get; set; } +/// RPC data type for ProtocolStaticSectionOverride operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProtocolStaticSectionOverride +{ + /// Declarative operation applied to the section. + [JsonPropertyName("action")] + public ProtocolStaticSectionAction Action { get; set; } - /// Settings scope used when persisting the selected model. - [JsonPropertyName("modelChangeScope")] - public string? ModelChangeScope { get; set; } + /// Optional content used by replace, append, and prepend operations. + [JsonPropertyName("content")] + public string? Content { 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; +/// Polymorphic base type discriminated by action. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "action", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ProtocolMarkerSectionOverrideTransform), "transform")] +[JsonDerivedType(typeof(ProtocolMarkerSectionOverridePreserve), "preserve")] +public partial class ProtocolMarkerSectionOverride +{ + /// The type discriminator. + [JsonPropertyName("action")] + public virtual string Action { get; set; } = string.Empty; +} - /// 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; } +/// The transform variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ProtocolMarkerSectionOverrideTransform : ProtocolMarkerSectionOverride +{ + /// + [JsonIgnore] + public override string Action => "transform"; +} - /// Reasoning summary mode to request for supported model clients. - [JsonPropertyName("reasoningSummary")] - public ReasoningSummary? ReasoningSummary { get; set; } +/// The preserve variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ProtocolMarkerSectionOverridePreserve : ProtocolMarkerSectionOverride +{ + /// + [JsonIgnore] + public override string Action => "preserve"; +} - /// Optional repository settings scope to persist after the switch commits. - [JsonPropertyName("repoScope")] - public string? RepoScope { get; set; } +/// JSON union data type for ProtocolSectionOverride. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSectionOverride +{ + /// Gets the value when this instance contains . + public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } - /// Require the target to be currently available and enabled before applying the switch. - [JsonPropertyName("requireAvailable")] - public bool? RequireAvailable { get; set; } + /// Gets the value when this instance contains . + public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } - /// When true, evaluate context-window compaction policy before applying the switch. - [JsonPropertyName("runCompactionPreflight")] - public bool? RunCompactionPreflight { get; set; } + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolStaticSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolStaticSectionOverride = value; + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); - /// 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; } + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolMarkerSectionOverride = value; + } - /// Output verbosity level to request for supported models. - [JsonPropertyName("verbosity")] - public Verbosity? Verbosity { get; set; } -} + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); -/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchAutoTierResult -{ - /// 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; } + /// 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."); + } - /// Auto preference currently committed for the session. - [JsonPropertyName("effectiveAutoTier")] - public AutoTier? EffectiveAutoTier { 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); + } - /// Latest unclaimed Auto preference waiting for a future user turn. - [JsonPropertyName("pendingAutoTier")] - public AutoTier? PendingAutoTier { get; set; } + throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); + } - /// Immediate request status. `pending` means accepted but not committed. - [JsonPropertyName("status")] - public ModelSwitchAutoTierStatus Status { 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; + } - /// 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; } + throw new JsonException("No ProtocolSectionOverride variant value is set."); + } + } } -/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. +/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSwitchAutoTierRequest +public sealed class ProtocolSystemMessageCustomizeConfig { - /// 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; } + /// Text appended after the customized sections. + [JsonPropertyName("content")] + public string? Content { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Customize-mode discriminator. + [JsonPropertyName("mode")] + public ProtocolCustomizeMode Mode { get; set; } - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. - [JsonPropertyName("source")] - public ModelChangeSource? Source { get; set; } + /// Named standard-prompt section overrides. + [JsonPropertyName("sections")] + public IDictionary? Sections { get; set; } } -/// Managed, repository, and CLI model overrides to overlay onto the session at startup. +/// JSON union data type for ProtocolSystemMessageConfig. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelApplyStartupOverlayRequest +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSystemMessageConfig { - /// 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; } + /// Gets the value when this instance contains . + public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } - /// Model explicitly selected by the CLI, when provided. - [JsonPropertyName("cliModel")] - public string? CliModel { get; set; } + /// Gets the value when this instance contains . + public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } - /// Whether the overlay is being applied while resuming a deferred session. - [JsonPropertyName("deferredResume")] - public bool? DeferredResume { get; set; } + /// Gets the value when this instance contains . + public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } - /// Model required by device-managed policy, when configured. - [JsonPropertyName("deviceManagedModel")] - public string? DeviceManagedModel { get; set; } + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageAppendConfig = value; + } - /// 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; } + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); - /// 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; } + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageReplaceConfig = value; + } - /// Context tier selected by repository settings, when configured. - [JsonPropertyName("repoContextTier")] - public string? RepoContextTier { get; set; } + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) => new(value); - /// Model selected by repository settings, when configured. - [JsonPropertyName("repoModel")] - public string? RepoModel { get; set; } + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageCustomizeConfig = value; + } - /// Reasoning effort selected by repository settings, when configured. - [JsonPropertyName("repoReasoningEffort")] - public string? RepoReasoningEffort { get; set; } + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); - /// Model required by server-managed policy, when configured. - [JsonPropertyName("serverManagedModel")] - public string? ServerManagedModel { 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."); + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + using var document = JsonDocument.ParseValue(ref reader); + var element = document.RootElement; + if (element.ValueKind == JsonValueKind.Object && (!element.TryGetProperty("mode", out _) || (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "append")))) + { + var protocolSystemMessageAppendConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); + return protocolSystemMessageAppendConfig is null ? throw new JsonException("Expected ProtocolSystemMessageAppendConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageAppendConfig); + } + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "replace"))) + { + var protocolSystemMessageReplaceConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); + return protocolSystemMessageReplaceConfig is null ? throw new JsonException("Expected ProtocolSystemMessageReplaceConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageReplaceConfig); + } + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "customize"))) + { + var protocolSystemMessageCustomizeConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); + return protocolSystemMessageCustomizeConfig is null ? throw new JsonException("Expected ProtocolSystemMessageCustomizeConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageCustomizeConfig); + } + + 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 applied host allowlist and effective session model policy after intersection. +/// A BYOK model definition referencing a named provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSetAllowedModelsResult +public sealed class ProviderModelConfig { - /// 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; } + /// Optional capability overrides (vision, tool_calls, reasoning, etc.). + [JsonPropertyName("capabilities")] + public ModelCapabilitiesOverride? Capabilities { 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; } + /// 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; - /// Effective deterministic fallback model, when the policy defines one. - [JsonPropertyName("fallbackModel")] - public string? FallbackModel { get; set; } + /// Maximum context window tokens for the model. + [JsonPropertyName("maxContextWindowTokens")] + public double? MaxContextWindowTokens { get; set; } - /// Selected session model after reconciling a now-disallowed concrete selection. + /// 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; } } -/// 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. +/// Azure-specific provider options. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSetAllowedModelsRequest +public sealed class ProviderConfigAzure { - /// Exact model IDs to permit, or null to clear the host restriction. - [JsonPropertyName("allowedModels")] - public IList? AllowedModels { get; set; } + /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + [JsonPropertyName("apiVersion")] + public string? ApiVersion { get; set; } +} - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class NamedProviderConfig +{ + /// Static API key used to authenticate provider requests. + [JsonPropertyName("apiKey")] + public string? ApiKey { get; set; } + + /// Azure authentication configuration for the provider. + [JsonPropertyName("azure")] + public ProviderConfigAzure? Azure { get; set; } + + /// Base URL for provider API requests. + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = string.Empty; + + /// 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; } } -/// 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. +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSetReasoningEffortResult +public sealed class ModelProviderConfigurationPlan { - /// Reasoning effort level recorded on the session after the update. - [JsonPropertyName("reasoningEffort")] - public string ReasoningEffort { 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; } + + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + [JsonPropertyName("provider")] + public NamedProviderConfig Provider { get => field ??= new(); set; } + + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + [JsonPropertyName("providerDisposition")] + public ModelProviderConfigurationDisposition ProviderDisposition { get; set; } + + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; + + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// Reasoning effort level to apply to the currently selected model. +/// 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)] -internal sealed class ModelSetReasoningEffortRequest +internal sealed class ModelProviderPrepareConfigurationRequest { - /// 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; + /// The discovered instance that serves the model. + [JsonPropertyName("instance")] + public ModelProviderInstance Instance { get => field ??= new(); set; } + + /// The discovered model to configure. + [JsonPropertyName("model")] + public DiscoveredModel Model { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Cost-category metadata for a CAPI model. +/// Managed sandbox enforcement state for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelPriceCategory +public sealed class SandboxEnforcementStatus { - /// CAPI model identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Whether an enforcement failure has permanently blocked the session. + [JsonPropertyName("blocked")] + public bool Blocked { get; set; } - /// Cost category assigned to the model. - [JsonPropertyName("priceCategory")] - public ModelPickerPriceCategory PriceCategory { get; set; } + /// The first sandbox enforcement failure that blocked the session. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Whether the effective managed policy requires an available sandbox backend. + [JsonPropertyName("required")] + public bool Required { get; set; } } -/// The list of models available to this session. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelList +internal sealed class SessionSandboxGetEnforcementStatusRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. - [JsonPropertyName("modelPriceCategories")] - public IList? ModelPriceCategories { get; set; } +/// Result of attempting to disable sandboxing for the current session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxDisableForSessionResult +{ + /// The authoritative sandbox enabled state after the operation. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Per-quota snapshots returned alongside the model list, keyed by quota type. - [JsonPropertyName("quotaSnapshots")] - public IDictionary? QuotaSnapshots { get; set; } + /// Whether this call resolved the pending request and applied the session opt-out. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// RPC data type for SessionModelList operations. +/// Optional informational context describing how and where the permission decision was made. This does not affect permission behavior. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelListRequest +public sealed class PermissionDecisionContext { - /// If true, bypasses the per-session model list cache and re-fetches from CAPI. - [JsonPropertyName("skipCache")] - public bool? SkipCache { get; set; } + /// Disposition of the permission request as observed by the responding client. + [JsonPropertyName("outcome")] + public PermissionDecisionOutcome Outcome { get; set; } + + /// Whether the responding client could ask a user interactively, was running headlessly, or had no response path. Omit when the client cannot determine this authoritatively. + [JsonPropertyName("responseCapability")] + public PermissionResponseCapability? ResponseCapability { get; set; } + + /// Controlled reason or actor responsible for the response. + [JsonPropertyName("source")] + public PermissionDecisionSource Source { get; set; } + + /// Client surface that submitted the response. + [JsonPropertyName("surface")] + public PermissionDecisionSurface Surface { get; set; } } -/// RPC data type for SessionModelListRequestWithSession operations. +/// Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModelListRequestWithSession +internal sealed class SandboxDisableForSessionRequest { + /// Optional attribution for the permission decision. + [JsonPropertyName("decisionContext")] + public PermissionDecisionContext? DecisionContext { get; set; } + + /// Identifier of the exact pending sandbox-bypass permission request that authorized the session opt-out. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// If true, bypasses the per-session model list cache and re-fetches from CAPI. - [JsonPropertyName("skipCache")] - public bool? SkipCache { get; set; } +/// Result of accepting a sandbox path grant. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxGrantPathForRequestResult +{ + /// Whether this call resolved the pending request and added the path to the session's sandbox policy. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Identifies the target session. +/// Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModeGetRequest +internal sealed class SandboxGrantPathForRequestRequest { + /// 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; } -/// Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. +/// Authentication status and account metadata for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModeSetResult +public sealed class SessionAuthStatus { - /// Whether the host should arm an interactive continuation after the mode change. - [JsonPropertyName("armInteractiveContinuation")] - public bool? ArmInteractiveContinuation { get; set; } + /// Authentication type. + [JsonPropertyName("authType")] + public AuthInfoType? AuthType { get; set; } - /// Compaction confirmation required before the mode change can complete. - [JsonPropertyName("confirmation")] - public ModelSwitchConfirmation? Confirmation { get; set; } + /// Copilot plan tier (e.g., individual_pro, business). + [JsonPropertyName("copilotPlan")] + public string? CopilotPlan { get; set; } - /// Whether the host must defer implementing the requested mode change. - [JsonPropertyName("deferImplementation")] - public bool? DeferImplementation { get; set; } + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string? Host { get; set; } - /// Deprecation warnings associated with the model selected by the mode change. - [JsonPropertyName("deprecationWarnings")] - public IList? DeprecationWarnings { get; set; } + /// Whether the session has resolved authentication. + [JsonPropertyName("isAuthenticated")] + public bool IsAuthenticated { get; set; } - /// User-facing outcome message for the model switch triggered by the mode change. - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Authenticated login/username, if available. + [JsonPropertyName("login")] + public string? Login { 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; } + /// Human-readable authentication status description. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } +} - /// Whether applying the mode changed the active model. - [JsonPropertyName("modelChanged")] - public bool ModelChanged { 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; +} - /// Lifecycle status of the requested mode change. - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; +/// Indicates whether the credential update succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSetCredentialsResult +{ + /// 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; } - /// User-facing warning produced while applying the mode change. - [JsonPropertyName("warning")] - public string? Warning { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Agent interaction mode to apply to the session. +/// 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)] -internal sealed class ModeSetRequest +[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 { - /// Explicit response to a model-switch compaction preflight. - [JsonPropertyName("compactionDecision")] - public string? CompactionDecision { get; set; } + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; +} - /// Mode the session must currently be in for the change to apply. When set and the session is in a different mode the request is a no-op and reports status 'unchanged'. - [JsonPropertyName("expectedMode")] - public SessionMode? ExpectedMode { get; set; } - /// Session whose plan-mode base state should be inherited. - [JsonPropertyName("inheritPlanBaseFromSessionId")] - public string? InheritPlanBaseFromSessionId { get; set; } +/// Authentication-info input variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret. +/// The hmac variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SettableAuthInfoHmac : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "hmac"; - /// The session mode the agent is operating in. - [JsonPropertyName("mode")] - public SessionMode Mode { 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; } - /// Whether the selected plan model should be persisted. - [JsonPropertyName("persistPlanSelection")] - public bool? PersistPlanSelection { get; set; } + /// HMAC secret used to sign requests. + [JsonPropertyName("hmac")] + public required string Hmac { get; set; } - /// Settings context used when persisting the selected plan model. - [JsonPropertyName("pickerSettingsContext")] - public ModelPickerSettingsContext? PickerSettingsContext { get; set; } + /// Authentication host. HMAC auth always targets the public GitHub host. + [JsonPropertyName("host")] + public required string Host { get; set; } +} - /// Context tier to use with the dedicated plan model. - [JsonPropertyName("planContextTier")] - public string? PlanContextTier { get; set; } +/// Authentication-info input variant for a token sourced from an environment variable, with host, optional login, token, and env var name. +/// The env variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SettableAuthInfoEnv : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "env"; - /// Action to perform when leaving plan mode. - [JsonPropertyName("planExitAction")] - public string? PlanExitAction { 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; } - /// Dedicated model to use in plan mode, when configured. - [JsonPropertyName("planModel")] - public string? PlanModel { get; set; } + /// Name of the environment variable the token was sourced from. + [JsonPropertyName("envVar")] + public required string EnvVar { get; set; } - /// Whether a dedicated plan model is configured. - [JsonPropertyName("planModelConfigured")] - public bool? PlanModelConfigured { get; set; } + /// Authentication host (e.g. https://github.com or a GHES host). + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Reasoning effort to use with the dedicated plan model. - [JsonPropertyName("planReasoningEffort")] - public string? PlanReasoningEffort { 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; } - /// Whether leaving plan mode should restore the session's previous model. - [JsonPropertyName("restorePlanModel")] - public bool? RestorePlanModel { get; 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 . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SettableAuthInfoToken : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "token"; + + /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } + + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// The token value itself. Treat as a secret. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// The session's friendly name, or null when not yet set. +/// Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host. +/// The copilot-api-token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NameGetResult +public partial class SettableAuthInfoCopilotApiToken : SettableAuthInfo { - /// The session name (user-set or auto-generated), or null if not yet set. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// + [JsonIgnore] + public override string Type => "copilot-api-token"; + + /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } + + /// Authentication host (always the public GitHub host). + [JsonPropertyName("host")] + public required string Host { get; set; } } -/// Identifies the target session. +/// 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)] -internal sealed class SessionNameGetRequest +public partial class SettableAuthInfoUser : SettableAuthInfo { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "user"; + + /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } + + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// OAuth user login. + [JsonPropertyName("login")] + public required string Login { get; set; } } -/// New friendly name to apply to the session. +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class NameSetRequest +public partial class SettableAuthInfoAccount : SettableAuthInfo { - /// 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; + /// + [JsonIgnore] + public override string Type => "account"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Indicates whether the auto-generated summary was applied as the session's name. +/// 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 NameSetAutoResult +public partial class SettableAuthInfoGhCli : SettableAuthInfo { - /// 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; } + /// + [JsonIgnore] + public override string Type => "gh-cli"; + + /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } + + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// User login as reported by `gh auth status`. + [JsonPropertyName("login")] + public required string Login { get; set; } + + /// The token returned by `gh auth token`. Treat as a secret. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// Auto-generated session summary to apply as the session's name when no user-set name exists. +/// 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 NameSetAutoRequest +public partial class SettableAuthInfoApiKey : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "api-key"; + + /// The API key. Treat as a secret. + [JsonPropertyName("apiKey")] + public required string ApiKey { get; set; } + + /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } + + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } +} + +/// New auth credentials to install on the session. Omit to leave credentials unchanged. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionSetCredentialsParams { + /// The new auth credentials to install on the session. When omitted or `undefined`, the call is a no-op and the session's existing credentials are preserved. The runtime installs the supplied value immediately for outbound model/API requests. When the credential carries a raw token (`token`, `env`, or `gh-cli`) but no `copilotUser`, the runtime additionally re-resolves `copilotUser` server-side (best-effort, asynchronously, after the synchronous install) so plan/quota/billing metadata regains fidelity; on resolution failure the verbatim credential remains installed. It does NOT otherwise validate the credential. Several variants carry secret material; treat this method's params as containing secrets at rest and in transit. + [JsonPropertyName("credentials")] + public SettableAuthInfo? Credentials { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Auto-generated session summary. Empty/whitespace-only values are ignored; values are trimmed before persisting. - [JsonPropertyName("summary")] - public string Summary { get; set; } = string.Empty; } -/// Existence, contents, and resolved path of the session plan file. +/// Credential-free authentication identity safe to expose to hosts and user interfaces. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadResult +public sealed class AuthIdentity { - /// The content of the plan file, or null if it does not exist. - [JsonPropertyName("content")] - public string? Content { get; set; } + /// Snapshot of the authenticated user's Copilot subscription info, if known. + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } - /// Whether the plan file exists in the workspace. - [JsonPropertyName("exists")] - public bool Exists { get; set; } + /// Name of the environment variable that supplied the credential, when applicable. + [JsonPropertyName("envVar")] + public string? EnvVar { get; set; } - /// Absolute file path of the plan file, or null if workspace is not enabled. - [JsonPropertyName("path")] - public string? Path { 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. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanReadRequest +internal sealed class SessionGitHubAuthGetCurrentAuthInfoRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Replacement contents to write to the session plan file. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PlanUpdateRequest +internal sealed class SessionGitHubAuthGetAllAuthAvailableRequest { - /// 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; @@ -10295,1060 +11289,1210 @@ internal sealed class PlanUpdateRequest /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanDeleteRequest +internal sealed class SessionGitHubAuthRefreshCopilotUserRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single todo row read from the session SQL `todos` table. All fields are optional because the SQL schema is best-effort and the agent may not have populated every column. +/// Internal GitHub login parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanSqlTodosRow +internal sealed class SessionAuthLoginRequest { - /// 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; } + /// GitHub host URL. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Todo description. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// GitHub login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Todo identifier. - [JsonPropertyName("id")] - public string? Id { get; set; } + /// Whether to persist the token after login. + [JsonPropertyName("persist")] + public bool? Persist { get; set; } - /// Todo status. - [JsonPropertyName("status")] - public string? Status { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Todo title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// GitHub authentication token. + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; } -/// Todo rows read from the session SQL database. Empty when no session database is available. +/// Parameters for switching the session's active authentication. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadSqlTodosResult +internal sealed class SessionAuthSwitchRequest { - /// 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; } + /// 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; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanReadSqlTodosRequest +internal sealed class SessionGitHubAuthLogoutRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single dependency edge read from the session SQL `todo_deps` table, indicating that one todo must complete before another. +/// Parameters identifying a GitHub authentication to log out. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanSqlTodoDependency +internal sealed class SessionAuthLogoutUserRequest { - /// ID of the todo it depends on. - [JsonPropertyName("dependsOn")] - public string DependsOn { get; set; } = string.Empty; + /// Authentication information to log out. + [JsonPropertyName("authInfo")] + public AuthInfo AuthInfo { get => field ??= new(); set; } - /// ID of the todo that has the dependency. - [JsonPropertyName("todoId")] - public string TodoId { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Todo rows + dependency edges read from the session SQL database. +/// Validation error from an authentication attempt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadSqlTodosWithDependenciesResult +public sealed class AuthValidationError { - /// Edges from the session SQL todo_deps table. Empty when no database, no todo_deps table, or the SELECT failed. Read independently from `rows`, so a broken todo_deps table does not affect the rows result and vice versa. - [JsonPropertyName("dependencies")] - public IList Dependencies { get => field ??= []; set; } + /// Optional message returned by GitHub. + [JsonPropertyName("githubMessage")] + public string? GitHubMessage { 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; } + /// 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 SessionPlanReadSqlTodosWithDependenciesRequest +internal sealed class SessionGitHubAuthLastAuthErrorsRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// RPC data type for WorkspacesGetWorkspaceResultWorkspace operations. -public sealed class WorkspacesGetWorkspaceResultWorkspace +/// 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 { - /// Current Git branch. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// Whether the per-session Chronicle upgrade prompt was dismissed for the workspace. - [JsonPropertyName("chronicle_sync_dismissed")] - public bool? ChronicleSyncDismissed { get; set; } - - /// Name of the client that created the workspace. - [JsonPropertyName("client_name")] - public string? ClientName { get; set; } - - /// Timestamp when the workspace was created. - [JsonPropertyName("created_at")] - public DateTimeOffset? CreatedAt { get; set; } - - /// Current working directory associated with the workspace. - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Git repository root associated with the workspace. - [JsonPropertyName("git_root")] - public string? GitRoot { get; set; } - /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. - [JsonPropertyName("host_type")] - public WorkspacesWorkspaceDetailsHostType? HostType { get; set; } +/// One signed-in account in the roster forest. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AccountStatus +{ + /// Whether this is the active account. + [JsonPropertyName("active")] + public bool Active { 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; + /// 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; } - /// Most recent Mission Control event identifier observed for the workspace. - [JsonPropertyName("mc_last_event_id")] - public string? McLastEventId { get; set; } + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Mission Control session identifier associated with the workspace. - [JsonPropertyName("mc_session_id")] - public string? McSessionId { get; set; } + /// The provider kind of this account. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } - /// Mission Control task identifier associated with the workspace. - [JsonPropertyName("mc_task_id")] - public string? McTaskId { get; set; } + /// Authenticated login/username. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Workspace display name. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Opaque selection id used to switch to, or log out, this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; +} - /// Whether the workspace session can be steered remotely. - [JsonPropertyName("remote_steerable")] - public bool? RemoteSteerable { get; set; } +/// The accounts variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthEnumerateValueAccounts : AuthEnumerateValue +{ + /// + [JsonIgnore] + public override string Kind => "accounts"; - /// Repository identifier associated with the workspace. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// The signed-in account forest; empty when not logged in. + [JsonPropertyName("items")] + public required IList Items { get; set; } +} - /// Number of persisted summaries in the workspace. - [JsonPropertyName("summary_count")] - public long? SummaryCount { get; set; } +/// A provider offered for interactive login. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderDescriptor +{ + /// Whether this provider is currently available to sign in with. + [JsonPropertyName("available")] + public bool Available { get; set; } - /// Timestamp when the workspace was last updated. - [JsonPropertyName("updated_at")] - public DateTimeOffset? UpdatedAt { get; set; } + /// The neutral provider kind. + [JsonPropertyName("kind")] + public LoginProviderKind Kind { get; set; } - /// Whether the workspace name was explicitly chosen by the user. - [JsonPropertyName("user_named")] - public bool? UserNamed { 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; } -/// Current workspace metadata for the session, including its absolute filesystem path when available. +/// The providers variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesGetWorkspaceResult +public partial class AuthEnumerateValueProviders : AuthEnumerateValue { - /// 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; } + /// + [JsonIgnore] + public override string Kind => "providers"; - /// Current workspace metadata, or null if not available. - [JsonPropertyName("workspace")] - public WorkspacesGetWorkspaceResultWorkspace? Workspace { get; set; } + /// The providers offered for interactive login. + [JsonPropertyName("items")] + public required IList Items { get; set; } } -/// Identifies the target session. +/// 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)] -internal sealed class SessionWorkspacesGetWorkspaceRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthEnumerateQueryAccounts), "accounts")] +[JsonDerivedType(typeof(AuthEnumerateQueryProviders), "providers")] +public partial class AuthEnumerateQuery { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Workspace metadata fields to update. + +/// The accounts variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesUpdateMetadataRequest +public partial class AuthEnumerateQueryAccounts : AuthEnumerateQuery { - /// Opaque workspace context supplied by the session host. - [JsonPropertyName("context")] - public JsonElement? Context { get; set; } + /// + [JsonIgnore] + public override string Kind => "accounts"; +} - /// Optional workspace display name override. - [JsonPropertyName("name")] - public string? Name { get; set; } +/// The providers variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthEnumerateQueryProviders : AuthEnumerateQuery +{ + /// + [JsonIgnore] + public override string Kind => "providers"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Optional session context used when creating a local workspace. +/// Enumerate request carrying the typed collection query. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesEnsureRequest +internal sealed class AccountsEnumerateRequest { - /// Opaque workspace context supplied by the session host. - [JsonPropertyName("context")] - public JsonElement? Context { get; set; } + /// Which typed accounts collection to enumerate. + [JsonPropertyName("query")] + public AuthEnumerateQuery Query { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Relative paths of files stored in the session workspace files directory. +/// The read result, keyed by the same selector as the query. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesListFilesResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthReadValueActiveAccount), "activeAccount")] +[JsonDerivedType(typeof(AuthReadValueStatus), "status")] +[JsonDerivedType(typeof(AuthReadValueLastErrors), "lastErrors")] +public partial class AuthReadValue { - /// Slash-separated relative file paths in the workspace files directory. - [JsonPropertyName("files")] - public IList Files { get => field ??= []; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Identifies the target session. + +/// The activeAccount variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesListFilesRequest +public partial class AuthReadValueActiveAccount : AuthReadValue { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "activeAccount"; + + /// The active account, or absent when not logged in. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("account")] + public AccountStatus? Account { get; set; } + + /// Credential-free identity metadata for the active account, including resolved Copilot user information when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("authInfo")] + public AuthIdentity? AuthInfo { get; set; } } -/// Contents of the requested workspace file as a UTF-8 string. +/// Neutral authentication status summary. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadFileResult +public sealed class AuthStatusDto { - /// File content as a UTF-8 string. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Number of signed-in accounts in the roster. + [JsonPropertyName("accountCount")] + public long AccountCount { get; set; } + + /// Active account host, if authenticated. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("activeHost")] + public string? ActiveHost { get; set; } + + /// Active account login, if authenticated. + [JsonPropertyName("activeLogin")] + public string? ActiveLogin { 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; } } -/// Relative path of the workspace file to read. +/// The status variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesReadFileRequest +public partial class AuthReadValueStatus : AuthReadValue { - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "status"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The neutral authentication status summary. + [JsonPropertyName("status")] + public required AuthStatusDto Status { get; set; } } -/// Relative path and UTF-8 content for the workspace file to create or overwrite. +/// The lastErrors variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesCreateFileRequest +public partial class AuthReadValueLastErrors : AuthReadValue { - /// File content to write as a UTF-8 string. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "lastErrors"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Validation errors from the most recent authentication attempt. + [JsonPropertyName("errors")] + public required IList Errors { get; set; } } -/// Filesystem metadata for a path in the session workspace files directory. +/// Selects which typed accounts datum to read. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesStatFileResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthReadQueryActiveAccount), "activeAccount")] +[JsonDerivedType(typeof(AuthReadQueryStatus), "status")] +[JsonDerivedType(typeof(AuthReadQueryLastErrors), "lastErrors")] +public partial class AuthReadQuery { - /// Creation time in Unix epoch milliseconds. - [JsonPropertyName("birthtimeMs")] - public double BirthtimeMs { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Whether the path identifies a directory. - [JsonPropertyName("isDirectory")] - public bool IsDirectory { get; set; } - /// Whether the path identifies a regular file. - [JsonPropertyName("isFile")] - public bool IsFile { get; set; } +/// The activeAccount variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadQueryActiveAccount : AuthReadQuery +{ + /// + [JsonIgnore] + public override string Kind => "activeAccount"; +} - /// Last modification time in Unix epoch milliseconds. - [JsonPropertyName("mtimeMs")] - public double MtimeMs { get; set; } +/// The status variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadQueryStatus : AuthReadQuery +{ + /// + [JsonIgnore] + public override string Kind => "status"; +} - /// Size in bytes. - [JsonPropertyName("size")] - public double Size { get; set; } +/// The lastErrors variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadQueryLastErrors : AuthReadQuery +{ + /// + [JsonIgnore] + public override string Kind => "lastErrors"; } -/// Relative path of the workspace file or directory to inspect. +/// Read request carrying the typed datum query. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesStatFileRequest +internal sealed class AccountsGetRequest { - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// 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; } -/// Directory to create within the session workspace files directory. +/// Result of a non-interactive accounts mutation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesCreateDirectoryRequest +public sealed class AuthWriteResult { - /// 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; } + /// For a logout, whether other signed-in accounts remain. + [JsonPropertyName("moreUsers")] + public bool? MoreUsers { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the mutation was applied. + [JsonPropertyName("ok")] + public bool Ok { get; set; } } -/// File or directory to remove from the session workspace files directory. +/// 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 WorkspacesRemovePathRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthWriteSwitchActive), "switchActive")] +[JsonDerivedType(typeof(AuthWriteLogout), "logout")] +[JsonDerivedType(typeof(AuthWriteSetCredentials), "setCredentials")] +public partial class AuthWrite { - /// Whether a missing path should be treated as success. Defaults to false. - [JsonPropertyName("force")] - public bool? Force { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - /// Whether to remove directory contents recursively. Defaults to false. - [JsonPropertyName("recursive")] - public bool? Recursive { 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; } } -/// Source and destination paths for a rename within the session workspace files directory. +/// The logout variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesRenamePathRequest +public partial class AuthWriteLogout : AuthWrite { - /// Slash-separated destination path relative to the workspace files directory. - [JsonPropertyName("destination")] - public string Destination { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "logout"; - /// Slash-separated source path relative to the workspace files directory. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; + /// 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; } } -/// Workspace checkpoint metadata with assigned number, human-readable title, and checkpoint filename. +/// The setCredentials variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesCheckpoints +public partial class AuthWriteSetCredentials : AuthWrite { - /// Filename of the checkpoint within the workspace checkpoints directory. - [JsonPropertyName("filename")] - public string Filename { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "setCredentials"; - /// Checkpoint number assigned by the workspace manager. - [JsonPropertyName("number")] - public long Number { get; set; } + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Human-readable checkpoint title. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; -} + /// Login/username for the credential. + [JsonPropertyName("login")] + public required string Login { 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; } + /// GitHub authentication token to install. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// Identifies the target session. +/// Mutation request carrying the typed write command. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesListCheckpointsRequest +internal sealed class AccountsSetRequest { + /// The non-interactive mutation command to apply. + [JsonPropertyName("command")] + public AuthWrite Command { get => field ??= new(); set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. +/// 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 WorkspacesReadCheckpointResult +[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 { - /// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. - [JsonPropertyName("content")] - public string? Content { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Checkpoint number to read. + +/// The open-url variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesReadCheckpointRequest +public partial class AuthLoginStepOpenUrl : AuthLoginStep { - /// Checkpoint number to read. - [JsonPropertyName("number")] - public long Number { get; set; } + /// + [JsonIgnore] + public override string Kind => "open-url"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Authorize URL the consumer should open in a browser (consumer-driven browser-open). + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// Metadata for the persisted summary. -public sealed class WorkspacesAddSummaryResultSummary +/// The input-required variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthLoginStepInputRequired : AuthLoginStep { + /// + [JsonIgnore] + public override string Kind => "input-required"; + + /// 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; } } -/// Refreshed metadata for the containing workspace. -public sealed class WorkspacesAddSummaryResultWorkspace +/// The awaiting variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthLoginStepAwaiting : AuthLoginStep { + /// + [JsonIgnore] + public override string Kind => "awaiting"; } -/// Persisted summary metadata and refreshed workspace metadata. +/// The needs-interaction variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesAddSummaryResult +public partial class AuthLoginStepNeedsInteraction : AuthLoginStep { - /// Metadata for the persisted summary. - [JsonPropertyName("summary")] - public WorkspacesAddSummaryResultSummary? Summary { get; set; } - - /// Refreshed metadata for the containing workspace. - [JsonPropertyName("workspace")] - public WorkspacesAddSummaryResultWorkspace? Workspace { get; set; } + /// + [JsonIgnore] + public override string Kind => "needs-interaction"; } -/// Compaction summary checkpoint to persist. +/// A credential-free account choice after sign-in. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesAddSummaryRequest +public sealed class AuthLoginAccount { - /// Markdown summary content to persist. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Host coordinate owned by the selected account's provider. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Provider kind that owns this account choice. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } - /// Summary title shown in checkpoint listings. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; + /// Human-readable login for the account choice. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Opaque identifier supplied to the next login step to select this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; } -/// Rollback point for local workspace summaries. +/// Result of an interactive login flow. Pending consent or account selection is not terminal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesTruncateSummariesRequest +public sealed class AuthLoginResultDto { - /// Number of newest summaries to keep. - [JsonPropertyName("keepCount")] - public long KeepCount { get; set; } + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + [JsonPropertyName("accounts")] + public IList? Accounts { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Host that was signed in, when completed. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string? Host { get; set; } + + /// Login that was signed in, when completed. + [JsonPropertyName("login")] + public string? Login { get; set; } + + /// Current disposition of the login, including pending user decisions. + [JsonPropertyName("status")] + public AuthLoginResultStatus Status { get; set; } } -/// Autopilot objective file content, or null when missing. +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadAutopilotObjectiveResult +public partial class AuthLoginStepCompleted : AuthLoginStep { - /// Autopilot objective file content, or null when missing. - [JsonPropertyName("content")] - public string? Content { get; set; } + /// + [JsonIgnore] + public override string Kind => "completed"; + + /// 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; } } -/// Identifies the target session. +/// The error variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesReadAutopilotObjectiveRequest +public partial class AuthLoginStepError : AuthLoginStep { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "error"; + + /// Human-readable failure message. + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Result of writing the autopilot objective file. +/// A started login flow: its opaque id and first step. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesWriteAutopilotObjectiveResult +public sealed class AuthLoginBegun { - /// Filesystem operation performed. - [JsonPropertyName("operation")] - public string Operation { get; set; } = string.Empty; + /// 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; } } -/// Autopilot objective file content to persist. +/// Begin an interactive login flow for a provider kind. Dispatch is kind-only. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesWriteAutopilotObjectiveRequest +internal sealed class AuthLoginBeginRequest { - /// Autopilot objective file content. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// 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; } -/// Result of deleting the autopilot objective file. +/// Advance an in-flight login flow, optionally fulfilling an input-required step. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesDeleteAutopilotObjectiveResult +internal sealed class AuthLoginAdvanceRequest { - /// True when a file was deleted. - [JsonPropertyName("deleted")] - public bool Deleted { get; set; } + /// Opaque flow id from begin. + [JsonPropertyName("flowId")] + public string FlowId { get; set; } = string.Empty; + + /// Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. + [JsonPropertyName("input")] + public string? Input { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Cancel an in-flight login flow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesDeleteAutopilotObjectiveRequest +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; } -/// Whether the autopilot objective file exists. +/// A file included in the session debug bundle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesAutopilotObjectiveExistsResult +public sealed class DebugCollectLogsCollectedEntry { - /// True when the objective file exists. - [JsonPropertyName("exists")] - public bool Exists { get; set; } + /// Relative path of the file in the staged bundle/archive. + [JsonPropertyName("bundlePath")] + public string BundlePath { get; set; } = string.Empty; + + /// Redacted output size in bytes. + [JsonPropertyName("sizeBytes")] + public long SizeBytes { get; set; } + + /// Source category for this entry. + [JsonPropertyName("source")] + public DebugCollectLogsSource Source { get; set; } } -/// Identifies the target session. +/// An optional debug bundle entry that could not be included. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesAutopilotObjectiveExistsRequest +public sealed class DebugCollectLogsSkippedEntry { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Relative path requested for this bundle entry. + [JsonPropertyName("bundlePath")] + public string BundlePath { get; set; } = string.Empty; + + /// Server-local source path that could not be read. + [JsonPropertyName("path")] + public string? Path { get; set; } + + /// Reason the entry was skipped. + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; } -/// RPC data type for WorkspacesSaveLargePasteResultSaved operations. -public sealed class WorkspacesSaveLargePasteResultSaved +/// Result of collecting a session debug bundle. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DebugCollectLogsResult { - /// Filename within the workspace files directory. - [JsonPropertyName("filename")] - public string Filename { get; set; } = string.Empty; + /// Files included in the bundle. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } - /// Absolute filesystem path to the saved paste file. - [JsonPropertyName("filePath")] - public string FilePath { get; set; } = string.Empty; + /// Destination kind that was written. + [JsonPropertyName("kind")] + public DebugCollectLogsResultKind Kind { get; set; } - /// Size of the saved file in bytes. - [JsonPropertyName("sizeBytes")] - public long SizeBytes { get; set; } + /// Actual archive path or staging directory path written. This may differ from the requested path when no-overwrite suffixing or fallback-to-temp-directory was needed. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Optional files or directories that could not be included. + [JsonPropertyName("skippedEntries")] + public IList? SkippedEntries { get; set; } } -/// Descriptor for the saved paste file, or null when the workspace is unavailable. +/// A caller-provided server-local file or directory to include in the debug bundle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesSaveLargePasteResult +public sealed class DebugCollectLogsEntry { - /// 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; } + /// 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; } + + /// 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; } } -/// Pasted content to save as a UTF-8 file in the session workspace. +/// Destination for the session debug bundle. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesSaveLargePasteRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] +[JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] +public partial class DebugCollectLogsDestination { - /// 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; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// A single changed file and its unified diff. + +/// The archive variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspaceDiffFileChange +public partial class DebugCollectLogsDestinationArchive : DebugCollectLogsDestination { - /// Type of change represented by this file diff. - [JsonPropertyName("changeType")] - public WorkspaceDiffFileChangeType ChangeType { get; set; } + /// + [JsonIgnore] + public override string Kind => "archive"; - /// Unified diff content for the file. Empty when the diff was truncated. - [JsonPropertyName("diff")] - public string Diff { get; set; } = string.Empty; + /// When true, create the archive atomically without overwriting an existing file by appending ` (N)` before the extension as needed. Defaults to false. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("noOverwrite")] + public bool? NoOverwrite { get; set; } - /// Whether the diff content was omitted because it exceeded the per-file size limit. - [JsonPropertyName("isTruncated")] - public bool? IsTruncated { get; set; } + /// Absolute or server-relative path for the .tgz archive to create. + [JsonPropertyName("outputPath")] + public required string OutputPath { get; set; } +} - /// Original file path for renamed files. - [JsonPropertyName("oldPath")] - public string? OldPath { get; set; } +/// The directory variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class DebugCollectLogsDestinationDirectory : DebugCollectLogsDestination +{ + /// + [JsonIgnore] + public override string Kind => "directory"; - /// 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; + /// Directory where files should be staged. The directory is created if needed. + [JsonPropertyName("outputDirectory")] + public required string OutputDirectory { get; set; } } -/// Workspace diff result for the requested mode. +/// Built-in session diagnostics to include in the bundle. Omitted fields default to true. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspaceDiffResult +public sealed class DebugCollectLogsInclude { - /// Default branch used for a branch diff, when branch mode was requested. - [JsonPropertyName("baseBranch")] - public string? BaseBranch { 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; } - /// Changed files and their unified diffs. - [JsonPropertyName("changes")] - public IList Changes { get => field ??= []; set; } + /// Include the session event log (`events.jsonl`). Defaults to true. + [JsonPropertyName("events")] + public bool? Events { 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; } + /// 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; } - /// Effective mode used for the returned changes. - [JsonPropertyName("mode")] - public WorkspaceDiffMode Mode { get; set; } + /// Maximum number of previous process logs to include. Defaults to 5. + [JsonPropertyName("previousProcessLogLimit")] + public long? PreviousProcessLogLimit { get; set; } - /// Diff mode requested by the client. - [JsonPropertyName("requestedMode")] - public WorkspaceDiffMode RequestedMode { 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; } - /// 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; } + /// 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; } } -/// Parameters for computing a workspace diff. +/// Options for collecting a session debug bundle with configurable redaction. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesDiffRequest +internal sealed class DebugCollectLogsRequest { - /// When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. - [JsonPropertyName("ignoreWhitespace")] - public bool? IgnoreWhitespace { 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; } - /// Diff mode requested by the client. - [JsonPropertyName("mode")] - public WorkspaceDiffMode Mode { 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; } + + /// Which built-in session diagnostics to include. Omitted fields default to true. + [JsonPropertyName("include")] + public DebugCollectLogsInclude? Include { 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. +/// 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 AutopilotObjectiveCreditLimit +public sealed class CanvasAction { - /// Configured AI-credit cap, when one is set. - [JsonPropertyName("credits")] - public double? Credits { get; set; } + /// Description of the action. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Window consumption in fractional AI credits, for display. - [JsonPropertyName("creditsUsed")] - public double CreditsUsed { get; set; } + /// JSON Schema for the action input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { 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; + /// Action name exposed by the canvas provider. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Public, persistence-independent projection of an autopilot objective. +/// Canvas available in the current session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AutopilotObjectiveState +public sealed class DiscoveredCanvas { - /// 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; } + /// Actions the agent or host may invoke on an open instance. + [JsonPropertyName("actions")] + public IList? Actions { get; set; } - /// Session-local objective identifier. - [JsonPropertyName("id")] - public long Id { get; set; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// User-provided objective text. - [JsonPropertyName("objective")] - public string Objective { 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; - /// Optional reason the objective is paused. - [JsonPropertyName("pauseReason")] - public string? PauseReason { get; set; } + /// Human-readable canvas name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Current normalized lifecycle status. - [JsonPropertyName("status")] - public AutopilotObjectiveStatus Status { get; set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Number of objective turns started. - [JsonPropertyName("turnCount")] - public long TurnCount { get; set; } -} + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } -/// Canonical runtime state for the session's current autopilot objective. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AutopilotObjectiveGetStateResult -{ - /// Current objective state, or `null` when the session has no objective. - [JsonPropertyName("state")] - public AutopilotObjectiveState? State { get; set; } -} + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAutopilotObjectiveGetStateRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// JSON Schema for canvas open input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } } -/// 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`). +/// Declared canvases available in this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CompletionsGetTriggerCharactersResult +public sealed class CanvasList { - /// Trigger characters advertised by the host (e.g. `["@", "#"]`). Empty disables host-driven completions for the session. - [JsonPropertyName("triggerCharacters")] - public IList TriggerCharacters { get => field ??= []; set; } + /// Declared canvases available in this session. + [JsonPropertyName("canvases")] + public IList Canvases { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCompletionsGetTriggerCharactersRequest +internal sealed class SessionCanvasListRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single host-driven completion. Accepting an item replaces `[rangeStart, rangeEnd)` (UTF-16 code units) in the composer with `insertText`; when the range is absent, the active token around the cursor is replaced. +/// Open canvas instance snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionCompletionItem +public sealed class OpenCanvasInstance { - /// Text spliced into the composer when the item is accepted. - [JsonPropertyName("insertText")] - public string InsertText { get; set; } = string.Empty; + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Render-kind hint for the picker row (e.g. `"document"`, `"directory"`), derived from the host's display kind. - [JsonPropertyName("kind")] - public string? Kind { get; set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Primary display label for the picker row. Falls back to `insertText` when absent. - [JsonPropertyName("label")] - public string? Label { get; set; } + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } - /// End (exclusive) of the replacement range in `text`, in UTF-16 code units. - [JsonPropertyName("rangeEnd")] - public long? RangeEnd { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } - /// Start of the replacement range in `text`, in UTF-16 code units. - [JsonPropertyName("rangeStart")] - public long? RangeStart { get; set; } -} + /// Input supplied when the instance was opened. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } -/// Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CompletionsRequestResult -{ - /// Completion items in host-ranked order. - [JsonPropertyName("items")] - public IList Items { get => field ??= []; set; } -} + /// Stable caller-supplied canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; -/// 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; } + /// Provider-supplied status text. + [JsonPropertyName("status")] + public string? Status { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Rendered title. + [JsonPropertyName("title")] + public string? Title { get; set; } - /// The full composed composer input. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// URL for web-rendered canvases. + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// Instruction sources loaded for the session, in merge order. +/// Live open-canvas snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstructionsGetSourcesResult +public sealed class CanvasListOpenResult { - /// Instruction sources for the session. - [JsonPropertyName("sources")] - public IList Sources { get => field ??= []; set; } + /// 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 SessionInstructionsGetSourcesRequest +internal sealed class SessionCanvasListOpenRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Canvas open parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionInstructionsReloadRequest +internal sealed class CanvasOpenRequest { + /// 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; } -/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. +/// Canvas close parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillsLoadDiagnostics +internal sealed class CanvasCloseRequest { - /// 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; } -} + /// Open canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; -/// 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; } -/// Indicates whether fleet mode was successfully activated. +/// Canvas action invocation result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FleetStartResult +public sealed class CanvasActionInvokeResult { - /// Whether fleet mode was successfully activated. - [JsonPropertyName("started")] - public bool Started { get; set; } + /// Provider-supplied action result. + [JsonPropertyName("result")] + public JsonElement? Result { 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. +/// Canvas action invocation parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FleetStartRequest +internal sealed class CanvasActionInvokeRequest { - /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request. - [JsonPropertyName("attachments")] - public IList? Attachments { get; set; } + /// Action name to invoke. + [JsonPropertyName("actionName")] + public string ActionName { get; set; } = string.Empty; - /// If false, this request will not trigger a Premium Request Unit charge. User requests default to billable. - [JsonInclude] - [JsonPropertyName("billable")] - internal bool? Billable { get; set; } + /// Action input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Optional user prompt to combine with fleet instructions. - [JsonPropertyName("prompt")] - public string? Prompt { 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; - - /// If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. - [JsonPropertyName("wait")] - public bool? Wait { get; set; } } -/// Agents available to the session. +/// Internal canvas provider registration parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentList +internal sealed class CanvasProviderRegisterRequest { - /// Available agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } + /// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// RPC data type for SessionAgentList operations. +/// Internal canvas provider unregistration parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionAgentListRequest +internal sealed class CanvasProviderUnregisterRequest { - /// 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; } + /// Connection identifier to unregister. + [JsonPropertyName("connectionId")] + public string ConnectionId { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// RPC data type for SessionAgentListRequestWithSession operations. +/// Machine-readable workflow run failure. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentListRequestWithSession +[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 { - /// When true, request the session's configured built-in agents alongside custom agents. Listing applies feature, context, inclusion, exclusion, and user-disabled-agent policy, but does not evaluate transient invocation requirements such as model availability. Built-in metadata may be omitted when the session cannot project it, such as a relay session. - [JsonPropertyName("includeBuiltInAgents")] - public bool? IncludeBuiltInAgents { get; set; } - - /// When true, request authored base prompt text on each AgentInfo. Prompt text may be omitted when unavailable, such as for agents projected through a relay session. - [JsonPropertyName("includePrompt")] - public bool? IncludePrompt { get; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// An in-memory authored prompt override for an available agent. + +/// The workflow_limit_reached variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentSetPromptRequest +public partial class WorkflowRunFailureWorkflowLimitReached : WorkflowRunFailure { - /// Stable effective agent id. Plugin namespace separators are normalized. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "workflow_limit_reached"; - /// Replacement authored prompt. Empty text is valid. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Resource ceiling that stopped the run. + [JsonPropertyName("kind")] + public required WorkflowRunFailureKind Kind { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Workflow run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } -/// The currently selected custom agent, or null when using the default agent. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentGetCurrentResult -{ - /// Currently selected custom agent, or null if using the default agent. - [JsonPropertyName("agent")] - public AgentInfo? Agent { get; set; } -} + /// Suggested larger ceiling when the runtime can derive one safely. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("suggestedValue")] + public double? SuggestedValue { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentGetCurrentRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Approved effective ceiling that was reached. + [JsonPropertyName("value")] + public required double Value { get; set; } } -/// The newly selected custom agent. +/// The workflow_resume_declined variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentSelectResult +public partial class WorkflowRunFailureWorkflowResumeDeclined : WorkflowRunFailure { - /// The newly selected custom agent. - [JsonPropertyName("agent")] - public AgentInfo Agent { get => field ??= new(); set; } -} + /// + [JsonIgnore] + public override string Type => "workflow_resume_declined"; -/// Name of the custom agent to select for subsequent turns. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentSelectRequest -{ - /// Name of the custom agent to select. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Human-readable reason the resume did not proceed. + [JsonPropertyName("reason")] + public required string Reason { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Workflow run identifier whose changed limits were declined. + [JsonPropertyName("runId")] + public required string RunId { get; set; } } -/// Identifies the target session. +/// The workflow_durable_failure variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentDeselectRequest +public partial class WorkflowRunFailureWorkflowDurableFailure : WorkflowRunFailure { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + [JsonIgnore] + public override string Type => "workflow_durable_failure"; -/// Custom agents available to the session after reloading definitions from disk. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentReloadResult -{ - /// Reloaded custom agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } -} + /// Stable failure code. + [JsonPropertyName("code")] + public required string Code { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentReloadRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Execution-critical durable operation that failed. + [JsonPropertyName("operation")] + public required WorkflowDurableOperation Operation { get; set; } -/// Identifier assigned to the newly started background agent task. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksStartAgentResult -{ - /// Generated agent ID for the background task. - [JsonPropertyName("agentId")] - public string AgentId { get; set; } = string.Empty; + /// Workflow run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } } -/// Agent type, prompt, name, and optional description and model override for the new task. +/// 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 TasksStartAgentRequest +public partial class WorkflowRunFailureWorkflowAccountingIncomplete : WorkflowRunFailure { - /// Type of agent to start (e.g., 'explore', 'task', 'general-purpose'). - [JsonPropertyName("agentType")] - public string AgentType { get; set; } = string.Empty; - - /// Short description of the task. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// + [JsonIgnore] + public override string Type => "workflow_accounting_incomplete"; - /// Optional model override. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Confirmed usage in nano-AIU, representing the floor of what the run spent. + [JsonPropertyName("drainedNanoAiu")] + public required long DrainedNanoAiu { get; set; } - /// Friendly, non-unique name used when displaying the agent. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Workflow run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } +} - /// Task prompt for the agent. - [JsonPropertyName("prompt")] - public string Prompt { 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 . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class WorkflowRunFailureWorkflowProviderDisconnected : WorkflowRunFailure +{ + /// + [JsonIgnore] + public override string Type => "workflow_provider_disconnected"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Workflow run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } } -/// Tracked task union returned by task APIs, containing an agent, client, or shell task. +/// Durable metadata describing who initiated a workflow pause. /// Polymorphic base type discriminated by type. [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 +[JsonDerivedType(typeof(WorkflowPauseInfoUser), "user")] +[JsonDerivedType(typeof(WorkflowPauseInfoCheckpoint), "checkpoint")] +public partial class WorkflowPauseInfo { /// The type discriminator. [JsonPropertyName("type")] @@ -11356,6833 +12500,6813 @@ public partial class TaskInfo } -/// Tracked background agent task metadata, including IDs, status, timing, agent type, prompt, model, result, and latest response. -/// The agent variant of . +/// The user variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoAgent : TaskInfo +public partial class WorkflowPauseInfoUser : WorkflowPauseInfo { /// [JsonIgnore] - public override string Type => "agent"; - - /// 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; } + public override string Type => "user"; +} - /// ISO 8601 timestamp when the task finished. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } +/// The checkpoint variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class WorkflowPauseInfoCheckpoint : WorkflowPauseInfo +{ + /// + [JsonIgnore] + public override string Type => "checkpoint"; - /// Short description of the task. - [JsonPropertyName("description")] - public required string Description { get; set; } + /// Stable author-defined checkpoint key that initiated the pause. + [JsonPropertyName("key")] + public required string Key { get; set; } +} - /// Friendly, non-unique name intended for display. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } +/// Complete current or terminal workflow run envelope. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowRunResult +{ + /// 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 when the task failed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Error message for an errored run. [JsonPropertyName("error")] public string? Error { get; set; } - /// Whether task execution is synchronously awaited or managed in the background. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("executionMode")] - public TaskExecutionMode? ExecutionMode { get; set; } + /// Machine-readable failure details for a halted or errored run. + [JsonPropertyName("failure")] + public WorkflowRunFailure? Failure { get; set; } - /// Unique task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } + /// Structured pause initiator metadata for a paused attempt. + [JsonPropertyName("pauseInfo")] + public WorkflowPauseInfo? PauseInfo { get; set; } - /// ISO 8601 timestamp when the agent entered idle state. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } + /// Reason for a halted or cancelled run. + [JsonPropertyName("reason")] + public string? Reason { get; set; } - /// Most recent response text from the agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("latestResponse")] - public string? LatestResponse { get; set; } + /// Completed workflow result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } - /// Requested model override for the task when specified. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { 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; } + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + [JsonPropertyName("snapshot")] + public JsonElement? Snapshot { get; set; } - /// Runtime model resolved for the task when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("resolvedModel")] - public string? ResolvedModel { get; set; } + /// Current or terminal workflow run status. + [JsonPropertyName("status")] + public WorkflowRunStatus Status { get; set; } +} - /// Result text from the task when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public string? Result { get; set; } +/// Wire-only per-invocation workflow resource ceiling overrides. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowRunLimits +{ + /// 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; } - /// ISO 8601 timestamp when the task was started. - [JsonPropertyName("startedAt")] - public required DateTimeOffset StartedAt { get; set; } + /// Maximum number of workflow subagents that may run concurrently. + [JsonPropertyName("maxConcurrentSubagents")] + public long? MaxConcurrentSubagents { get; set; } - /// Current lifecycle status of the task. - [JsonPropertyName("status")] - public required TaskStatus Status { get; set; } + /// Maximum total number of workflow subagents that may be admitted. + [JsonPropertyName("maxTotalSubagents")] + public long? MaxTotalSubagents { get; set; } - /// Tool call ID associated with this agent task. - [JsonPropertyName("toolCallId")] - public required string ToolCallId { 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; } } -/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. +/// Options controlling workflow invocation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskClientOwner +public sealed class WorkflowRunOptions { - /// ISO 8601 timestamp when the bound join disconnected. - [JsonPropertyName("disconnectedAt")] - public DateTimeOffset? DisconnectedAt { get; set; } + /// Per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Display-only owner name. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Whether to emit workflow phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } - /// Opaque identity of the currently or most recently bound session join. - [JsonPropertyName("joinId")] - public string JoinId { get; set; } = string.Empty; + /// Whether to notify the originating session when the workflow completes. + [JsonPropertyName("notifyOnComplete")] + public bool? NotifyOnComplete { get; set; } - /// Class of the task owner. - [JsonPropertyName("kind")] - public TaskClientOwnerKind Kind { get; set; } + /// Run identifier whose journal and progress should seed this resumed run. + [JsonPropertyName("resumeFromRunId")] + public string? ResumeFromRunId { get; set; } +} - /// Opaque session-scoped participant identity. - [JsonPropertyName("participantId")] - public string ParticipantId { get; set; } = string.Empty; +/// Parameters for invoking a registered workflow. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowRunRequest +{ + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } - /// Whether this task's bound join is currently connected. - [JsonPropertyName("presence")] - public TaskClientOwnerPresence Presence { get; set; } + /// Registered workflow name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Display-only owner source. - [JsonPropertyName("source")] - public string? Source { get; set; } + /// Workflow invocation options. + [JsonPropertyName("options")] + public WorkflowRunOptions? Options { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Tracked client-owned task metadata. -/// The client variant of . +/// Resolved persisted workflow identity and resumed run envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoClient : TaskInfo +public sealed class WorkflowResumeResult { - /// - [JsonIgnore] - public override string Type => "client"; + /// Terminal resumed run envelope. + [JsonPropertyName("run")] + public WorkflowRunResult Run { get => field ??= new(); set; } - /// ISO 8601 timestamp when the current active segment started. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("activeStartedAt")] - public DateTimeOffset? ActiveStartedAt { get; set; } + /// Persisted workflow name resolved for the resumed run. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; +} - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeTimeMs")] - public required long ActiveTimeMs { get; set; } +/// Parameters for resuming a workflow run from its persisted identity. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowResumeRequest +{ + /// Optional per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Whether the currently bound owner can receive a cancellation request. - [JsonPropertyName("canCancel")] - public required bool CanCancel { get; set; } + /// Whether to emit workflow phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } - /// Human-readable reason for terminal cancellation. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cancellationReason")] - public string? CancellationReason { get; set; } + /// Whether to notify the originating session when the workflow completes. + [JsonPropertyName("notifyOnComplete")] + public bool? NotifyOnComplete { get; set; } - /// Owner-scoped registration and reclaim key. - [JsonPropertyName("clientTaskId")] - public required string ClientTaskId { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// ISO 8601 timestamp when the task reached a terminal status. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Task description. - [JsonPropertyName("description")] - public required string Description { get; set; } +/// Options for an internal tool-originated workflow invocation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowToolRunOptions +{ + /// Per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Optional task display name. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Run identifier whose journal and progress should seed this resumed run. + [JsonPropertyName("resumeFromRunId")] + public string? ResumeFromRunId { get; set; } +} - /// Human-readable terminal failure message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("error")] - public string? Error { get; set; } +/// Internal parameters for invoking a registered workflow from a tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowToolRunRequest +{ + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } - /// Optional owner-supplied terminal failure code. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("errorCode")] - public string? ErrorCode { get; set; } + /// Registered workflow name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Execution mode, which is always background for client-owned tasks. - [JsonPropertyName("executionMode")] - public required TaskClientExecutionMode ExecutionMode { get; set; } + /// Tool-originated workflow invocation options. + [JsonPropertyName("options")] + public WorkflowToolRunOptions? Options { get; set; } - /// Canonical runtime-generated task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// ISO 8601 timestamp when the connected owner entered idle status. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } + /// Opaque identifier of the originating tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } +} - /// ISO 8601 timestamp of the most recent orphan transition. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("orphanedAt")] - public DateTimeOffset? OrphanedAt { get; set; } +/// Internal parameters for resuming a workflow run from a tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowToolResumeRequest +{ + /// Optional per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { 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; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Client task lifecycle status. - [JsonPropertyName("status")] - public required TaskClientStatus Status { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// ISO 8601 timestamp of the latest accepted lifecycle change. - [JsonPropertyName("updatedAt")] - public required DateTimeOffset UpdatedAt { get; set; } + /// Opaque identifier of the originating tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } } -/// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. -/// The shell variant of . +/// Parameters for retrieving a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoShell : TaskInfo +internal sealed class WorkflowGetRunRequest { - /// - [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; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Unique task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Path to the detached shell log, when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("logPath")] - public string? LogPath { get; set; } +/// Declared or approved workflow resource ceilings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowDeclaredLimits +{ + /// Maximum AI credits consumed by subagents and descendants. + [JsonPropertyName("maxAiCredits")] + public double? MaxAiCredits { get; set; } - /// Process ID when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pid")] - public long? Pid { get; set; } + /// Maximum concurrently active subagents. + [JsonPropertyName("maxConcurrentSubagents")] + public long? MaxConcurrentSubagents { get; set; } - /// ISO 8601 timestamp when the task was started. - [JsonPropertyName("startedAt")] - public required DateTimeOffset StartedAt { get; set; } + /// Maximum total subagents spawned by the run. + [JsonPropertyName("maxTotalSubagents")] + public long? MaxTotalSubagents { get; set; } - /// Current lifecycle status of the task. - [JsonPropertyName("status")] - public required TaskStatus Status { get; set; } + /// Maximum accumulated active execution time in seconds. + [JsonPropertyName("timeoutSeconds")] + public double? TimeoutSeconds { get; set; } } -/// Background tasks currently tracked by the session. +/// Durable workflow resource consumption. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskList +public sealed class WorkflowRunConsumed { - /// Currently tracked tasks. - [JsonPropertyName("tasks")] - public IList Tasks { get => field ??= []; set; } + /// 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; } + + /// Total subagents spawned by the run. + [JsonPropertyName("subagents")] + public long Subagents { get; set; } } -/// Identifies the target session. +/// Current workflow phase identity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksListRequest +public sealed class WorkflowCurrentPhase { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Current phase identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Zero-based declared phase ordinal, or null for an undeclared phase. + [JsonPropertyName("ordinal")] + public long? Ordinal { get; set; } } -/// Tracked client-owned task metadata. +/// Prompt-safe terminal workflow outcome. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskClientInfo +public sealed class WorkflowRunTerminal { - /// ISO 8601 timestamp when the current active segment started. - [JsonPropertyName("activeStartedAt")] - public DateTimeOffset? ActiveStartedAt { get; set; } + /// Human-readable terminal error. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeTimeMs")] - public long ActiveTimeMs { get; set; } + /// Machine-readable terminal failure. + [JsonPropertyName("failure")] + public WorkflowRunFailure? Failure { get; set; } - /// Whether the currently bound owner can receive a cancellation request. - [JsonPropertyName("canCancel")] - public bool CanCancel { get; set; } + /// Pause initiator metadata, or null when the run did not pause. + [JsonPropertyName("pauseInfo")] + public WorkflowPauseInfo? PauseInfo { get; set; } - /// Human-readable reason for terminal cancellation. - [JsonPropertyName("cancellationReason")] - public string? CancellationReason { get; set; } + /// Human-readable terminal reason. + [JsonPropertyName("reason")] + public string? Reason { get; set; } - /// Owner-scoped registration and reclaim key. - [JsonPropertyName("clientTaskId")] - public string ClientTaskId { get; set; } = string.Empty; + /// Prompt-safe preview of the completed result. + [JsonPropertyName("resultPreview")] + public string? ResultPreview { get; set; } +} - /// ISO 8601 timestamp when the task reached a terminal status. - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } +/// Durable workflow run summary with read-time live overlays. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowRunSummary +{ + /// Epoch milliseconds when the current active segment started, or null while inactive. + [JsonPropertyName("activeSegmentStartedAt")] + public long? ActiveSegmentStartedAt { get; set; } - /// Task description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Approved effective resource ceilings, or null until approved. + [JsonPropertyName("approved")] + public WorkflowDeclaredLimits? Approved { get; set; } - /// Optional task display name. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Whether the durable run state currently passes runtime resume eligibility checks. + [JsonPropertyName("canResume")] + public bool CanResume { get; set; } - /// Human-readable terminal failure message. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Epoch milliseconds when the run completed, or null while nonterminal. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } - /// Optional owner-supplied terminal failure code. - [JsonPropertyName("errorCode")] - public string? ErrorCode { get; set; } + /// Durable resource consumption. + [JsonPropertyName("consumed")] + public WorkflowRunConsumed Consumed { get => field ??= new(); set; } - /// Execution mode, which is always background for client-owned tasks. - [JsonPropertyName("executionMode")] - public TaskClientExecutionMode ExecutionMode { get; set; } + /// Epoch milliseconds when the run was created. + [JsonPropertyName("createdAt")] + public long CreatedAt { get; set; } - /// Canonical runtime-generated task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Current phase identity, or null before any phase is entered. + [JsonPropertyName("currentPhase")] + public WorkflowCurrentPhase? CurrentPhase { get; set; } - /// ISO 8601 timestamp when the connected owner entered idle status. - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } + /// Resource ceilings declared by the workflow. + [JsonPropertyName("declaredLimits")] + public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } - /// ISO 8601 timestamp of the most recent orphan transition. - [JsonPropertyName("orphanedAt")] - public DateTimeOffset? OrphanedAt { get; set; } + /// Number of phases declared by the workflow. + [JsonPropertyName("declaredPhaseCount")] + public long DeclaredPhaseCount { get; set; } - /// Public attribution and presence for the task owner. - [JsonPropertyName("owner")] - public TaskClientOwner Owner { get => field ??= new(); set; } + /// Human-readable workflow description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// ISO 8601 timestamp of the most recent successful reclaim. - [JsonPropertyName("reclaimedAt")] - public DateTimeOffset? ReclaimedAt { get; set; } + /// Number of direct workflow agents currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } - /// Opaque successful terminal result supplied by the task owner. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Epoch milliseconds when this live-overlay snapshot was observed. + [JsonPropertyName("observedAt")] + public long ObservedAt { get; set; } - /// Sequence number of the latest accepted owner update. - [JsonPropertyName("sequence")] - public long Sequence { get; set; } + /// Monotonic durable run revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } - /// ISO 8601 timestamp when the task started. + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; + + /// Epoch milliseconds when execution first started, or null before start. [JsonPropertyName("startedAt")] - public DateTimeOffset StartedAt { get; set; } + public long? StartedAt { get; set; } - /// Client task lifecycle status. + /// Current workflow run status. [JsonPropertyName("status")] - public TaskClientStatus Status { get; set; } + public WorkflowRunStatus Status { get; set; } - /// Task kind. - [JsonPropertyName("type")] - public TaskClientType Type { get; set; } + /// Terminal run outcome, or null while nonterminal. + [JsonPropertyName("terminal")] + public WorkflowRunTerminal? Terminal { get; set; } - /// ISO 8601 timestamp of the latest accepted lifecycle change. + /// 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 DateTimeOffset UpdatedAt { get; set; } + public long UpdatedAt { get; set; } + + /// Registered workflow name. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; } -/// Result of registering or reclaiming a client-owned task. +/// A page of workflow runs in durable creation order. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRegisterResult +public sealed class WorkflowListRunsResult { - /// True only when this invocation created a new task. - [JsonPropertyName("created")] - public bool Created { get; set; } + /// Whether terminal runs newer than this page exist. + [JsonPropertyName("hasMoreNewer")] + public bool? HasMoreNewer { get; set; } - /// True only when this invocation reclaimed an orphaned task. - [JsonPropertyName("reclaimed")] - public bool Reclaimed { get; set; } + /// Newest terminal-run cursor in this page, or null when the terminal window is empty. + [JsonPropertyName("newestSeq")] + public long? NewestSeq { get; set; } - /// Authoritative registered or reclaimed task. - [JsonPropertyName("task")] - public TaskClientInfo Task { get => field ??= new(); set; } + /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. + [JsonPropertyName("oldestSeq")] + public long? OldestSeq { get; set; } + + /// Number of terminal runs older than this page. + [JsonPropertyName("omittedOlder")] + public long? OmittedOlder { get; set; } + + /// Workflow run summaries in durable creation order. + [JsonPropertyName("runs")] + public IList Runs { get => field ??= []; set; } } -/// Registers or reclaims a client-owned task. +/// Parameters for paging workflow runs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksRegisterRequest +internal sealed class WorkflowListRunsRequest { - /// 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; + /// Exclusive forward cursor. + [JsonPropertyName("afterSeq")] + public long? AfterSeq { get; set; } - /// Optional short display name for the external work. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Exclusive backward cursor. + [JsonPropertyName("beforeSeq")] + public long? BeforeSeq { get; set; } - /// Expected current sequence for idempotent registration or orphan reclaim. - [JsonPropertyName("expectedSequence")] - public long? ExpectedSequence { 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; - - /// Task kind. - [JsonPropertyName("type")] - public TaskClientType Type { get; set; } } -/// Result of publishing a client-owned task update. +/// Prompt-safe durable identity and live status for a direct workflow agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksUpdateResult +public sealed class WorkflowAgentSummary { - /// Whether this invocation changed task state. - [JsonPropertyName("applied")] - public bool Applied { get; set; } + /// Accumulated active agent time in milliseconds. + [JsonPropertyName("activeMs")] + public long ActiveMs { get; set; } - /// Whether this invocation repeated the latest accepted update. - [JsonPropertyName("duplicate")] - public bool Duplicate { get; set; } + /// Prompt-safe live activity text. + [JsonPropertyName("activity")] + public string? Activity { get; set; } - /// Authoritative task after processing the update. - [JsonPropertyName("task")] - public TaskClientInfo Task { get => field ??= new(); set; } -} + /// Stable direct-agent identifier. + [JsonPropertyName("agentId")] + public string AgentId { get; set; } = string.Empty; -/// 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; -} + /// Registered agent type. + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; + /// Epoch milliseconds when the agent completed. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } -/// Publishes nonterminal progress for a running or idle client task. -/// The progress variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateProgress : TaskClientUpdate -{ - /// - [JsonIgnore] - public override string Kind => "progress"; + /// Friendly, non-unique name intended for display. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Optional progress message appended to recent activity when nonempty. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Friendly, non-unique name intended for display. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; - /// Optional completion percentage; null clears the current percentage. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("percentage")] - public double? Percentage { get; set; } + /// Phase identifier active when the agent was launched, or null. + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } - /// Optional progress phase; null clears the current phase. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("phase")] - public string? Phase { get; set; } + /// Model requested when the agent was launched. + [JsonPropertyName("requestedModel")] + public string? RequestedModel { get; set; } - /// Optional active status transition. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("status")] - public TaskClientActiveStatus? Status { get; set; } -} + /// Concrete model resolved for the agent. + [JsonPropertyName("resolvedModel")] + public string? ResolvedModel { get; set; } -/// Reports successful terminal completion. -/// The completed variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateCompleted : TaskClientUpdate -{ - /// - [JsonIgnore] - public override string Kind => "completed"; + /// Owning workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Optional final progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Epoch milliseconds when the agent started. + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } - /// Optional opaque successful terminal result. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Current durable or live agent status. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + /// Tool-call identifier that launched the agent. + [JsonPropertyName("toolCallId")] + public string ToolCallId { get; set; } = string.Empty; } -/// Reports terminal failure. -/// The failed variant of . +/// Durable lifecycle and timing for one workflow phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateFailed : TaskClientUpdate +public sealed class WorkflowPhaseObservation { - /// - [JsonIgnore] - public override string Kind => "failed"; + /// Completed active time accumulated by this phase in milliseconds. + [JsonPropertyName("accumulatedActiveMs")] + public long AccumulatedActiveMs { get; set; } - /// Optional owner-supplied terminal failure code. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("code")] - public string? Code { 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; } - /// Human-readable terminal failure message. - [JsonPropertyName("error")] - public required string Error { get; set; } + /// Current live active time for this phase in milliseconds. + [JsonPropertyName("currentActiveMs")] + public long CurrentActiveMs { get; set; } - /// Optional final progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } -} + /// Optional human-readable phase detail. + [JsonPropertyName("detail")] + public string? Detail { get; set; } -/// Reports terminal cancellation after external work stopped. -/// The cancelled variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateCancelled : TaskClientUpdate -{ - /// - [JsonIgnore] - public override string Kind => "cancelled"; + /// Number of times execution entered this phase. + [JsonPropertyName("entryCount")] + public long EntryCount { get; set; } - /// 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; } -} - -/// Updates a client-owned task. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksUpdateRequest -{ - /// Canonical runtime-generated task identifier. + /// Phase identifier. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Owner update sequence to apply. - [JsonPropertyName("sequence")] - public long Sequence { 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Direct agents in this phase that are currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } - /// Progress or terminal update payload. - [JsonPropertyName("update")] - public TaskClientUpdate Update { get => field ??= new(); set; } -} + /// Zero-based declared phase ordinal, or null for an undeclared phase. + [JsonPropertyName("ordinal")] + public long? Ordinal { get; 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRefreshResult -{ -} + /// 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; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksRefreshRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Derived lifecycle state of the phase. + [JsonPropertyName("status")] + public WorkflowPhaseStatus Status { get; set; } -/// 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 -{ -} + /// Human-readable phase title. + [JsonPropertyName("title")] + public string Title { 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; + /// Total direct agents associated with this phase. + [JsonPropertyName("totalAgentCount")] + public long TotalAgentCount { get; set; } } -/// 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. +/// One durable workflow progress record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(TaskProgressAgent), "agent")] -[JsonDerivedType(typeof(TaskProgressClient), "client")] -[JsonDerivedType(typeof(TaskProgressShell), "shell")] -public partial class TaskProgress +public sealed class WorkflowProgressLine { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} + /// Resume attempt that emitted this record. + [JsonPropertyName("attempt")] + public long Attempt { get; set; } + /// Progress record kind. + [JsonPropertyName("kind")] + public WorkflowLogLineKind Kind { get; set; } -/// Timestamped display line for task progress output or recent agent activity. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskProgressLine -{ - /// Display message, e.g., "▸ bash", "✓ edit src/foo.ts". - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Phase active when the record was emitted, or null before any phase. + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } - /// ISO 8601 timestamp when this event occurred. - [JsonPropertyName("timestamp")] - public DateTimeOffset Timestamp { 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; } -/// Progress snapshot for an agent task, with recent activity lines and optional latest intent. -/// The agent variant of . +/// A bidirectional page of workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressAgent : TaskProgress +public sealed class WorkflowProgressPage { - /// - [JsonIgnore] - public override string Type => "agent"; + /// Whether progress records newer than this page exist. + [JsonPropertyName("hasMoreNewer")] + public bool HasMoreNewer { get; set; } - /// The most recent intent reported by the agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("latestIntent")] - public string? LatestIntent { get; set; } + /// Whether progress records older than this page exist. + [JsonPropertyName("hasMoreOlder")] + public bool HasMoreOlder { get; set; } - /// Recent tool execution events converted to display lines. - [JsonPropertyName("recentActivity")] - public required IList RecentActivity { 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; } } -/// Generic progress for a client-owned task. -/// The client variant of . +/// Full workflow run observability detail. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressClient : TaskProgress +public sealed class WorkflowRunDetail { - /// - [JsonIgnore] - public override string Type => "client"; + /// Epoch milliseconds when the current active segment started, or null while inactive. + [JsonPropertyName("activeSegmentStartedAt")] + public long? ActiveSegmentStartedAt { get; set; } - /// Most recent nonempty progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("lastMessage")] - public string? LastMessage { get; set; } + /// Durable identities and live statuses for direct workflow agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } - /// Current completion percentage from zero through one hundred. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("percentage")] - public double? Percentage { get; set; } + /// Approved effective resource ceilings, or null until approved. + [JsonPropertyName("approved")] + public WorkflowDeclaredLimits? Approved { get; set; } - /// Current owner-defined progress phase. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("phase")] - public string? Phase { get; set; } + /// Whether the durable run state currently passes runtime resume eligibility checks. + [JsonPropertyName("canResume")] + public bool CanResume { get; set; } - /// Recent server-timestamped progress messages. - [JsonPropertyName("recentActivity")] - public required IList RecentActivity { get; set; } + /// Epoch milliseconds when the run completed, or null while nonterminal. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } - /// Sequence number of the latest accepted owner update. - [JsonPropertyName("sequence")] - public required long Sequence { get; set; } + /// Durable resource consumption. + [JsonPropertyName("consumed")] + public WorkflowRunConsumed Consumed { get => field ??= new(); set; } - /// Current client task lifecycle status. + /// 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; } + + /// Number of phases declared by the workflow. + [JsonPropertyName("declaredPhaseCount")] + public long DeclaredPhaseCount { get; set; } + + /// Human-readable workflow description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Number of direct workflow agents currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } + + /// Epoch milliseconds when this live-overlay snapshot was observed. + [JsonPropertyName("observedAt")] + public long ObservedAt { get; set; } + + /// Lifecycle and timing observations for each workflow phase. + [JsonPropertyName("phases")] + public IList Phases { get => field ??= []; set; } + + /// Bidirectional page of durable workflow progress. + [JsonPropertyName("progress")] + public WorkflowProgressPage Progress { get => field ??= new(); set; } + + /// Monotonic durable run revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; + + /// Epoch milliseconds when execution first started, or null before start. + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } + + /// Current workflow run status. [JsonPropertyName("status")] - public required TaskClientStatus Status { get; set; } + public WorkflowRunStatus Status { get; set; } - /// ISO 8601 timestamp of the latest accepted lifecycle change. + /// 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 required DateTimeOffset UpdatedAt { get; set; } + public long UpdatedAt { get; set; } + + /// Registered workflow name. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; } -/// Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. -/// The shell variant of . +/// Parameters for paging workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressShell : TaskProgress +internal sealed class WorkflowGetRunProgressRequest { - /// - [JsonIgnore] - public override string Type => "shell"; + /// Exclusive forward cursor. + [JsonPropertyName("afterSeq")] + public long? AfterSeq { get; set; } - /// Process ID when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pid")] - public long? Pid { get; set; } + /// Exclusive backward cursor. + [JsonPropertyName("beforeSeq")] + public long? BeforeSeq { get; set; } - /// Recent stdout/stderr lines from the running shell command. - [JsonPropertyName("recentOutput")] - public required string RecentOutput { get; set; } -} + /// Maximum records to return. Defaults to 200 and is capped at 500. + [JsonPropertyName("limit")] + public int? Limit { get; set; } -/// 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; } -} + /// Optional phase identifier used to scope records and cursors. + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } -/// Identifier of the background task to fetch progress for. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksGetProgressRequest -{ - /// Task identifier (agent ID or shell ID). - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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 first sync-waiting task that can currently be promoted to background mode. +/// Parameters for cancelling a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksGetCurrentPromotableResult +internal sealed class WorkflowCancelRequest { - /// 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; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Parameters for pausing a running workflow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksGetCurrentPromotableRequest +internal sealed class WorkflowPauseRequest { + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the task was successfully promoted to background mode. +/// RPC data type for SessionWorkflowPauseAtCheckpoint operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksPromoteToBackgroundResult +internal sealed class SessionWorkflowPauseAtCheckpointResult { - /// Whether the task was successfully promoted to background mode. - [JsonPropertyName("promoted")] - public bool Promoted { get; set; } + /// Whether this execution attempt must pause or may continue. + [JsonPropertyName("action")] + public WorkflowPauseCheckpointAction Action { get; set; } } -/// Identifier of the task to promote to background mode. +/// Parameters for an owned durable pause checkpoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksPromoteToBackgroundRequest +internal sealed class WorkflowPauseCheckpointRequest { - /// Task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; } -/// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. +/// Acknowledgement that a workflow request was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksPromoteCurrentToBackgroundResult +public sealed class WorkflowAckResult { - /// 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; } } -/// Identifies the target session. +/// One ordered workflow progress line. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksPromoteCurrentToBackgroundRequest +public sealed class WorkflowLogLine { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Progress line kind. + [JsonPropertyName("kind")] + public WorkflowLogLineKind Kind { get; set; } -/// Indicates whether the background task was successfully cancelled. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksCancelResult -{ - /// Whether the task was successfully cancelled. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// 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; } -/// Identifier of the background task to cancel. +/// Parameters for recording workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksCancelRequest +internal sealed class WorkflowLogRequest { - /// Task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; /// 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. +/// Result of one workflow-scoped subagent call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRemoveResult +public sealed class WorkflowAgentResult { - /// 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; } + /// Agent result, omitted when the agent produced no result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } } -/// Identifier of the completed or cancelled task to remove from tracking. +/// Options for one workflow-scoped subagent call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksRemoveRequest +public sealed class WorkflowAgentOptions { - /// Task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Optional built-in or custom agent name whose definition configures the subagent. + [JsonPropertyName("agent")] + public string? Agent { get; set; } + + /// Optional context tier override for the subagent. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } + + /// Optional label distinguishing otherwise identical memoized agent calls. + [JsonPropertyName("label")] + public string? Label { get; set; } + + /// 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; } +} + +/// Parameters for one workflow-scoped subagent call. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowAgentRequest +{ + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; + + /// 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; } -/// Indicates whether the message was delivered, with an error message when delivery failed. +/// Result of reading a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksSendMessageResult +public sealed class WorkflowJournalGetResult { - /// Error message if delivery failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Whether the journal contained the requested key. + [JsonPropertyName("hit")] + public bool Hit { get; set; } - /// Whether the message was successfully delivered or steered. - [JsonPropertyName("sent")] - public bool Sent { get; set; } + /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. + [JsonPropertyName("resultJson")] + public JsonElement? ResultJson { get; set; } } -/// Identifier of the target agent task, message content, and optional sender agent ID. +/// Parameters for reading a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksSendMessageRequest +internal sealed class WorkflowJournalGetRequest { - /// Agent ID of the sender, if sent on behalf of another agent. - [JsonPropertyName("fromAgentId")] - public string? FromAgentId { get; set; } + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; - /// Agent task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Namespaced journal key. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; - /// Message content to send to the agent. - [JsonPropertyName("message")] - public string Message { 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; } -/// Skill metadata available to a session, with name, description, source, enabled/invocable state, path, plugin, and argument hint. +/// Parameters for storing a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Skill +internal sealed class WorkflowJournalPutRequest { - /// 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; } + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; - /// Source location type (e.g., project, personal-copilot, plugin, builtin). - [JsonPropertyName("source")] - public SkillSource Source { get; set; } + /// Namespaced journal key. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; - /// Whether the skill can be invoked by the user as a slash command. - [JsonPropertyName("userInvocable")] - public bool UserInvocable { get; set; } -} + /// JSON result to memoize. + [JsonPropertyName("resultJson")] + public JsonElement ResultJson { get; set; } -/// Skills available to the session, with their enabled state. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillList -{ - /// Available skills. - [JsonPropertyName("skills")] - public IList Skills { get => field ??= []; set; } -} + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsListRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Skill invocation record with name, path, content, allowed tools, and turn number. +/// 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 SkillsInvokedSkill +public sealed class CurrentModel { - /// Tools that should be auto-approved when this skill is active, captured at invocation time. - [JsonPropertyName("allowedTools")] - public IList? AllowedTools { get; set; } + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } - /// Full content of the skill file. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// 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; } - /// Whether model invocation was disabled when this skill was invoked. - [JsonPropertyName("disableModelInvocation")] - public bool? DisableModelInvocation { get; set; } + /// Context tier for models that support multiple context-window sizes. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } - /// Turn number when the skill was invoked. - [JsonPropertyName("invokedAtTurn")] - public long InvokedAtTurn { get; set; } + /// Currently active model identifier. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } - /// Unique identifier for the skill. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// 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; } - /// 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; -} + /// 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; } -/// 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; } + /// 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 SessionSkillsGetInvokedRequest +internal sealed class SessionModelGetCurrentRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Name of the skill to enable for the session. +/// RPC data type for ModelSwitchConfirmation operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsEnableRequest +public sealed class ModelSwitchConfirmation { - /// Name of the skill to enable. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Current conversation token count before switching models. + [JsonPropertyName("currentTokens")] + public double CurrentTokens { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } -/// Name of the skill to disable for the session. +/// The model identifier active on the session after the switch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsDisableRequest +public sealed class ModelSwitchToResult { - /// Name of the skill to disable. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// 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; } -/// 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; + /// 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; } + + /// User-facing warning produced while applying the model switch. + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsEnsureLoadedRequest +/// Environment variables consulted while resolving model-picker settings. +public sealed class ModelPickerSettingsContextEnvironment { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; } -/// Recorded MCP server connection failure. +/// Filesystem and environment context used to resolve model-picker settings. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerFailureInfo +public sealed class ModelPickerSettingsContext { - /// Failure message produced when the MCP server connection failed. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Optional Copilot configuration directory containing persisted settings. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } - /// epoch-ms timestamp at which the failure was recorded. - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } + /// Environment variables consulted while resolving model-picker settings. + [JsonPropertyName("environment")] + public ModelPickerSettingsContextEnvironment Environment { get => field ??= new(); set; } + + /// User home directory used when resolving persisted settings. + [JsonPropertyName("homeDirectory")] + public string HomeDirectory { get; set; } = string.Empty; } -/// Recorded MCP server pending-auth state. +/// RPC data type for ModelPickerPersistence operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerNeedsAuthInfo +public sealed class ModelPickerPersistenceRequest { - /// epoch-ms timestamp at which the server signalled it needs authentication. - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } + /// 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; } + + /// Filesystem and environment context used to resolve settings persistence. + [JsonPropertyName("settingsContext")] + public ModelPickerSettingsContext SettingsContext { get => field ??= new(); set; } } -/// Host-level state, omitted when no MCP host is initialized. +/// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpHostState +internal sealed class ModelSwitchToRequest { - /// Names of currently-connected MCP clients. - [JsonPropertyName("clients")] - public IList Clients { get => field ??= []; set; } + /// 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; } - /// Configured servers that are explicitly disabled. - [JsonPropertyName("disabledServers")] - public IList DisabledServers { get => field ??= []; 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; } - /// Map of server name to recorded connection failure. - [JsonPropertyName("failedServers")] - public IDictionary FailedServers { get => field ??= new Dictionary(); 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; } - /// Configured servers filtered out by MCP server policy. - [JsonPropertyName("filteredServers")] - public IList FilteredServers { get => field ??= []; 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; } - /// Whether third-party MCP servers are policy-enabled for this session. - [JsonPropertyName("mcp3pEnabled")] - public bool Mcp3pEnabled { get; set; } + /// Override individual model capabilities resolved by the runtime. + [JsonPropertyName("modelCapabilities")] + public ModelCapabilitiesOverride? ModelCapabilities { get; set; } - /// Map of server name to recorded pending-auth state. - [JsonPropertyName("needsAuthServers")] - public IDictionary NeedsAuthServers { get => field ??= new Dictionary(); set; } + /// Settings scope used when persisting the selected model. + [JsonPropertyName("modelChangeScope")] + public string? ModelChangeScope { get; set; } - /// Names of servers with in-flight connection attempts. - [JsonPropertyName("pendingConnections")] - public IList PendingConnections { get => field ??= []; set; } -} + /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; -/// MCP server status entry, including config source/plugin source and any connection error. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServer -{ - /// Human-readable display name supplied by a managed server catalog. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Optional settings context and explicit-override flags used to persist a picker selection. + [JsonPropertyName("pickerPersistence")] + public ModelPickerPersistenceRequest? PickerPersistence { get; set; } - /// Error message if the server failed to connect. - [JsonPropertyName("error")] - public string? Error { 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; } - /// 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; + /// Reasoning summary mode to request for supported model clients. + [JsonPropertyName("reasoningSummary")] + public ReasoningSummary? ReasoningSummary { 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; } + /// Optional repository settings scope to persist after the switch commits. + [JsonPropertyName("repoScope")] + public string? RepoScope { get; set; } - /// Configuration source: user, workspace, plugin, builtin, or managed. - [JsonPropertyName("source")] - public McpServerSource? Source { get; set; } + /// Require the target to be currently available and enabled before applying the switch. + [JsonPropertyName("requireAvailable")] + public bool? RequireAvailable { get; set; } - /// Plugin name that provided this server, when source is plugin. - [JsonPropertyName("sourcePlugin")] - public string? SourcePlugin { get; set; } + /// When true, evaluate context-window compaction policy before applying the switch. + [JsonPropertyName("runCompactionPreflight")] + public bool? RunCompactionPreflight { get; set; } - /// Plugin version that provided this server, when source is plugin. - [JsonPropertyName("sourcePluginVersion")] - public string? SourcePluginVersion { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured. - [JsonPropertyName("status")] - public McpServerStatus Status { 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; } } -/// MCP servers configured for the session, with their connection status and host-level state. +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerList +public sealed class ModelSwitchAutoTierResult { - /// Host-level state, omitted when no MCP host is initialized. - [JsonPropertyName("host")] - public McpHostState? Host { get; set; } + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } - /// Configured MCP servers. - [JsonPropertyName("servers")] - public IList Servers { get => field ??= []; set; } + /// Auto preference currently committed for the session. + [JsonPropertyName("effectiveAutoTier")] + public AutoTier? EffectiveAutoTier { get; set; } + + /// Latest unclaimed Auto preference waiting for a future user turn. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } + + /// Immediate request status. `pending` means accepted but not committed. + [JsonPropertyName("status")] + public ModelSwitchAutoTierStatus Status { get; set; } + + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + [JsonPropertyName("supersededAutoTier")] + public AutoTier? SupersededAutoTier { get; set; } } -/// Identifies the target session. +/// 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 SessionMcpListRequest +internal sealed class ModelSwitchAutoTierRequest { + /// 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; } } -/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. +/// Managed, repository, and CLI model overrides to overlay onto the session at startup. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpToolUi +internal sealed class ModelApplyStartupOverlayRequest { - /// 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; } + /// 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; } - /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. - [JsonPropertyName("visibility")] - public IList? Visibility { get; set; } -} + /// Model explicitly selected by the CLI, when provided. + [JsonPropertyName("cliModel")] + public string? CliModel { 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; } + /// Whether the overlay is being applied while resuming a deferred session. + [JsonPropertyName("deferredResume")] + public bool? DeferredResume { get; set; } - /// Tool name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Model required by device-managed policy, when configured. + [JsonPropertyName("deviceManagedModel")] + public string? DeviceManagedModel { 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; } -} + /// 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; } -/// Tools exposed by the connected MCP server. Throws when the server is not connected. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpListToolsResult -{ - /// Tools exposed by the server. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } -} + /// 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; } -/// 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; + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + [JsonPropertyName("policyHelperModel")] + public string? PolicyHelperModel { get; set; } + + /// Auto routing preference selected by repository settings, when configured. Applied only when the overlay selects the Auto model; beside a concrete model it stays dormant. + [JsonPropertyName("repoAutoTier")] + public string? RepoAutoTier { get; set; } + + /// Context tier selected by repository settings, when configured. + [JsonPropertyName("repoContextTier")] + public string? RepoContextTier { get; set; } + + /// Model selected by repository settings, when configured. + [JsonPropertyName("repoModel")] + public string? RepoModel { get; set; } + + /// Reasoning effort selected by repository settings, when configured. + [JsonPropertyName("repoReasoningEffort")] + public string? RepoReasoningEffort { get; set; } + + /// Model required by server-managed policy, when configured. + [JsonPropertyName("serverManagedModel")] + public string? ServerManagedModel { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Name of the MCP server to enable for the session. +/// The applied host allowlist and effective session model policy after intersection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpEnableRequest +public sealed class ModelSetAllowedModelsResult { - /// 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; + /// 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Name of the MCP server to disable for the session. +/// Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpDisableRequest +internal sealed class ModelSetAllowedModelsRequest { - /// 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; + /// Exact model IDs to permit, or null to clear the host restriction. + [JsonPropertyName("allowedModels")] + public IList? AllowedModels { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// 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)] -internal sealed class SessionMcpReloadRequest +public sealed class ModelSetReasoningEffortResult +{ + /// Reasoning effort level recorded on the session after the update. + [JsonPropertyName("reasoningEffort")] + public string ReasoningEffort { get; set; } = string.Empty; +} + +/// Reasoning effort level to apply to the currently selected model. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelSetReasoningEffortRequest { + /// Reasoning effort level to apply to the currently selected model. The host is responsible for validating the value against the model's supported levels before calling. + [JsonPropertyName("reasoningEffort")] + public string ReasoningEffort { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of moving in-flight MCP loading to the background. +/// Cost-category metadata for a CAPI model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MoveMcpLoadingToBackgroundResult +public sealed class SessionModelPriceCategory { - /// 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; } -} + /// 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 SessionMcpMoveLoadingToBackgroundRequest -{ - /// 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; } } -/// MCP server allowed by policy, with server name and optional PII-free explanatory note. +/// 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 McpAllowedServer +public sealed class ModelProviderDescriptor { - /// Allowed server name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Opaque, stable provider id, stamped onto every model this provider returns. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// PII-free note explaining why the server was allowed. - [JsonPropertyName("redactedNote")] - public string? RedactedNote { get; set; } -} + /// The neutral provider kind. + [JsonPropertyName("kind")] + public ModelProviderKind Kind { 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; } + /// Human-readable menu label, owned by the runtime so every consumer renders identical text. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; - /// The config key of the server that failed to connect. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Stable ordering key for presenting providers in a deterministic sequence. + [JsonPropertyName("ordering")] + public long Ordering { get; set; } } -/// MCP server filtered by policy, with name, reason, and optional redacted reason. +/// The list of models available to this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpFilteredServer +public sealed class SessionModelList { - /// 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; } + /// 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; } - /// Filtered server name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. + [JsonPropertyName("modelPriceCategories")] + public IList? ModelPriceCategories { get; set; } - /// Human-readable filter reason. - [JsonPropertyName("reason")] - public string Reason { 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; } - /// PII-free filter reason. - [JsonPropertyName("redactedReason")] - public string? RedactedReason { get; set; } + /// Per-quota snapshots returned alongside the model list, keyed by quota type. + [JsonPropertyName("quotaSnapshots")] + public IDictionary? QuotaSnapshots { get; set; } } -/// MCP server startup filtering result. +/// RPC data type for SessionModelList operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStartServersResult +public sealed class SessionModelListRequest { - /// 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; } + /// If true, bypasses the per-session model list cache and re-fetches from CAPI. + [JsonPropertyName("skipCache")] + public bool? SkipCache { get; set; } } -/// Opaque MCP reload configuration. +/// RPC data type for SessionModelListRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpReloadWithConfigRequest +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; } } -/// 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. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpExecuteSamplingResult +internal sealed class SessionModeGetRequest { + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Outcome of an MCP sampling execution: success result, failure error, or cancellation. +/// 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 McpSamplingExecutionResult +public sealed class ModeSetResult { - /// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. - [JsonPropertyName("action")] - public McpSamplingExecutionAction Action { get; set; } + /// Whether the host should arm an interactive continuation after the mode change. + [JsonPropertyName("armInteractiveContinuation")] + public bool? ArmInteractiveContinuation { get; set; } - /// Error description, present when action='failure'. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Compaction confirmation required before the mode change can complete. + [JsonPropertyName("confirmation")] + public ModelSwitchConfirmation? Confirmation { 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 the host must defer implementing the requested mode change. + [JsonPropertyName("deferImplementation")] + public bool? DeferImplementation { 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 -{ -} + /// Deprecation warnings associated with the model selected by the mode change. + [JsonPropertyName("deprecationWarnings")] + public IList? DeprecationWarnings { 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; } + /// User-facing outcome message for the model switch triggered by the mode change. + [JsonPropertyName("message")] + public string? Message { 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; } + /// 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; } - /// 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; + /// Whether applying the mode changed the active model. + [JsonPropertyName("modelChanged")] + public bool ModelChanged { get; set; } - /// Name of the MCP server that initiated the sampling request. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Lifecycle status of the requested mode change. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// User-facing warning produced while applying the mode change. + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. +/// Agent interaction mode to apply to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpCancelSamplingExecutionResult +internal sealed class ModeSetRequest { - /// 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; } -} + /// Explicit response to a model-switch compaction preflight. + [JsonPropertyName("compactionDecision")] + public string? CompactionDecision { 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; + /// 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; } + + /// Session whose plan-mode base state should be inherited. + [JsonPropertyName("inheritPlanBaseFromSessionId")] + public string? InheritPlanBaseFromSessionId { get; set; } + + /// The session mode the agent is operating in. + [JsonPropertyName("mode")] + public SessionMode Mode { get; set; } + + /// Whether the selected plan model should be persisted. + [JsonPropertyName("persistPlanSelection")] + public bool? PersistPlanSelection { get; set; } + + /// Settings context used when persisting the selected plan model. + [JsonPropertyName("pickerSettingsContext")] + public ModelPickerSettingsContext? PickerSettingsContext { get; set; } + + /// Context tier to use with the dedicated plan model. + [JsonPropertyName("planContextTier")] + public string? PlanContextTier { get; set; } + + /// 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; } -/// Env-value mode recorded on the session after the update. +/// The session's friendly name, or null when not yet set. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpSetEnvValueModeResult +public sealed class NameGetResult { - /// Mode recorded on the session after the update. - [JsonPropertyName("mode")] - public McpSetEnvValueModeDetails Mode { get; set; } + /// The session name (user-set or auto-generated), or null if not yet set. + [JsonPropertyName("name")] + public string? Name { get; set; } } -/// Mode controlling how MCP server env values are resolved (`direct` or `indirect`). +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpSetEnvValueModeParams +internal sealed class SessionNameGetRequest { - /// 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; } -/// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). +/// New friendly name to apply to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpRemoveGitHubResult +internal sealed class NameSetRequest { - /// 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; } -} + /// 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; -/// 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; } -/// Result of configuring GitHub MCP. +/// Indicates whether the auto-generated summary was applied as the session's name. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigureGitHubResult +public sealed class NameSetAutoResult { - /// Whether GitHub MCP configuration changed. - [JsonPropertyName("changed")] - public bool Changed { get; set; } + /// Whether the auto-generated summary was persisted. False if the session already has a user-set name, the summary normalized to empty, or the session does not have a workspace. + [JsonPropertyName("applied")] + public bool Applied { get; set; } } -/// Credential-free authentication identity used to configure GitHub MCP. +/// 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 McpConfigureGitHubRequest +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; } -/// 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. +/// Existence, contents, and resolved path of the session plan file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStartServerRequest +public sealed class PlanReadResult { - /// 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; } + /// The content of the plan file, or null if it does not exist. + [JsonPropertyName("content")] + public string? Content { get; set; } - /// Name of the MCP server to start. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Whether the plan file exists in the workspace. + [JsonPropertyName("exists")] + public bool Exists { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Absolute file path of the plan file, or null if workspace is not enabled. + [JsonPropertyName("path")] + public string? Path { 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. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpRestartServerRequest +internal sealed class SessionPlanReadRequest { - /// 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; } - - /// 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; } -/// Server name for an individual MCP server stop. +/// Replacement contents to write to the session plan file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStopServerRequest +internal sealed class PlanUpdateRequest { - /// Name of the MCP server to stop. - [JsonPropertyName("serverName")] - public string ServerName { 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; } -/// Registration parameters for an external MCP client. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpRegisterExternalClientRequest +internal sealed class SessionPlanDeleteRequest { - /// Logical server name for the external client. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Server name identifying the external client to remove. +/// A single todo row read from the session SQL `todos` table. All fields are optional because the SQL schema is best-effort and the agent may not have populated every column. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpUnregisterExternalClientRequest +public sealed class PlanSqlTodosRow { - /// Server name of the external client to unregister. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Todo description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Todo identifier. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Todo status. + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// Todo title. + [JsonPropertyName("title")] + public string? Title { get; set; } } -/// Whether the named MCP server is running. +/// Todo rows read from the session SQL database. Empty when no session database is available. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpIsServerRunningResult +public sealed class PlanReadSqlTodosResult { - /// True if the server has an active client and transport. - [JsonPropertyName("running")] - public bool Running { 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. + [JsonPropertyName("rows")] + public IList Rows { get => field ??= []; set; } } -/// Server name to check running status for. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpIsServerRunningRequest +internal sealed class SessionPlanReadSqlTodosRequest { - /// Name of the MCP server to check. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the pending MCP OAuth response was accepted. +/// 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 McpOauthHandlePendingResult +public sealed class PlanSqlTodoDependency { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// ID of the todo it depends on. + [JsonPropertyName("dependsOn")] + public string DependsOn { get; set; } = string.Empty; -/// 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(McpOauthPendingRequestResponseToken), "token")] -[JsonDerivedType(typeof(McpOauthPendingRequestResponseCancelled), "cancelled")] -public partial class McpOauthPendingRequestResponse -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// ID of the todo that has the dependency. + [JsonPropertyName("todoId")] + public string TodoId { get; set; } = string.Empty; } - -/// The token variant of . +/// Todo rows + dependency edges read from the session SQL database. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthPendingRequestResponseToken : McpOauthPendingRequestResponse +public sealed class PlanReadSqlTodosWithDependenciesResult { - /// - [JsonIgnore] - public override string Kind => "token"; - - /// Access token acquired by the SDK host. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } - - /// Token lifetime in seconds, if known. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiresIn")] - public long? ExpiresIn { get; set; } - - /// OAuth token type. Defaults to bearer when omitted. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("tokenType")] - public string? TokenType { get; set; } -} + /// 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; } -/// The cancelled variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthPendingRequestResponseCancelled : McpOauthPendingRequestResponse -{ - /// - [JsonIgnore] - public override string Kind => "cancelled"; + /// 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; } } -/// Pending MCP OAuth request ID and host-provided token or cancellation response. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthHandlePendingRequest +internal sealed class SessionPlanReadSqlTodosWithDependenciesRequest { - /// OAuth request identifier from the mcp.oauth_required event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; - - /// Host response to the pending OAuth request. - [JsonPropertyName("result")] - public McpOauthPendingRequestResponse Result { get => field ??= new(); set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the MCP server whose persisted OAuth credentials were updated. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthAuthenticationStateChangedRequest +/// RPC data type for WorkspacesGetWorkspaceResultWorkspace operations. +public sealed class WorkspacesGetWorkspaceResultWorkspace { - /// 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; } + /// Current Git branch. + [JsonPropertyName("branch")] + public string? Branch { 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; } + /// Whether the per-session Chronicle upgrade prompt was dismissed for the workspace. + [JsonPropertyName("chronicle_sync_dismissed")] + public bool? ChronicleSyncDismissed { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Name of the client that created the workspace. + [JsonPropertyName("client_name")] + public string? ClientName { get; set; } -/// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthLoginResult -{ - /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. When present, the runtime starts the callback listener before returning and continues the flow in the background; completion is signaled via session.mcp_server_status_changed. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("authorizationUrl")] - public string? AuthorizationUrl { get; set; } -} + /// Timestamp when the workspace was created. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } -/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthLoginRequest -{ - /// 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; } + /// Current working directory associated with the workspace. + [JsonPropertyName("cwd")] + public string? Cwd { 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; } + /// Git repository root associated with the workspace. + [JsonPropertyName("git_root")] + public string? GitRoot { 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; } + /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. + [JsonPropertyName("host_type")] + public WorkspacesWorkspaceDetailsHostType? HostType { 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; } + /// 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; - /// 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; } + /// Most recent Mission Control event identifier observed for the workspace. + [JsonPropertyName("mc_last_event_id")] + public string? McLastEventId { 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; } + /// Mission Control session identifier associated with the workspace. + [JsonPropertyName("mc_session_id")] + public string? McSessionId { 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; } + /// Mission Control task identifier associated with the workspace. + [JsonPropertyName("mc_task_id")] + public string? McTaskId { 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; + /// Workspace display name. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether the workspace session can be steered remotely. + [JsonPropertyName("remote_steerable")] + public bool? RemoteSteerable { get; set; } -/// Passive MCP OAuth probe result. `authenticated` means the server accepted the probe request while an OAuth-origin access token was attached; it does not prove the server required or independently validated that token. The probe does not make a second unauthenticated request. Failed is an expected probe-domain outcome; JSON-RPC errors are reserved for API-call failures. -/// Polymorphic base type discriminated by status. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "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 -{ - /// The type discriminator. - [JsonPropertyName("status")] - public virtual string Status { get; set; } = string.Empty; -} + /// Repository identifier associated with the workspace. + [JsonPropertyName("repository")] + public string? Repository { get; set; } + /// Number of persisted summaries in the workspace. + [JsonPropertyName("summary_count")] + public long? SummaryCount { get; set; } -/// The no-auth-required variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultNoAuthRequired : McpOauthProbeResult -{ - /// - [JsonIgnore] - public override string Status => "no-auth-required"; + /// Timestamp when the workspace was last updated. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } - /// HTTP response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } + /// Whether the workspace name was explicitly chosen by the user. + [JsonPropertyName("user_named")] + public bool? UserNamed { get; set; } } -/// The authenticated variant of . +/// Current workspace metadata for the session, including its absolute filesystem path when available. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultAuthenticated : McpOauthProbeResult +public sealed class WorkspacesGetWorkspaceResult { - /// - [JsonIgnore] - public override string Status => "authenticated"; + /// 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; } - /// HTTP response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } + /// Current workspace metadata, or null if not available. + [JsonPropertyName("workspace")] + public WorkspacesGetWorkspaceResultWorkspace? Workspace { get; set; } } -/// The needs-auth variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultNeedsAuth : McpOauthProbeResult +internal sealed class SessionWorkspacesGetWorkspaceRequest { - /// - [JsonIgnore] - public override string Status => "needs-auth"; - - /// HTTP 401 or 403 response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } - - /// Why authentication is needed. - [JsonPropertyName("reason")] - public required McpOauthProbeNeedsAuthReason Reason { get; set; } - - /// Parsed WWW-Authenticate challenge parameters, when present and parseable. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("wwwAuthenticateParams")] - public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The failed variant of . +/// Workspace metadata fields to update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultFailed : McpOauthProbeResult +internal sealed class WorkspacesUpdateMetadataRequest { - /// - [JsonIgnore] - public override string Status => "failed"; + /// Opaque workspace context supplied by the session host. + [JsonPropertyName("context")] + public JsonElement? Context { get; set; } - /// Human-readable probe failure detail. - [JsonPropertyName("error")] - public required string Error { get; set; } + /// Optional workspace display name override. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// HTTP response returned by the server, when the probe reached the server and captured the complete response. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("httpResponse")] - public McpOauthHttpResponse? HttpResponse { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Remote MCP server name for a passive OAuth status probe. +/// Optional session context used when creating a local workspace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthProbeRequest +internal sealed class WorkspacesEnsureRequest { - /// 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; + /// Opaque workspace context supplied by the session host. + [JsonPropertyName("context")] + public JsonElement? Context { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the pending MCP OAuth response was accepted. +/// Relative paths of files stored in the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthRespondResult +public sealed class WorkspacesListFilesResult { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Slash-separated relative file paths in the workspace files directory. + [JsonPropertyName("files")] + public IList Files { get => field ??= []; set; } } -/// Pending MCP OAuth request id to respond to. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthRespondRequest +internal sealed class SessionWorkspacesListFilesRequest { - /// 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; } -/// Indicates whether the pending MCP headers refresh response was accepted. +/// Contents of the requested workspace file as a UTF-8 string. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult +public sealed class WorkspacesReadFileResult { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// File content as a UTF-8 string. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; } -/// Host response: supply dynamic headers or decline this refresh. -/// Polymorphic base type discriminated by kind. +/// Relative path of the workspace file to read. [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 WorkspacesReadFileRequest { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} -/// The headers variant of . +/// Relative path and UTF-8 content for the workspace file to create or overwrite. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHeadersHandlePendingHeadersRefreshRequest +internal sealed class WorkspacesCreateFileRequest { - /// - [JsonIgnore] - public override string Kind => "headers"; + /// File content to write as a UTF-8 string. + [JsonPropertyName("content")] + public string Content { 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; } + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The none variant of . +/// Filesystem metadata for a path in the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeadersHandlePendingHeadersRefreshRequest +public sealed class WorkspacesStatFileResult { - /// - [JsonIgnore] - public override string Kind => "none"; + /// Creation time in Unix epoch milliseconds. + [JsonPropertyName("birthtimeMs")] + public double BirthtimeMs { get; set; } + + /// Whether the path identifies a directory. + [JsonPropertyName("isDirectory")] + public bool IsDirectory { get; 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; } } -/// The error variant of . +/// Relative path of the workspace file or directory to inspect. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestError : McpHeadersHandlePendingHeadersRefreshRequest +internal sealed class WorkspacesStatFileRequest { - /// - [JsonIgnore] - public override string Kind => "error"; + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Host credential broker failure, denial, or revocation reason. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// MCP headers refresh request id and the host response. +/// Directory to create within the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest +internal sealed class WorkspacesCreateDirectoryRequest { - /// Headers refresh request identifier from mcp.headers_refresh_required. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Host response: supply dynamic headers or decline this refresh. - [JsonPropertyName("result")] - public McpHeadersHandlePendingHeadersRefreshRequest Result { get => field ??= new(); set; } + /// Whether to create missing parent directories. Defaults to false. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } /// 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. +/// File or directory to remove from the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsResourceContent +internal sealed class WorkspacesRemovePathRequest { - /// Resource-level metadata (CSP, permissions, etc.). - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } - - /// Base64-encoded binary content. - [JsonPropertyName("blob")] - public string? Blob { get; set; } - - /// MIME type of the content. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } + /// Whether a missing path should be treated as success. Defaults to false. + [JsonPropertyName("force")] + public bool? Force { get; set; } - /// Text content (e.g. HTML). - [JsonPropertyName("text")] - public string? Text { get; set; } + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// The resource URI (typically ui://...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; -} + /// Whether to remove directory contents recursively. Defaults to false. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } -/// Resource contents returned by the MCP server. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsReadResourceResult -{ - /// Resource contents returned by the server. - [JsonPropertyName("contents")] - public IList Contents { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// MCP server and resource URI to fetch. +/// Source and destination paths for a rename within the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsReadResourceRequest +internal sealed class WorkspacesRenamePathRequest { - /// 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; + /// Slash-separated destination path relative to the workspace files directory. + [JsonPropertyName("destination")] + public string Destination { 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; + /// Slash-separated source path relative to the workspace files directory. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; } -/// App-callable tools from the named MCP server. +/// Workspace checkpoint metadata with assigned number, human-readable title, and checkpoint filename. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsListToolsResult +public sealed class WorkspacesCheckpoints { - /// App-callable tools from the server. - [JsonPropertyName("tools")] - public IList> Tools { get => field ??= []; set; } + /// Filename of the checkpoint within the workspace checkpoints directory. + [JsonPropertyName("filename")] + public string Filename { get; set; } = string.Empty; + + /// Checkpoint number assigned by the workspace manager. + [JsonPropertyName("number")] + public long Number { get; set; } + + /// Human-readable checkpoint title. + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; } -/// MCP server to list app-callable tools for. +/// Workspace checkpoints in chronological order; empty when the workspace is not enabled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsListToolsRequest +public sealed class WorkspacesListCheckpointsResult { - /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("originServerName")] - public string OriginServerName { get; set; } = string.Empty; - - /// MCP server hosting the app. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Workspace checkpoints in chronological order. Empty when workspace is not enabled. + [JsonPropertyName("checkpoints")] + public IList Checkpoints { get => field ??= []; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionWorkspacesListCheckpointsRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// MCP server, tool name, and arguments to invoke from an MCP App view. +/// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsCallToolRequest +public sealed class WorkspacesReadCheckpointResult { - /// Tool arguments. - [JsonPropertyName("arguments")] - public IDictionary? Arguments { 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; + /// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. + [JsonPropertyName("content")] + public string? Content { 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; +/// Checkpoint number to read. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesReadCheckpointRequest +{ + /// Checkpoint number to read. + [JsonPropertyName("number")] + public long Number { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// MCP tool name. - [JsonPropertyName("toolName")] - public string ToolName { get; set; } = string.Empty; } -/// Host context advertised to MCP App guests. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsSetHostContextDetails +/// Metadata for the persisted summary. +public sealed class WorkspacesAddSummaryResultSummary { - /// Display modes the host supports. - [JsonPropertyName("availableDisplayModes")] - public IList? AvailableDisplayModes { get; set; } +} - /// Current display mode (SEP-1865). - [JsonPropertyName("displayMode")] - public McpAppsSetHostContextDetailsDisplayMode? DisplayMode { get; set; } +/// Refreshed metadata for the containing workspace. +public sealed class WorkspacesAddSummaryResultWorkspace +{ +} - /// BCP-47 locale, e.g. 'en-US'. - [JsonPropertyName("locale")] - public string? Locale { 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; } - /// Platform type for responsive design. - [JsonPropertyName("platform")] - public McpAppsSetHostContextDetailsPlatform? Platform { get; set; } + /// Refreshed metadata for the containing workspace. + [JsonPropertyName("workspace")] + public WorkspacesAddSummaryResultWorkspace? Workspace { get; set; } +} - /// UI theme preference per SEP-1865. - [JsonPropertyName("theme")] - public McpAppsSetHostContextDetailsTheme? Theme { 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; - /// IANA timezone, e.g. 'America/New_York'. - [JsonPropertyName("timeZone")] - public string? TimeZone { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Host application identifier. - [JsonPropertyName("userAgent")] - public string? UserAgent { get; set; } + /// Summary title shown in checkpoint listings. + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; } -/// Host context to advertise to MCP App guests. +/// Rollback point for local workspace summaries. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsSetHostContextRequest +internal sealed class WorkspacesTruncateSummariesRequest { - /// Host context advertised to MCP App guests. - [JsonPropertyName("context")] - public McpAppsSetHostContextDetails Context { get => field ??= new(); set; } + /// 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; } -/// Current host context. +/// Autopilot objective file content, or null when missing. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsHostContextDetails +public sealed class WorkspacesReadAutopilotObjectiveResult { - /// Display modes the host supports. - [JsonPropertyName("availableDisplayModes")] - public IList? AvailableDisplayModes { get; set; } + /// Autopilot objective file content, or null when missing. + [JsonPropertyName("content")] + public string? Content { get; set; } +} - /// Current display mode (SEP-1865). - [JsonPropertyName("displayMode")] - public McpAppsHostContextDetailsDisplayMode? DisplayMode { 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; +} - /// 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; } +/// 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; +} - /// IANA timezone, e.g. 'America/New_York'. - [JsonPropertyName("timeZone")] - public string? TimeZone { get; set; } +/// Autopilot objective file content to persist. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesWriteAutopilotObjectiveRequest +{ + /// Autopilot objective file content. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Host application identifier. - [JsonPropertyName("userAgent")] - public string? UserAgent { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Current host context advertised to MCP App guests. +/// Result of deleting the autopilot objective file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsHostContext +public sealed class WorkspacesDeleteAutopilotObjectiveResult { - /// Current host context. - [JsonPropertyName("context")] - public McpAppsHostContextDetails Context { get => field ??= new(); 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 SessionMcpAppsGetHostContextRequest +internal sealed class SessionWorkspacesDeleteAutopilotObjectiveRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Capability negotiation snapshot. +/// Whether the autopilot objective file exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseCapability +public sealed class WorkspacesAutopilotObjectiveExistsResult { - /// 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; } - - /// Whether the session has the `mcp-apps` capability. - [JsonPropertyName("sessionHasMcpApps")] - public bool SessionHasMcpApps { get; set; } + /// True when the objective file exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } } -/// What the server returned for this session. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseServer +internal sealed class SessionWorkspacesAutopilotObjectiveExistsRequest { - /// Whether the named server is currently connected. - [JsonPropertyName("connected")] - public bool Connected { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Up to 5 tool names with `_meta.ui` for quick inspection. - [JsonPropertyName("sampleToolNames")] - public IList SampleToolNames { get => field ??= []; set; } +/// RPC data type for WorkspacesSaveLargePasteResultSaved operations. +public sealed class WorkspacesSaveLargePasteResultSaved +{ + /// Filename within the workspace files directory. + [JsonPropertyName("filename")] + public string Filename { get; set; } = string.Empty; - /// Total tools returned by the server's tools/list. - [JsonPropertyName("toolCount")] - public double ToolCount { get; set; } + /// Absolute filesystem path to the saved paste file. + [JsonPropertyName("filePath")] + public string FilePath { get; set; } = string.Empty; - /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). - [JsonPropertyName("toolsWithUiMeta")] - public double ToolsWithUiMeta { get; set; } + /// Size of the saved file in bytes. + [JsonPropertyName("sizeBytes")] + public long SizeBytes { get; set; } } -/// Diagnostic snapshot of MCP Apps wiring for the named server. +/// Descriptor for the saved paste file, or null when the workspace is unavailable. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseResult +public sealed class WorkspacesSaveLargePasteResult { - /// 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; } + /// 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; } } -/// MCP server to diagnose MCP Apps wiring for. +/// Pasted content to save as a UTF-8 file in the session workspace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsDiagnoseRequest +internal sealed class WorkspacesSaveLargePasteRequest { - /// 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; + /// 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; } -/// MCP resource content with URI, optional MIME type, text or base64 blob, and resource metadata. +/// A single changed file and its unified diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceContent +public sealed class WorkspaceDiffFileChange { - /// Resource-level metadata (CSP, permissions, etc.). - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } + /// Type of change represented by this file diff. + [JsonPropertyName("changeType")] + public WorkspaceDiffFileChangeType ChangeType { get; set; } - /// Base64-encoded binary content. - [JsonPropertyName("blob")] - public string? Blob { get; set; } + /// Unified diff content for the file. Empty when the diff was truncated. + [JsonPropertyName("diff")] + public string Diff { get; set; } = string.Empty; - /// MIME type of the content. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } + /// Whether the diff content was omitted because it exceeded the per-file size limit. + [JsonPropertyName("isTruncated")] + public bool? IsTruncated { get; set; } - /// Text content (e.g. HTML). - [JsonPropertyName("text")] - public string? Text { get; set; } + /// Original file path for renamed files. + [JsonPropertyName("oldPath")] + public string? OldPath { get; set; } - /// The resource URI. - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; + /// 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; } -/// Resource contents returned by the MCP server. +/// Workspace diff result for the requested mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesReadResult +public sealed class WorkspaceDiffResult { - /// Resource contents returned by the server. - [JsonPropertyName("contents")] - public IList Contents { get => field ??= []; set; } -} + /// Default branch used for a branch diff, when branch mode was requested. + [JsonPropertyName("baseBranch")] + public string? BaseBranch { get; set; } -/// MCP server and resource URI to fetch. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesReadRequest -{ - /// 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; + /// Changed files and their unified diffs. + [JsonPropertyName("changes")] + public IList Changes { get => field ??= []; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } - /// Resource URI. - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; + /// 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; } } -/// Standard MCP resource annotations plus preserved non-standard annotation fields. +/// Parameters for computing a workspace diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceAnnotations +internal sealed class WorkspacesDiffRequest { - /// Server-provided non-standard annotation fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } - - /// Intended audience roles for this resource. - [JsonPropertyName("audience")] - public IList? Audience { get; set; } + /// When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. + [JsonPropertyName("ignoreWhitespace")] + public bool? IgnoreWhitespace { get; set; } - /// Last-modified timestamp hint. - [JsonPropertyName("lastModified")] - public string? LastModified { get; set; } + /// Diff mode requested by the client. + [JsonPropertyName("mode")] + public WorkspaceDiffMode Mode { get; set; } - /// Priority hint for model/client use. - [JsonPropertyName("priority")] - public double? Priority { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// A resource icon descriptor plus preserved non-standard icon fields. +/// Current per-window credit limit and consumption for an autopilot objective. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceIcon +public sealed class AutopilotObjectiveCreditLimit { - /// Server-provided non-standard icon fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } - - /// Icon MIME type, when known. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } - - /// Icon sizes hint. - [JsonPropertyName("sizes")] - public string? Sizes { get; set; } + /// Configured AI-credit cap, when one is set. + [JsonPropertyName("credits")] + public double? Credits { get; set; } - /// Icon URI. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; + /// Window consumption in fractional AI credits, for display. + [JsonPropertyName("creditsUsed")] + public double CreditsUsed { get; set; } - /// Theme hint for this icon. - [JsonPropertyName("theme")] - public string? Theme { 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; } -/// 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`. +/// Public, persistence-independent projection of an autopilot objective. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResource +public sealed class AutopilotObjectiveState { - /// Resource-level metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } - - /// Server-provided non-standard descriptor fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } - - /// Model/client annotations associated with this resource. - [JsonPropertyName("annotations")] - public McpResourceAnnotations? Annotations { get; set; } + /// Optional summary recorded when the objective completed. + [JsonPropertyName("completionSummary")] + public string? CompletionSummary { get; set; } - /// Optional description of what this resource represents. - [JsonPropertyName("description")] - public string? Description { 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; - /// Icons associated with this resource. - [JsonPropertyName("icons")] - public IList? Icons { get; set; } + /// Current per-window consumption and optional cap, when a credit-tracking window is present. + [JsonPropertyName("creditLimit")] + public AutopilotObjectiveCreditLimit? CreditLimit { get; set; } - /// MIME type of the resource, if known. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } + /// Session-local objective identifier. + [JsonPropertyName("id")] + public long Id { get; set; } - /// The programmatic name of the resource. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// User-provided objective text. + [JsonPropertyName("objective")] + public string Objective { get; set; } = string.Empty; - /// Resource size in bytes, when known. - [JsonPropertyName("size")] - public long? Size { get; set; } + /// Optional reason the objective is paused. + [JsonPropertyName("pauseReason")] + public string? PauseReason { get; set; } - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Current normalized lifecycle status. + [JsonPropertyName("status")] + public AutopilotObjectiveStatus Status { get; set; } - /// The resource URI (e.g. ui://... or file:///...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; + /// Number of objective turns started. + [JsonPropertyName("turnCount")] + public long TurnCount { get; set; } } -/// One page of resources advertised by the named MCP server. +/// Canonical runtime state for the session's current autopilot objective. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesListResult +public sealed class AutopilotObjectiveGetStateResult { - /// Opaque cursor for the next page, if the server has more resources. - [JsonPropertyName("nextCursor")] - public string? NextCursor { get; set; } - - /// Resources advertised by the server (proxied MCP `resources/list`). - [JsonPropertyName("resources")] - public IList Resources { get => field ??= []; set; } + /// Current objective state, or `null` when the session has no objective. + [JsonPropertyName("state")] + public AutopilotObjectiveState? State { get; set; } } -/// MCP server whose resources to enumerate. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesListRequest +internal sealed class SessionAutopilotObjectiveGetStateRequest { - /// Opaque MCP pagination cursor from a prior `nextCursor` value. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// 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; +/// 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 CompletionsGetTriggerCharactersResult +{ + /// Trigger characters advertised by the host (e.g. `["@", "#"]`). Empty disables host-driven completions for the session. + [JsonPropertyName("triggerCharacters")] + public IList TriggerCharacters { get => field ??= []; set; } +} +/// 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; } -/// 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`. +/// 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 McpResourceTemplate +public sealed class SessionCompletionItem { - /// Resource-template-level metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } + /// Text spliced into the composer when the item is accepted. + [JsonPropertyName("insertText")] + public string InsertText { get; set; } = string.Empty; - /// Server-provided non-standard descriptor fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { 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; } - /// Model/client annotations associated with this template. - [JsonPropertyName("annotations")] - public McpResourceAnnotations? Annotations { get; set; } + /// Primary display label for the picker row. Falls back to `insertText` when absent. + [JsonPropertyName("label")] + public string? Label { get; set; } - /// Optional description of what this template is for. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// End (exclusive) of the replacement range in `text`, in UTF-16 code units. + [JsonPropertyName("rangeEnd")] + public long? RangeEnd { get; set; } - /// Icons associated with resources matching this template. - [JsonPropertyName("icons")] - public IList? Icons { get; set; } + /// Start of the replacement range in `text`, in UTF-16 code units. + [JsonPropertyName("rangeStart")] + public long? RangeStart { get; set; } +} - /// MIME type for resources matching this template, if uniform. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } +/// Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CompletionsRequestResult +{ + /// Completion items in host-ranked order. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } +} - /// The programmatic name of the resource template. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// 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; } - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// An RFC 6570 URI template for constructing resource URIs. - [JsonPropertyName("uriTemplate")] - public string UriTemplate { get; set; } = string.Empty; + /// The full composed composer input. + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; } -/// One page of resource templates advertised by the named MCP server. +/// Instruction sources loaded for the session, in merge order. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesListTemplatesResult +public sealed class InstructionsGetSourcesResult { - /// 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; } + /// Instruction sources for the session. + [JsonPropertyName("sources")] + public IList Sources { get => field ??= []; set; } } -/// MCP server whose resource templates to enumerate. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesListTemplatesRequest +internal sealed class SessionInstructionsGetSourcesRequest { - /// Opaque MCP pagination cursor from a prior `nextCursor` value. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } - - /// Name of the MCP server whose resource templates to enumerate. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// MCP diagnostic source configuration. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpDiagnosticSourceConfiguration +internal sealed class SessionInstructionsReloadRequest { - /// Threshold to apply to MCP diagnostic producers in this session. - [JsonPropertyName("level")] - public DiagnosticLogLevel Level { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. +/// Result of one customization reload component. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiagnosticSourcesConfiguration +public sealed class CustomizationReloadOutcome { - /// MCP diagnostic capture threshold. Omit to leave the current threshold unchanged. - [JsonPropertyName("mcp")] - public McpDiagnosticSourceConfiguration? Mcp { get; set; } + /// Reason for a skipped component or description of a failure, when available. + [JsonPropertyName("detail")] + public string? Detail { 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; } } -/// Per-source session diagnostics configuration. +/// 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 DiagnosticsConfiguration +public sealed class CustomizationsReloadResult { - /// Diagnostic thresholds keyed by supported source. - [JsonPropertyName("sources")] - public DiagnosticSourcesConfiguration Sources { get => field ??= new(); 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; } } -/// Patch session diagnostic thresholds for explicitly supplied sources. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DiagnosticsConfigureRequest +internal sealed class SessionCustomizationsReloadRequest { /// 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 fleet mode was successfully activated. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "source", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(DiagnosticEntryMcp), "mcp")] -public partial class DiagnosticEntry +public sealed class FleetStartResult { - /// The type discriminator. - [JsonPropertyName("source")] - public virtual string Source { get; set; } = string.Empty; + /// Whether fleet mode was successfully activated. + [JsonPropertyName("started")] + public bool Started { get; set; } } - -/// MCP-specific detail for a source-discriminated diagnostic entry. +/// 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 McpDiagnosticDetails +internal sealed class FleetStartRequest { - /// Fresh identifier for the MCP connection attempt, including failed starts. - [JsonPropertyName("connectionId")] - public string ConnectionId { get; set; } = string.Empty; + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } - /// Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. - [JsonPropertyName("data")] - public string? Data { 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; } - /// Protocol-frame direction when kind is protocol. - [JsonPropertyName("direction")] - public McpDiagnosticDirection? Direction { get; set; } + /// Optional user prompt to combine with fleet instructions. + [JsonPropertyName("prompt")] + public string? Prompt { get; set; } - /// Diagnostic record category. - [JsonPropertyName("kind")] - public McpDiagnosticKind Kind { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Configured MCP server name. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } } -/// The mcp variant of . +/// Agents available to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class DiagnosticEntryMcp : DiagnosticEntry +public sealed class AgentList { - /// - [JsonIgnore] - public override string Source => "mcp"; - - /// Agent identifier for a subagent host. Omitted for the root agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("agentId")] - public string? AgentId { get; set; } - - /// Typed MCP diagnostic detail. - [JsonPropertyName("details")] - public required McpDiagnosticDetails Details { get; set; } - - /// Severity of this emitted diagnostic record. - [JsonPropertyName("level")] - public required DiagnosticSeverity Level { get; set; } - - /// Human-readable diagnostic summary. - [JsonPropertyName("message")] - public required string Message { get; set; } - - /// Original byte count when a known-size message or data value was truncated. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("originalBytes")] - public long? OriginalBytes { get; set; } - - /// UTC RFC 3339 timestamp captured at the diagnostic source. - [JsonPropertyName("timestamp")] - public required string Timestamp { get; set; } - - /// Whether message or data was truncated to the record-size bound. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("truncated")] - public bool? Truncated { get; set; } + /// Available agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } } -/// One cursor-addressed page of retained session diagnostics. +/// RPC data type for SessionAgentList operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiagnosticsReadResult +public sealed class SessionAgentListRequest { - /// 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; } + /// 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; } - /// Whether additional retained records follow this page. - [JsonPropertyName("hasMore")] - public bool HasMore { 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; } } -/// Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). +/// RPC data type for SessionAgentListRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DiagnosticsReadRequest +internal sealed class SessionAgentListRequestWithSession { - /// Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. - [JsonPropertyName("cursor")] - public string? Cursor { 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; } - /// Maximum number of records to return, from 1 through 500. Omit for 100. - [JsonPropertyName("max")] - public long? Max { get; set; } + /// When true, request authored base prompt text on each AgentInfo. Prompt text may be omitted when unavailable, such as for agents projected through a relay session. + [JsonPropertyName("includePrompt")] + public bool? IncludePrompt { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// 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; } } -/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +/// An in-memory authored prompt override for an available agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCapabilities +internal sealed class AgentSetPromptRequest { - /// Connector API contract version. - [JsonPropertyName("apiVersion")] - public long ApiVersion { get; set; } - - /// Current session availability. Disabled availability is reported without making a Connector request. - [JsonPropertyName("availability")] - public ConnectorAvailability Availability { get; set; } - - /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. - [JsonPropertyName("consentContinuation")] - public bool ConsentContinuation { get; set; } - - /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. - [JsonPropertyName("maxDeadlineMs")] - public long MaxDeadlineMs { get; set; } + /// Stable effective agent id. Plugin namespace separators are normalized. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Maximum accepted polling attempts for one continuation call. - [JsonPropertyName("maxPollAttempts")] - public long MaxPollAttempts { get; set; } + /// Replacement authored prompt. Empty text is valid. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Maximum accepted delay in milliseconds between polling attempts. - [JsonPropertyName("maxPollIntervalMs")] - public long MaxPollIntervalMs { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// 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; } +/// The currently selected custom agent, or null when using the default agent. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AgentGetCurrentResult +{ + /// Currently selected custom agent, or null if using the default agent. + [JsonPropertyName("agent")] + public AgentInfo? Agent { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionConnectorsGetCapabilitiesRequest +internal sealed class SessionAgentGetCurrentRequest { /// 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. +/// The newly selected custom agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorAuthorizationRequirement +public sealed class AgentSelectResult { - /// 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; } + /// The newly selected custom agent. + [JsonPropertyName("agent")] + public AgentInfo Agent { get => field ??= new(); set; } } -/// Credential-free Connector catalog entry. +/// Name of the custom agent to select for subsequent turns. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCatalogEntry +internal sealed class AgentSelectRequest { - /// 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. + /// Name of the custom agent to select. [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; } - - /// Current authoritative service connection state. - [JsonPropertyName("status")] - public ConnectorCatalogStatus Status { 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; } - - /// Unix epoch milliseconds when this snapshot was accepted. - [JsonPropertyName("refreshedAtMs")] - public long RefreshedAtMs { get; set; } - - /// Monotonically increasing session-local catalog revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Live status of one session-owned MCP projection. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorRuntimeStatus +internal sealed class SessionAgentDeselectRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// Custom agents available to the session after reloading definitions from disk. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorStatus +public sealed class AgentReloadResult { - /// 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; } + /// Reloaded custom agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionConnectorsGetStatusRequest +internal sealed class SessionAgentReloadRequest { /// 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. +/// Identifier assigned to the newly started background agent task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorAccountRequest +public sealed class TasksStartAgentResult { - /// Opaque account selection ID previously returned by an account discovery API. - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; + /// Generated agent ID for the background task. + [JsonPropertyName("agentId")] + public string AgentId { get; set; } = string.Empty; +} + +/// Agent type, prompt, name, and optional description and model override for the new task. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksStartAgentRequest +{ + /// Type of agent to start (e.g., 'explore', 'task', 'general-purpose'). + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; + + /// Short description of the task. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Optional model override. + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// Friendly, non-unique name used when displaying the agent. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Task prompt for the agent. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Typed result of initiating or continuing a Connector connection. -/// Polymorphic base type discriminated by kind. +/// 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)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", + TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ConnectorConnectResultConnected), "connected")] -[JsonDerivedType(typeof(ConnectorConnectResultConsentRequired), "consent_required")] -[JsonDerivedType(typeof(ConnectorConnectResultPending), "pending")] -public partial class ConnectorConnectResult +[JsonDerivedType(typeof(TaskInfoAgent), "agent")] +[JsonDerivedType(typeof(TaskInfoClient), "client")] +[JsonDerivedType(typeof(TaskInfoShell), "shell")] +public partial class TaskInfo { /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// The service is connected and the session MCP graph was reconciled. -/// The connected variant of . +/// 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 partial class ConnectorConnectResultConnected : ConnectorConnectResult +public partial class TaskInfoAgent : TaskInfo { /// [JsonIgnore] - public override string Kind => "connected"; + public override string Type => "agent"; - /// Fresh authoritative Connector state after MCP reconciliation. - [JsonPropertyName("status")] - public required ConnectorStatus Status { get; set; } -} + /// ISO 8601 timestamp when the current active period began. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { 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"; + /// Accumulated active execution time in milliseconds. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeTimeMs")] + public TimeSpan? ActiveTime { get; set; } - /// Validated HTTPS consent URL. The runtime does not open it. - [JsonPropertyName("consentUrl")] - public required string ConsentUrl { get; set; } + /// Type of agent running this task. + [JsonPropertyName("agentType")] + public required string AgentType { get; set; } - /// Opaque ID accepted by continueConnection. - [JsonPropertyName("continuationId")] - public required string ContinuationId { 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; } -/// 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"; + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } - /// Opaque ID accepted by continueConnection. - [JsonPropertyName("continuationId")] - public required string ContinuationId { get; set; } -} + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { 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; + /// Friendly, non-unique name intended for display. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Canonical Connector name from the current catalog. - [JsonPropertyName("connectorName")] - public string ConnectorName { get; set; } = string.Empty; + /// Error message when the task failed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether task execution is synchronously awaited or managed in the background. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskExecutionMode? ExecutionMode { get; set; } -/// 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; + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { 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; } + /// ISO 8601 timestamp when the agent entered idle state. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } - /// Maximum catalog requests made by this call. Must be between one and the capability limit. - [JsonPropertyName("maxAttempts")] - public int MaxAttempts { get; set; } + /// Most recent response text from the agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("latestResponse")] + public string? LatestResponse { get; set; } - /// Delay in milliseconds between attempts. Must not exceed the capability limit. - [JsonPropertyName("pollIntervalMs")] - public int PollIntervalMs { get; set; } + /// Requested model override for the task when specified. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Most recent prompt delivered to the agent. Updated whenever the agent receives a follow-up message. + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } -/// 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; } + /// Runtime model resolved for the task when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resolvedModel")] + public string? ResolvedModel { get; set; } - /// Fresh authoritative session state after removing Connector-owned MCP servers. - [JsonPropertyName("status")] - public ConnectorStatus Status { get => field ??= new(); set; } -} + /// Result text from the task when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public string? Result { get; set; } -/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorReconcileRequest -{ - /// 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; + /// ISO 8601 timestamp when the task was started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } - /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. - [JsonPropertyName("refreshCatalog")] - public bool? RefreshCatalog { get; set; } + /// Current lifecycle status of the task. + [JsonPropertyName("status")] + public required TaskStatus Status { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Tool call ID associated with this agent task. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } } -/// Identifies the target session. +/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionConnectorsWithdrawProjectionRequest +public sealed class TaskClientOwner { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// 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; } + + /// 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; } +} -/// 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. +/// Tracked client-owned task metadata. +/// The client variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ManagedSettingsResolvedData +public partial class TaskInfoClient : TaskInfo { - /// 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; } + /// + [JsonIgnore] + public override string Type => "client"; - /// Whether a session-local permissions layer injected by the SDK host was present. - [JsonPropertyName("clientManaged")] - public bool? ClientManaged { get; set; } + /// ISO 8601 timestamp when the current active segment started. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } - /// Whether an actual device MDM/plist/registry/file managed-settings layer was present. - [JsonPropertyName("deviceManaged")] - public bool DeviceManaged { get; set; } + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public required long ActiveTimeMs { 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; } + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public required bool CanCancel { 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; } + /// Human-readable reason for terminal cancellation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { 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; } + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public required string ClientTaskId { 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; } + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { 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; } + /// Task description. + [JsonPropertyName("description")] + public required string Description { get; set; } - /// Whether the server (account/org) managed-settings layer was present. - [JsonPropertyName("serverManaged")] - public bool ServerManaged { get; set; } + /// Optional task display name. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { 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; } + /// Human-readable terminal failure message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { 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 owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorCode")] + public string? ErrorCode { 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; + /// 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; } } -/// Session plugin metadata, with name, marketplace, optional version, and enabled state. +/// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. +/// The shell variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Plugin +public partial class TaskInfoShell : TaskInfo { - /// Opaque stable identity for a direct plugin source. - [JsonPropertyName("directSourceId")] - public string? DirectSourceId { get; set; } + /// + [JsonIgnore] + public override string Type => "shell"; - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Whether the shell runs inside a managed PTY session or as an independent background process. + [JsonPropertyName("attachmentMode")] + public required TaskShellInfoAttachmentMode AttachmentMode { get; set; } - /// Whether this managed desired plugin has an installed or live record. - [JsonPropertyName("installed")] - public bool? Installed { get; set; } + /// Whether this shell task can be promoted to background mode. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("canPromoteToBackground")] + public bool? CanPromoteToBackground { get; set; } - /// Absolute marketplace directory for a live plugin. - [JsonPropertyName("installedFrom")] - public string? InstalledFrom { get; set; } + /// Command being executed. + [JsonPropertyName("command")] + public required string Command { get; set; } - /// Whether enterprise managed settings control this plugin. - [JsonPropertyName("managed")] - public bool? Managed { get; set; } + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } - /// Enabled state required by enterprise managed settings. - [JsonPropertyName("managedDesiredEnabled")] - public bool? ManagedDesiredEnabled { get; set; } + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { get; set; } - /// Marketplace the plugin came from. - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; + /// Whether task execution is synchronously awaited or managed in the background. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskExecutionMode? ExecutionMode { get; set; } - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } - /// Runtime plugin provenance, such as "builtin". - [JsonPropertyName("source")] - public string? Source { get; set; } + /// Path to the detached shell log, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("logPath")] + public string? LogPath { get; set; } - /// Installed version. - [JsonPropertyName("version")] - public string? Version { 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; } } -/// Plugins installed for the session, with their enabled state and version metadata. +/// Background tasks currently tracked by the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginList +public sealed class TaskList { - /// Installed plugins. - [JsonPropertyName("plugins")] - public IList Plugins { get => field ??= []; set; } + /// Currently tracked tasks. + [JsonPropertyName("tasks")] + public IList Tasks { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsListRequest +internal sealed class SessionTasksListRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Plugin source resolved relative to the session's authoritative working directory. +/// Tracked client-owned task metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsInstallRequest +public sealed class TaskClientInfo { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// ISO 8601 timestamp when the current active segment started. + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } - /// 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; -} + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public long ActiveTimeMs { get; set; } -/// 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; } + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public bool CanCancel { 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; + /// Human-readable reason for terminal cancellation. + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { 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; + /// 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; -/// 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; } + /// Optional task display name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public string? Error { get; set; } -/// 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; } + /// Optional owner-supplied terminal failure code. + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public TaskClientExecutionMode ExecutionMode { get; set; } -/// 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; } + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadCustomAgents")] - public bool? ReloadCustomAgents { get; set; } + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { 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; } + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { 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; } + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public TaskClientOwner Owner { get => field ??= new(); set; } - /// Reload MCP server connections after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadMcp")] - public bool? ReloadMcp { get; set; } -} + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { 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; } + /// Opaque successful terminal result supplied by the task owner. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } - /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadCustomAgents")] - public bool? ReloadCustomAgents { get; set; } + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public long Sequence { 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; } + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public DateTimeOffset StartedAt { 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; } + /// Client task lifecycle status. + [JsonPropertyName("status")] + public TaskClientStatus Status { get; set; } - /// Reload MCP server connections after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadMcp")] - public bool? ReloadMcp { get; set; } + /// Task kind. + [JsonPropertyName("type")] + public TaskClientType Type { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public DateTimeOffset UpdatedAt { get; set; } } -/// Identifies the target session. +/// Result of registering or reclaiming a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsMarketplacesListRequest +public sealed class TasksRegisterResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// True only when this invocation created a new task. + [JsonPropertyName("created")] + public bool Created { get; set; } + + /// True only when this invocation reclaimed an orphaned task. + [JsonPropertyName("reclaimed")] + public bool Reclaimed { get; set; } + + /// Authoritative registered or reclaimed task. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } } -/// Marketplace source and optional working directory for relative-path resolution. +/// Registers or reclaims a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesAddRequestWithSession +internal sealed class TasksRegisterRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the owner supports runtime cancellation requests. + [JsonPropertyName("cancellable")] + public bool Cancellable { get; set; } - /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; + /// Owner-scoped idempotency key used for registration and reclaim. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { 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; } -} + /// Human-readable description of the external work. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; -/// Name of the marketplace to remove and an optional force flag. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesRemoveRequestWithSession -{ - /// When true, also uninstall every plugin sourced from this marketplace. When false (default), removal is a no-op if any plugin from this marketplace is installed and the dependent plugin names are returned in the result. - [JsonPropertyName("force")] - public bool? Force { get; set; } + /// Optional short display name for the external work. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Marketplace name to remove. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// 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; } } -/// Name of the marketplace whose plugin catalog to fetch. +/// Result of publishing a client-owned task update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesBrowseRequestWithSession +public sealed class TasksUpdateResult { - /// Marketplace name to browse. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Whether this invocation changed task state. + [JsonPropertyName("applied")] + public bool Applied { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether this invocation repeated the latest accepted update. + [JsonPropertyName("duplicate")] + public bool Duplicate { get; set; } -/// RPC data type for SessionPluginsMarketplacesRefresh operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionPluginsMarketplacesRefreshRequest -{ - /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Authoritative task after processing the update. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } } -/// RPC data type for SessionPluginsMarketplacesRefreshRequestWithSession operations. +/// Progress or terminal update for a client-owned task. +/// Polymorphic base type discriminated by kind. [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; +[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; } -/// 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. + +/// Publishes nonterminal progress for a running or idle client task. +/// The progress variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderSessionToken +public partial class TaskClientUpdateProgress : TaskClientUpdate { - /// 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; } + /// + [JsonIgnore] + public override string Kind => "progress"; - /// HTTP header name the token must be sent under. - [JsonPropertyName("header")] - public string Header { get; set; } = string.Empty; + /// Optional progress message appended to recent activity when nonempty. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } - /// The model the token is bound to, when applicable. When set, the token is only valid for requests against this model. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Optional completion percentage; null clears the current percentage. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } - /// The short-lived token value. - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; + /// Optional progress phase; null clears the current phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } + + /// Optional active status transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public TaskClientActiveStatus? Status { get; set; } } -/// A snapshot of the provider endpoint the session is currently configured to talk to. +/// Reports successful terminal completion. +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderEndpoint +public partial class TaskClientUpdateCompleted : TaskClientUpdate { - /// A credential the caller should use with this endpoint. Omitted only when the endpoint accepts unauthenticated requests. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } + /// + [JsonIgnore] + public override string Kind => "completed"; - /// Base URL to pass to the LLM client library. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } - /// HTTP headers the caller must include on every outbound request. - [JsonPropertyName("headers")] - public IDictionary Headers { get => field ??= new Dictionary(); set; } + /// Optional opaque successful terminal result. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} - /// Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one. - [JsonPropertyName("sessionToken")] - public ProviderSessionToken? SessionToken { get; set; } +/// Reports terminal failure. +/// The failed variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class TaskClientUpdateFailed : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "failed"; - /// Transport to be used for provider requests. - [JsonPropertyName("transport")] - public ProviderEndpointTransport? Transport { get; set; } + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("code")] + public string? Code { get; set; } - /// Provider family. Matches the `type` field of a BYOK provider config. - [JsonPropertyName("type")] - public ProviderEndpointType Type { get; set; } + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public required string Error { get; set; } - /// Wire API to be used, when required for the provider type. - [JsonPropertyName("wireApi")] - public ProviderEndpointWireApi? WireApi { get; set; } + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } } -/// RPC data type for SessionProviderGetEndpoint operations. +/// Reports terminal cancellation after external work stopped. +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionProviderGetEndpointRequest +public partial class TaskClientUpdateCancelled : TaskClientUpdate { - /// 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; } + /// + [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; } } -/// RPC data type for SessionProviderGetEndpointRequestWithSession operations. +/// Updates a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionProviderGetEndpointRequestWithSession +internal sealed class TasksUpdateRequest { - /// 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; } + /// 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; -} -/// 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; } + /// Progress or terminal update payload. + [JsonPropertyName("update")] + public TaskClientUpdate Update { get => field ??= new(); set; } } -/// RPC data type for ProtocolSystemMessageAppendConfig operations. +/// 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 ProtocolSystemMessageAppendConfig +public sealed class TasksRefreshResult { - /// Text appended to the standard system prompt. - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// Append-mode discriminator. Omission also selects append mode. - [JsonPropertyName("mode")] - public ProtocolAppendMode? Mode { get; set; } } -/// RPC data type for SystemMessageBlock operations. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SystemMessageBlock +internal sealed class SessionTasksRefreshRequest { - /// Whether providers with explicit prompt caching should place a cache breakpoint after this block. - [JsonPropertyName("cacheBreakpoint")] - public bool? CacheBreakpoint { get; set; } - - /// Text content for this system-message block. - [JsonPropertyName("content")] - public string Content { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// RPC data type for ProtocolSystemMessageReplaceConfig operations. +/// 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 ProtocolSystemMessageReplaceConfig +public sealed class TasksWaitForPendingResult { - /// Complete replacement system-message text. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - - /// Optional structured blocks corresponding to the replacement content. - [JsonPropertyName("contentBlocks")] - public IList? ContentBlocks { get; set; } - - /// Replace-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolReplaceMode Mode { get; set; } } -/// RPC data type for ProtocolStaticSectionOverride operations. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolStaticSectionOverride +internal sealed class SessionTasksWaitForPendingRequest { - /// Declarative operation applied to the section. - [JsonPropertyName("action")] - public ProtocolStaticSectionAction Action { get; 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. +/// 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 = "action", + TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverrideTransform), "transform")] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverridePreserve), "preserve")] -public partial class ProtocolMarkerSectionOverride +[JsonDerivedType(typeof(TaskProgressAgent), "agent")] +[JsonDerivedType(typeof(TaskProgressClient), "client")] +[JsonDerivedType(typeof(TaskProgressShell), "shell")] +public partial class TaskProgress { /// The type discriminator. - [JsonPropertyName("action")] - public virtual string Action { get; set; } = string.Empty; + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// The transform variant of . +/// Timestamped display line for task progress output or recent agent activity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ProtocolMarkerSectionOverrideTransform : ProtocolMarkerSectionOverride +public sealed class TaskProgressLine { - /// - [JsonIgnore] - public override string Action => "transform"; + /// 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; } } -/// The preserve 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 ProtocolMarkerSectionOverridePreserve : ProtocolMarkerSectionOverride +public partial class TaskProgressAgent : TaskProgress { /// [JsonIgnore] - public override string Action => "preserve"; + 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; } } -/// JSON union data type for ProtocolSectionOverride. +/// Generic progress for a client-owned task. +/// The client variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSectionOverride +public partial class TaskProgressClient : TaskProgress { - /// Gets the value when this instance contains . - public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } + /// + [JsonIgnore] + public override string Type => "client"; - /// Gets the value when this instance contains . - public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } + /// Most recent nonempty progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lastMessage")] + public string? LastMessage { get; set; } - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolStaticSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolStaticSectionOverride = value; - } + /// Current completion percentage from zero through one hundred. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); + /// Current owner-defined progress phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolMarkerSectionOverride = value; - } + /// Recent server-timestamped progress messages. + [JsonPropertyName("recentActivity")] + public required IList RecentActivity { get; set; } - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { 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."); - } + /// Current client task lifecycle status. + [JsonPropertyName("status")] + public required TaskClientStatus Status { 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); - } + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public required DateTimeOffset UpdatedAt { get; set; } +} - throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); - } +/// 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 TaskProgressShell : TaskProgress +{ + /// + [JsonIgnore] + public override string Type => "shell"; - /// - 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; - } + /// Process ID when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pid")] + public long? Pid { get; set; } - throw new JsonException("No ProtocolSectionOverride variant value is set."); - } - } + /// Recent stdout/stderr lines from the running shell command. + [JsonPropertyName("recentOutput")] + public required string RecentOutput { get; set; } } -/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. +/// Progress information for the task, or null when no task with that ID is tracked. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageCustomizeConfig +public sealed class TasksGetProgressResult { - /// Text appended after the customized sections. - [JsonPropertyName("content")] - public string? Content { 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; } +} - /// Customize-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolCustomizeMode Mode { get; set; } +/// Identifier of the background task to fetch progress for. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksGetProgressRequest +{ + /// Task identifier (agent ID or shell ID). + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Named standard-prompt section overrides. - [JsonPropertyName("sections")] - public IDictionary? Sections { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// JSON union data type for ProtocolSystemMessageConfig. +/// The first sync-waiting task that can currently be promoted to background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSystemMessageConfig +public sealed class TasksGetCurrentPromotableResult { - /// Gets the value when this instance contains . - public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } + /// 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; } +} - /// Gets the value when this instance contains . - public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionTasksGetCurrentPromotableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Gets the value when this instance contains . - public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } +/// Indicates whether the task was successfully promoted to background mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class TasksPromoteToBackgroundResult +{ + /// Whether the task was successfully promoted to background mode. + [JsonPropertyName("promoted")] + public bool Promoted { get; set; } +} - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageAppendConfig = value; - } +/// Identifier of the task to promote to background mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksPromoteToBackgroundRequest +{ + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageReplaceConfig = value; - } +/// 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 TasksPromoteCurrentToBackgroundResult +{ + /// 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; } +} - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) => new(value); +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionTasksPromoteCurrentToBackgroundRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageCustomizeConfig = value; - } +/// Indicates whether the background task was successfully cancelled. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class TasksCancelResult +{ + /// Whether the task was successfully cancelled. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); +/// Identifier of the background task to cancel. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksCancelRequest +{ + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// 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."); - } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - using var document = JsonDocument.ParseValue(ref reader); - var element = document.RootElement; - if (element.ValueKind == JsonValueKind.Object && (!element.TryGetProperty("mode", out _) || (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "append")))) - { - var protocolSystemMessageAppendConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); - return protocolSystemMessageAppendConfig is null ? throw new JsonException("Expected ProtocolSystemMessageAppendConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageAppendConfig); - } - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "replace"))) - { - var protocolSystemMessageReplaceConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); - return protocolSystemMessageReplaceConfig is null ? throw new JsonException("Expected ProtocolSystemMessageReplaceConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageReplaceConfig); - } - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "customize"))) - { - var protocolSystemMessageCustomizeConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); - return protocolSystemMessageCustomizeConfig is null ? throw new JsonException("Expected ProtocolSystemMessageCustomizeConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageCustomizeConfig); - } +/// Indicates whether the task was removed. False when the task does not exist or is still running/idle. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class TasksRemoveResult +{ + /// Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). + [JsonPropertyName("removed")] + public bool Removed { get; set; } +} - throw new JsonException("JSON value did not match any ProtocolSystemMessageConfig variant."); - } +/// Identifier of the completed or cancelled task to remove from tracking. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksRemoveRequest +{ + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// - 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; - } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - throw new JsonException("No ProtocolSystemMessageConfig variant value is set."); - } - } +/// Indicates whether the message was delivered, with an error message when delivery failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class TasksSendMessageResult +{ + /// Error message if delivery failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Whether the message was successfully delivered or steered. + [JsonPropertyName("sent")] + public bool Sent { get; set; } } -/// A BYOK model definition referencing a named provider. +/// Identifier of the target agent task, message content, and optional sender agent ID. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderModelConfig +internal sealed class TasksSendMessageRequest { - /// Optional capability overrides (vision, tool_calls, reasoning, etc.). - [JsonPropertyName("capabilities")] - public ModelCapabilitiesOverride? Capabilities { get; set; } + /// Agent ID of the sender, if sent on behalf of another agent. + [JsonPropertyName("fromAgentId")] + public string? FromAgentId { 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`. + /// Agent task identifier. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Maximum context window tokens for the model. - [JsonPropertyName("maxContextWindowTokens")] - public double? MaxContextWindowTokens { get; set; } + /// Message content to send to the agent. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// Maximum output tokens for the model. - [JsonPropertyName("maxOutputTokens")] - public double? MaxOutputTokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Maximum prompt/input tokens for the model. - [JsonPropertyName("maxPromptTokens")] - public double? MaxPromptTokens { get; set; } +/// 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 Skill +{ + /// Optional freeform hint describing the skill's expected arguments, from the `argument-hint` frontmatter field. + [JsonPropertyName("argumentHint")] + public string? ArgumentHint { get; set; } - /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } + /// Canonical slash command name used to invoke the skill, without the leading '/'. + [JsonPropertyName("commandName")] + public string? CommandName { get; set; } - /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// Description of what the skill does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + /// 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; } + public string Name { get; set; } = string.Empty; - /// Name of the configured provider that serves this model. - [JsonPropertyName("provider")] - public string Provider { get; set; } = string.Empty; + /// Absolute path to the skill file. + [JsonPropertyName("path")] + public string? Path { 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; } + /// Name of the plugin that provides the skill, when source is 'plugin'. + [JsonPropertyName("pluginName")] + public string? PluginName { get; set; } - /// The model name sent to the provider API for inference. Defaults to `id`. - [JsonPropertyName("wireModel")] - public string? WireModel { 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; } } -/// Azure-specific provider options. +/// Skills available to the session, with their enabled state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderConfigAzure +public sealed class SkillList { - /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - [JsonPropertyName("apiVersion")] - public string? ApiVersion { get; set; } + /// Available skills. + [JsonPropertyName("skills")] + public IList Skills { get => field ??= []; set; } } -/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NamedProviderConfig +internal sealed class SessionSkillsListRequest { - /// Static API key used to authenticate provider requests. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } - - /// Azure authentication configuration for the provider. - [JsonPropertyName("azure")] - public ProviderConfigAzure? Azure { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Base URL for provider API requests. - [JsonPropertyName("baseUrl")] - public string BaseUrl { 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; } - /// Static bearer token used to authenticate provider requests. - [JsonPropertyName("bearerToken")] - public string? BearerToken { get; set; } + /// Full content of the skill file. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Whether the host supplies bearer tokens dynamically. - [JsonPropertyName("hasBearerTokenProvider")] - public bool? HasBearerTokenProvider { get; set; } + /// Whether model invocation was disabled when this skill was invoked. + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } - /// Additional HTTP headers included with provider requests. - [JsonPropertyName("headers")] - public IDictionary? Headers { get; set; } + /// Turn number when the skill was invoked. + [JsonPropertyName("invokedAtTurn")] + public long InvokedAtTurn { get; set; } - /// Unique provider name used to qualify model selection IDs. + /// Unique identifier for the skill. [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; } + /// 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; } -/// BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. +/// Skills invoked during this session, ordered by invocation time (most recent last). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ProviderAddRequest +public sealed class SkillsGetInvokedResult { - /// 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; } + /// Skills invoked during this session, ordered by invocation time (most recent last). + [JsonPropertyName("skills")] + public IList Skills { get => field ??= []; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionSkillsGetInvokedRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The selectable model entries and selection ids synthesized for the synchronized BYOK models. +/// Name of the skill to enable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderSyncResult +internal sealed class SkillsEnableRequest { - /// 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; } + /// Name of the skill to enable. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Provider-qualified model selection ids present after synchronization. - [JsonPropertyName("selectionIds")] - public IList SelectionIds { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Authoritative BYOK provider and model registry snapshot to apply atomically to the session. +/// Name of the skill to disable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ProviderSyncRequest +internal sealed class SkillsDisableRequest { - /// 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; } + /// 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; } -/// Indicates whether the session options patch was applied successfully. +/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionUpdateOptionsResult +public sealed class SkillsLoadDiagnostics { - /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated. - [JsonPropertyName("pluginHookCount")] - public long? PluginHookCount { get; set; } + /// Errors emitted while loading skills (e.g. skills that failed to load entirely). + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Warnings emitted while loading skills (e.g. skills that loaded but had issues). + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicyRuleSource +internal sealed class SessionSkillsReloadRequest { - /// Name of the policy source. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Type of the policy source. - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Single content-exclusion rule supplied to `session.options.update`, with paths, match conditions, and source. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicyRule +internal sealed class SessionSkillsEnsureLoadedRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Path patterns covered by this rule. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } +/// The IDE a host is connected to, as reported to the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionConnectedIdeInfo +{ + /// Display name of the connected IDE, for example `VS Code`. + [JsonPropertyName("ideName")] + public string IdeName { get; set; } = string.Empty; - /// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. - [JsonPropertyName("source")] - public OptionsUpdateAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } + /// Absolute path of the workspace folder the IDE has open. + [JsonPropertyName("workspaceFolder")] + public string WorkspaceFolder { get; set; } = string.Empty; } -/// Content-exclusion policy supplied to `session.options.update`, with rules, last-updated data, and scope. +/// Records which IDE the host is connected to, or clears it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicy +internal sealed class SessionMcpSetConnectedIdeInfoParams { - /// 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; } + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + [JsonPropertyName("ide")] + public SessionConnectedIdeInfo? Ide { get; set; } - /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. - [JsonPropertyName("scope")] - public OptionsUpdateAdditionalContentExclusionPolicyScope Scope { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Options scoped to the built-in CAPI (Copilot API) provider. +/// Recorded MCP server connection failure. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CapiSessionOptions +public sealed class McpServerFailureInfo { - /// 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; } + /// Failure message produced when the MCP server connection failed. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// 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; } + /// epoch-ms timestamp at which the failure was recorded. + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } } -/// Installed plugin record for a session, with marketplace, version, install time, enabled state, cache path, and source. +/// Recorded MCP server pending-auth state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionInstalledPlugin +public sealed class McpServerNeedsAuthInfo { - /// 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; } + /// epoch-ms timestamp at which the server signalled it needs authentication. + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } +} - /// Installation timestamp (ISO-8601). - [JsonPropertyName("installed_at")] - public string InstalledAt { get; set; } = string.Empty; +/// 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; } - /// 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; } + /// Configured servers that are explicitly disabled. + [JsonPropertyName("disabledServers")] + public IList DisabledServers { get => field ??= []; set; } - /// Marketplace the plugin came from (empty string for direct repo installs). - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; + /// Map of server name to recorded connection failure. + [JsonPropertyName("failedServers")] + public IDictionary FailedServers { get => field ??= new Dictionary(); set; } - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Configured servers filtered out by MCP server policy. + [JsonPropertyName("filteredServers")] + public IList FilteredServers { get => field ??= []; set; } - /// Source descriptor for direct repo installs (when marketplace is empty). - [JsonPropertyName("source")] - public JsonElement? Source { get; set; } + /// Whether third-party MCP servers are policy-enabled for this session. + [JsonPropertyName("mcp3pEnabled")] + public bool Mcp3pEnabled { 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; } + /// Map of server name to recorded pending-auth state. + [JsonPropertyName("needsAuthServers")] + public IDictionary NeedsAuthServers { get => field ??= new Dictionary(); set; } - /// Installed version, if known. - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Names of servers with in-flight connection attempts. + [JsonPropertyName("pendingConnections")] + public IList PendingConnections { get => field ??= []; set; } } -/// Custom model-provider configuration (BYOK). +/// Owned installation that a listed MCP server's live configuration came from. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderConfig +public sealed class McpServerOwnership { - /// 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; } + /// Stable installation identifier from the owned installation receipt. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; +} - /// Maximum context window tokens for the model. - [JsonPropertyName("maxContextWindowTokens")] - public double? MaxContextWindowTokens { get; set; } +/// MCP server status entry, including config source/plugin source and any connection error. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpServer +{ + /// Human-readable display name supplied by a managed server catalog. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Maximum output tokens for the model. - [JsonPropertyName("maxOutputTokens")] - public double? MaxOutputTokens { get; set; } + /// Error message if the server failed to connect. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Maximum prompt/input tokens for the model. - [JsonPropertyName("maxPromptTokens")] - public double? MaxPromptTokens { 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; - /// Overrides for model capabilities when they cannot be inferred from modelId. - [JsonPropertyName("modelCapabilities")] - public ModelCapabilitiesOverride? ModelCapabilities { 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; } - /// 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; } + /// 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; } - /// Provider name used for model and telemetry attribution. - [JsonPropertyName("providerName")] - public string? ProviderName { get; set; } + /// Configuration source: user, workspace, plugin, builtin, or managed. + [JsonPropertyName("source")] + public McpServerSource? Source { get; set; } - /// Provider transport. Defaults to "http". - [JsonPropertyName("transport")] - public ProviderConfigTransport? Transport { get; set; } + /// Plugin name that provided this server, when source is plugin. + [JsonPropertyName("sourcePlugin")] + public string? SourcePlugin { get; set; } - /// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. - [JsonPropertyName("type")] - public ProviderConfigType? Type { get; set; } + /// Plugin version that provided this server, when source is plugin. + [JsonPropertyName("sourcePluginVersion")] + public string? SourcePluginVersion { get; set; } - /// Wire API format (openai/azure only). Defaults to "completions". - [JsonPropertyName("wireApi")] - public ProviderConfigWireApi? WireApi { get; set; } + /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured. + [JsonPropertyName("status")] + public McpServerStatus Status { 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; } + /// Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// 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 servers configured for the session, with their connection status and host-level state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigAuth +public sealed class McpServerList { - /// Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the OS keyring the sandbox blocks. Default: false (opt-in). - [JsonPropertyName("gh")] - public bool? Gh { get; set; } + /// Host-level state, omitted when no MCP host is initialized. + [JsonPropertyName("host")] + public McpHostState? Host { get; set; } - /// Whether to inject git credentials as an `http.<url>.extraheader` so authenticated HTTPS git works inside the sandbox without the shell-based credential helper the sandbox blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's own helper before the sandbox is applied. Default: false (opt-in). - [JsonPropertyName("git")] - public bool? Git { get; set; } + /// Configured MCP servers. + [JsonPropertyName("servers")] + public IList Servers { get => field ??= []; set; } } -/// macOS seatbelt experimental options. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyExperimentalSeatbelt +internal sealed class SessionMcpListRequest { - /// 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; } -/// Platform-specific experimental policy fields. +/// Observational state for a matching already materialized MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyExperimental +public sealed class McpConfiguredServerState { - /// macOS seatbelt experimental options. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicyExperimentalSeatbelt? Seatbelt { get; set; } + /// Observed connection error, when the materialized server failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Observed connection status. This is not a configuration or readiness guarantee. + [JsonPropertyName("status")] + public McpServerStatus Status { get; set; } } -/// Filesystem rules to merge into the base policy. +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyFilesystem +public sealed class McpConfiguredServer { - /// Whether to clear the policy when the session exits. - [JsonPropertyName("clearPolicyOnExit")] - public bool? ClearPolicyOnExit { get; set; } + /// Human-readable display name supplied by configuration. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Paths explicitly denied. - [JsonPropertyName("deniedPaths")] - public IList? DeniedPaths { get; set; } + /// Whether this configured server is enabled after session configuration and policy filtering. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Paths granted read-only access. - [JsonPropertyName("readonlyPaths")] - public IList? ReadonlyPaths { 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; } - /// Paths granted read/write access. - [JsonPropertyName("readwritePaths")] - public IList? ReadwritePaths { 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; } } -/// HTTP proxy configuration for sandboxed traffic. +/// Effective MCP configuration with optional live observations from matching already materialized servers. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyNetworkProxy +public sealed class McpConfiguredServerList { - /// 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; + /// Effective configured MCP servers. + [JsonPropertyName("servers")] + public IList Servers { get => field ??= []; 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; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpListConfiguredRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Network rules to merge into the base policy. +/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyNetwork +public sealed class McpToolUi { - /// 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; } + /// 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; } - /// Whether traffic to local/loopback addresses is allowed. - [JsonPropertyName("allowLocalNetwork")] - public bool? AllowLocalNetwork { get; set; } + /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + [JsonPropertyName("visibility")] + public IList? Visibility { get; set; } +} - /// Whether outbound network traffic is allowed at all. - [JsonPropertyName("allowOutbound")] - public bool? AllowOutbound { 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; } - /// 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; } + /// Tool name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// 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; } + /// 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; } } -/// macOS seatbelt-specific options. +/// Tools exposed by the connected MCP server. Throws when the server is not connected. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicySeatbelt +public sealed class McpListToolsResult { - /// Whether the macOS seatbelt profile may access the keychain. - [JsonPropertyName("keychainAccess")] - public bool? KeychainAccess { get; set; } + /// Tools exposed by the server. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } } -/// User-managed sandbox policy fragment merged into the auto-discovered base policy. +/// Server name whose tool list should be returned. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicy +internal sealed class McpListToolsRequest { - /// 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; } + /// 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; - /// macOS seatbelt options to merge into the base policy. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicySeatbelt? Seatbelt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Resolved sandbox configuration. +/// Name of the MCP server to enable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfig +public sealed class McpEnableRequest { - /// Whether to auto-add the current working directory to readwritePaths. Default: true. - [JsonPropertyName("addCurrentWorkingDirectory")] - public bool? AddCurrentWorkingDirectory { get; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { 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; } + /// Name of the MCP server to enable. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} - /// Whether 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; } +/// 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; } - /// Credential-injection capability flags. - [JsonPropertyName("auth")] - public SandboxConfigAuth? Auth { 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; - /// Whether sandboxing is enabled for the session. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. - [JsonInclude] - [JsonPropertyName("managedLspRoutingLocked")] - internal bool? ManagedLspRoutingLocked { get; set; } +/// Name of the MCP server to disable for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpDisableRequest +{ + /// Required for an owned installation; omission preserves only manual-server behaviour. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// 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; } + /// Name of the MCP server to disable. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} - /// Whether 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; } +/// 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; } - /// 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; } + /// 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; - /// User-managed sandbox policy fragment merged into the auto-discovered base policy. - [JsonPropertyName("userPolicy")] - public SandboxConfigUserPolicy? UserPolicy { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// -/// Command-scoped GitHub credential injection for the shell commands an agent runs. -/// -/// Each channel is opt-in and independent, and injection is scoped to the individual command -/// spawn: the credential is resolved from the session's *current* authentication at every spawn -/// and reaches only spawns whose script actually invokes `git` or `gh`. Because nothing is -/// retained between spawns, replacing the session credential (`session.gitHubAuth.setCredentials`) -/// changes what the next spawned command presents — which seeding a credential into the runtime -/// process's own environment cannot do, since a child's environment is fixed at `exec`. -/// -/// The credential is matched to the host it authenticates to, so a github.com credential is never -/// presented to a GitHub Enterprise host and vice versa. Where a channel cannot express that -/// boundary it injects nothing rather than crossing it -- see `gh` below. -/// -/// This is independent of `sandboxConfig`: it is a decision about which identity the agent -/// presents, not about what the agent may touch, and it works on every platform whether or not -/// an OS sandboxing backend is available. `sandboxConfig.auth` remains the sandbox-scoped -/// spelling and is additive with this one. -/// +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellCredentials +internal sealed class SessionMcpReloadRequest { - /// - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// A host-provided script sourced before each built-in shell command when its shell target matches the active shell. +/// Result of moving in-flight MCP loading to the background. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellInitScript +public sealed class MoveMcpLoadingToBackgroundResult { - /// Path to the script to source. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// 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; } +} - /// Built-in shell that may source this script. - [JsonPropertyName("shell")] - public ShellInitScriptShell Shell { 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; } -/// Per-session settings for built-in shell tools. +/// MCP server allowed by policy, with server name and optional PII-free explanatory note. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellOptions +public sealed class McpAllowedServer { - /// Command-scoped GitHub credential injection for shell commands. - [JsonPropertyName("credentials")] - public ShellCredentials? Credentials { get; set; } + /// Allowed server name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Controls automatic non-interactive profile loading where supported. Explicit initScripts are unaffected. - [JsonPropertyName("initProfile")] - public ShellInitProfile? InitProfile { get; set; } + /// PII-free note explaining why the server was allowed. + [JsonPropertyName("redactedNote")] + public string? RedactedNote { 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; } +/// 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; } - /// - /// 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; } + /// The config key of the server that failed to connect. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Patch of mutable session options to apply to the running session. +/// MCP server filtered by policy, with name, reason, and optional redacted reason. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUpdateOptionsParams +public sealed class McpFilteredServer { - /// 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; } + /// 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; } - /// Runtime context discriminator (e.g., `cli`, `actions`). - [JsonPropertyName("agentContext")] - public string? AgentContext { get; set; } + /// Filtered server name. + [JsonPropertyName("name")] + public string Name { 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; } + /// Human-readable filter reason. + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; - /// Whether to disable the `ask_user` tool (encourages autonomous behavior). - [JsonPropertyName("askUserDisabled")] - public bool? AskUserDisabled { get; set; } + /// PII-free filter reason. + [JsonPropertyName("redactedReason")] + public string? RedactedReason { get; set; } +} - /// Allowlist of tool names available to this session. - [JsonPropertyName("availableTools")] - public IList? AvailableTools { 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; } - /// Options scoped to the built-in CAPI (Copilot API) provider. - [JsonPropertyName("capi")] - public CapiSessionOptions? Capi { get; set; } + /// Servers whose connection attempt failed. + [JsonPropertyName("failedServers")] + public IList? FailedServers { get; set; } - /// Identifier of the client driving the session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// Servers filtered out before startup. + [JsonPropertyName("filteredServers")] + public IList FilteredServers { get => field ??= []; set; } +} - /// Whether to include the `Co-authored-by` trailer in commit messages. - [JsonPropertyName("coauthorEnabled")] - public bool? CoauthorEnabled { get; set; } +/// 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; +} - /// 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; } +/// MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpExecuteSamplingResult +{ +} - /// Whether to allow auto-mode continuation across turns. - [JsonPropertyName("continueOnAutoMode")] - public bool? ContinueOnAutoMode { get; set; } +/// 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; } - /// Override URL for the Copilot API endpoint. - [JsonPropertyName("copilotUrl")] - public string? CopilotUrl { get; set; } + /// Error description, present when action='failure'. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Whether to default custom agents to local-only execution. - [JsonPropertyName("customAgentsLocalOnly")] - public bool? CustomAgentsLocalOnly { 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; } +} - /// Instruction source IDs to exclude from the system prompt. - [JsonPropertyName("disabledInstructionSources")] - public IList? DisabledInstructionSources { 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 +{ +} - /// Skill IDs that should be excluded from this session. - [JsonPropertyName("disabledSkills")] - public IList? DisabledSkills { 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; } - /// Whether to enable loading of `.github/hooks/` filesystem hooks. Separate from the SDK callback hook mechanism. - [JsonPropertyName("enableFileHooks")] - public bool? EnableFileHooks { 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; } - /// Whether to enable host git operations (context resolution, child repo scanning, git info in system prompt). - [JsonPropertyName("enableHostGitOperations")] - public bool? EnableHostGitOperations { 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; - /// 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; } + /// Name of the MCP server that initiated the sampling request. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Whether to surface reasoning-summary events from the model. - [JsonPropertyName("enableReasoningSummaries")] - public bool? EnableReasoningSummaries { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether shell-script safety heuristics are enabled. - [JsonPropertyName("enableScriptSafety")] - public bool? EnableScriptSafety { 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; } +} - /// Whether to enable cross-session store writes and reads. - [JsonPropertyName("enableSessionStore")] - public bool? EnableSessionStore { 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; - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether the session is running in an interactive UI. - [JsonPropertyName("runningInInteractiveMode")] - public bool? RunningInInteractiveMode { get; set; } +/// 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; } +} - /// Resolved sandbox configuration. - [JsonPropertyName("sandboxConfig")] - public SandboxConfig? SandboxConfig { get; set; } +/// Mode controlling how MCP server env values are resolved (`direct` or `indirect`). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpSetEnvValueModeParams +{ + /// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". + [JsonPropertyName("mode")] + public McpSetEnvValueModeDetails Mode { get; set; } - /// Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. - [JsonInclude] - [JsonPropertyName("sandboxConfigSource")] - internal SandboxConfigSource? SandboxConfigSource { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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; } +/// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpRemoveGitHubResult +{ + /// True when the auto-managed `github` MCP server was removed; false when no removal happened (e.g. user has explicitly configured a `github` server, or the server was not registered). + [JsonPropertyName("removed")] + public bool Removed { get; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpRemoveGitHubRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// Optional session limits. Pass null to clear the session limits. - [JsonPropertyName("sessionLimits")] - public SessionLimitsConfig? SessionLimits { get; set; } +/// Result of configuring GitHub MCP. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpConfigureGitHubResult +{ + /// Whether GitHub MCP configuration changed. + [JsonPropertyName("changed")] + public bool Changed { get; set; } +} - /// Per-session settings for built-in shell tools. - [JsonPropertyName("shell")] - public ShellOptions? Shell { 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; +} - /// 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; } +/// 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; } - /// PowerShell process flags applied to built-in and user-requested shell commands. - [JsonPropertyName("shellProcessFlags")] - public IList? ShellProcessFlags { get; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Additional directories to search for skills. - [JsonPropertyName("skillDirectories")] - public IList? SkillDirectories { get; set; } + /// Name of the MCP server to start. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} - /// Whether to skip loading custom instruction sources. - [JsonPropertyName("skipCustomInstructions")] - public bool? SkipCustomInstructions { 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; } - /// Whether to skip embedding retrieval pipeline initialization and execution. - [JsonPropertyName("skipEmbeddingRetrieval")] - public bool? SkipEmbeddingRetrieval { get; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { 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; } + /// Name of the MCP server to start. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. - [JsonPropertyName("toolFilterPrecedence")] - public OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Optional path for trajectory output. - [JsonPropertyName("trajectoryFile")] - public string? TrajectoryFile { 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)] +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; } - /// Output verbosity level for supported models. - [JsonPropertyName("verbosity")] - public Verbosity? Verbosity { get; set; } + /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Absolute working-directory path for shell tools. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } + /// Name of the MCP server to restart. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Parameters for (re)loading the merged LSP configuration 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 LspInitializeRequest +internal sealed class McpRestartServerRequestWithSession { - /// Force re-initialization even when LSP configs were already loaded for the working directory. - [JsonPropertyName("force")] - public bool? Force { 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; } - /// Git root used as the boundary when traversing for project-level LSP configs (supports monorepos). - [JsonPropertyName("gitRoot")] - public string? GitRoot { 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; - - /// 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. +/// Server name for an individual MCP server stop. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Extension +public sealed class McpStopServerRequest { - /// 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; } + /// Exact owned receipt identity. Stop also forgets this session's durable activation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Current status: running, disabled, failed, or starting. - [JsonPropertyName("status")] - public ExtensionStatus Status { get; set; } + /// Name of the MCP server to stop. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Extensions discovered for the session, with their current status. +/// Server name for an individual MCP server stop. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExtensionList +internal sealed class McpStopServerRequestWithSession { - /// Discovered extensions and their current status. - [JsonPropertyName("extensions")] - public IList Extensions { get => field ??= []; set; } -} + /// Exact owned receipt identity. Stop also forgets this session's durable activation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } + + /// Name of the MCP server to stop. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; -/// 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. +/// Registration parameters for an external MCP client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ExtensionsEnableRequest +internal sealed class McpRegisterExternalClientRequest { - /// Source-qualified extension ID to enable. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Logical server name for the external client. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Source-qualified extension identifier to disable for the session. +/// Server name identifying the external client to remove. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ExtensionsDisableRequest +internal sealed class McpUnregisterExternalClientRequest { - /// Source-qualified extension ID to disable. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Server name of the external client to unregister. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Whether the named MCP server is running. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionExtensionsReloadRequest +public sealed class McpIsServerRunningResult +{ + /// True if the server has an active client and transport. + [JsonPropertyName("running")] + public bool Running { get; set; } +} + +/// Server name to check running status for. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpIsServerRunningRequest { + /// Name of the MCP server to check. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// 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 pending MCP OAuth response was accepted. [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 McpOauthHandlePendingResult { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { 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; } } - -/// Optional line range to scope the attachment to a specific section of the file. +/// Host response to the pending OAuth request. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentFileLineRange +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpOauthPendingRequestResponseToken), "token")] +[JsonDerivedType(typeof(McpOauthPendingRequestResponseCancelled), "cancelled")] +public partial class McpOauthPendingRequestResponse { - /// 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; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// File attachment. -/// The file variant of . + +/// The token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentFile : PushAttachment +public partial class McpOauthPendingRequestResponseToken : McpOauthPendingRequestResponse { /// [JsonIgnore] - public override string Type => "file"; + public override string Kind => "token"; - /// User-facing display name for the attachment. - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } + /// Access token acquired by the SDK host. + [JsonPropertyName("accessToken")] + public required string AccessToken { get; set; } - /// Optional line range to scope the attachment to a specific section of the file. + /// Token lifetime in seconds, if known. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("lineRange")] - public PushAttachmentFileLineRange? LineRange { get; set; } + [JsonPropertyName("expiresIn")] + public long? ExpiresIn { get; set; } - /// Absolute file path. - [JsonPropertyName("path")] - public required string Path { get; set; } + /// OAuth token type. Defaults to bearer when omitted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenType")] + public string? TokenType { get; set; } } -/// Directory attachment. -/// The directory variant of . +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentDirectory : PushAttachment +public partial class McpOauthPendingRequestResponseCancelled : McpOauthPendingRequestResponse { /// [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; } + public override string Kind => "cancelled"; } -/// End position of the selection. +/// Pending MCP OAuth request ID and host-provided token or cancellation response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentSelectionDetailsEnd +internal sealed class McpOauthHandlePendingRequest { - /// End character offset within the line (0-based). - [JsonPropertyName("character")] - public long Character { get; set; } + /// OAuth request identifier from the mcp.oauth_required event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// End line number (0-based). - [JsonPropertyName("line")] - public long Line { get; set; } + /// 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; } -/// Start position of the selection. +/// Identifies the MCP server whose persisted OAuth credentials were updated. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentSelectionDetailsStart +internal sealed class McpOauthAuthenticationStateChangedRequest { - /// Start character offset within the line (0-based). - [JsonPropertyName("character")] - public long Character { 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; } - /// Start line number (0-based). - [JsonPropertyName("line")] - public long Line { 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; } -/// Position range of the selection within the file. +/// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentSelectionDetails +public sealed class SessionMcpOauthPrepareLoginResult { - /// End position of the selection. - [JsonPropertyName("end")] - public PushAttachmentSelectionDetailsEnd End { get => field ??= new(); set; } + /// Original expiry, not extended by consumption, retries or cancellation. + [JsonPropertyName("expiresAt")] + public DateTimeOffset ExpiresAt { get; set; } - /// Start position of the selection. - [JsonPropertyName("start")] - public PushAttachmentSelectionDetailsStart Start { 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; } -/// Code selection attachment from an editor. -/// The selection variant of . +/// Effect-free preparation bound to the existing local session, requester and installation, with frozen options. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentSelection : PushAttachment +internal sealed class SessionMcpOauthPrepareLoginRequest { - /// - [JsonIgnore] - public override string Type => "selection"; + /// 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; } - /// User-facing display name for the selection. - [JsonPropertyName("displayName")] - public required string DisplayName { 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; } - /// Absolute path to the file containing the selection. - [JsonPropertyName("filePath")] - public required string FilePath { get; set; } + /// Exact installation identity from owned inventory, never a server-name alias. + [JsonPropertyName("expectedInstallationId")] + public string ExpectedInstallationId { get; set; } = string.Empty; - /// Position range of the selection within the file. - [JsonPropertyName("selection")] - public required PushAttachmentSelectionDetails Selection { get; set; } + /// Request a new authorisation rather than accepting a usable cached grant. + [JsonPropertyName("forceReauth")] + public bool? ForceReauth { get; set; } - /// The selected text content. - [JsonPropertyName("text")] - public required string Text { 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; } -/// GitHub issue, pull request, or discussion reference. -/// The github_reference variant of . +/// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubReference : PushAttachment +public sealed class McpOauthLoginResult { - /// - [JsonIgnore] - public override string Type => "github_reference"; - - /// Issue, pull request, or discussion number. - [JsonPropertyName("number")] - public required long Number { get; set; } + /// 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; } - /// 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; } + /// 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; } - /// Title of the referenced item. - [JsonPropertyName("title")] - public required string Title { get; set; } + /// Runtime-issued owned flow identity; never a server name or installation operation ID. + [JsonPropertyName("loginId")] + public string? LoginId { get; set; } - /// URL to the referenced item on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. + [JsonPropertyName("status")] + public McpOwnedOauthLoginStatus? Status { get; set; } } -/// Pointer to a GitHub repository. +/// 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 PushGitHubRepoRef +public sealed class McpOauthLoginRequest { - /// 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; -} + /// 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; } -/// 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"; + /// 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; } - /// First line of the commit message. - [JsonPropertyName("message")] - public required string Message { 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; } - /// Full commit SHA. - [JsonPropertyName("oid")] - public required string Oid { 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; } - /// Repository the commit belongs to. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// Exact owned receipt identity. Owned login never uses an implicit helper session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// URL to the commit on GitHub. - [JsonPropertyName("url")] - public required string Url { 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; } -/// 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"; + /// 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; } - /// Human-readable release name. - [JsonPropertyName("name")] - public required string Name { 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; } - /// Repository the release belongs to. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { 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; } - /// Git tag the release is anchored to. - [JsonPropertyName("tagName")] - public required string TagName { 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; } - /// URL to the release on GitHub. - [JsonPropertyName("url")] - public required string Url { 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; } } -/// Pointer to a GitHub Actions job. -/// The github_actions_job variant of . +/// 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 partial class PushAttachmentGitHubActionsJob : PushAttachment +internal sealed class McpOauthLoginRequestWithSession { - /// - [JsonIgnore] - public override string Type => "github_actions_job"; + /// 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; } - /// 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; } + /// 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; } - /// Job id within the workflow run. - [JsonPropertyName("jobId")] - public required long JobId { 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; } - /// Display name of the job. - [JsonPropertyName("jobName")] - public required string JobName { 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; } - /// Repository the workflow run belongs to. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// Exact owned receipt identity. Owned login never uses an implicit helper session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// URL to the job on GitHub. - [JsonPropertyName("url")] - public required string Url { 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; } - /// Display name of the workflow the job ran in. - [JsonPropertyName("workflowName")] - public required string WorkflowName { 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; } -/// 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"; + /// + /// 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; } - /// Short description of the repository. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description { 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; } - /// 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; } + /// 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; } - /// Repository pointer. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { 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; - /// URL to the repository on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// One side of a file diff (head or base). +/// Host-delivered callback for a runtime-managed MCP OAuth login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentGitHubFileDiffSide +internal sealed class McpOauthCompleteRequest { - /// Repository-relative path to the file. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Opaque identifier returned by session.mcp.oauth.login for the pending external callback. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("authorizationId")] + public string AuthorizationId { get; set; } = string.Empty; - /// Git ref (branch, tag, or commit SHA) the file is read at. - [JsonPropertyName("ref")] - public string Ref { 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; - /// Repository the file lives in. - [JsonPropertyName("repo")] - public PushGitHubRepoRef Repo { get => field ??= new(); set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Pointer to a single-file diff. At least one of `head` and `base` must be present. -/// The github_file_diff variant of . +/// 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)] -public partial class PushAttachmentGitHubFileDiff : PushAttachment +[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 { - /// - [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; } + /// The type discriminator. + [JsonPropertyName("status")] + public virtual string Status { get; set; } = string.Empty; } -/// 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 . +/// The no-auth-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubTreeComparison : PushAttachment +public partial class McpOauthProbeResultNoAuthRequired : McpOauthProbeResult { /// [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 Status => "no-auth-required"; - /// URL to the comparison on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// HTTP response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } } -/// Generic GitHub URL reference. -/// The github_url variant of . +/// The authenticated variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubUrl : PushAttachment +public partial class McpOauthProbeResultAuthenticated : McpOauthProbeResult { /// [JsonIgnore] - public override string Type => "github_url"; + public override string Status => "authenticated"; - /// URL to the GitHub resource. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// HTTP response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } } -/// Pointer to a file in a GitHub repository at a specific ref. -/// The github_file variant of . +/// The needs-auth variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubFile : PushAttachment +public partial class McpOauthProbeResultNeedsAuth : McpOauthProbeResult { /// [JsonIgnore] - public override string Type => "github_file"; - - /// Repository-relative path to the file. - [JsonPropertyName("path")] - public required string Path { get; set; } + public override string Status => "needs-auth"; - /// Git ref the file is read at (branch, tag, or commit SHA). - [JsonPropertyName("ref")] - public required string Ref { get; set; } + /// HTTP 401 or 403 response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } - /// Repository the file lives in. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// Why authentication is needed. + [JsonPropertyName("reason")] + public required McpOauthProbeNeedsAuthReason Reason { get; set; } - /// URL to the file on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Parsed WWW-Authenticate challenge parameters, when present and parseable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("wwwAuthenticateParams")] + public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; } } -/// Pointer to a line range inside a file in a GitHub repository. -/// The github_snippet variant of . +/// The failed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubSnippet : PushAttachment +public partial class McpOauthProbeResultFailed : McpOauthProbeResult { /// [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; } + public override string Status => "failed"; - /// Repository the file lives in. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// Human-readable probe failure detail. + [JsonPropertyName("error")] + public required string Error { get; set; } - /// URL to the snippet on GitHub (with line anchor). - [JsonPropertyName("url")] - public required string Url { 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; } } -/// Blob attachment with inline base64-encoded data. -/// The blob variant of . +/// Remote MCP server name for a passive OAuth status probe. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentBlob : PushAttachment +public sealed class McpOauthProbeRequest { - /// - [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; } + /// Exact owned receipt identity; probing never activates a dormant installation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// MIME type of the inline data. - [JsonPropertyName("mimeType")] - public required string MimeType { 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; } } -/// Slim input shape for extension_context attachments; identity fields are runtime-derived. -/// The extension_context variant of . +/// Remote MCP server name for a passive OAuth status probe. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentExtensionContext : PushAttachment +internal sealed class McpOauthProbeRequestWithSession { - /// - [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; } + /// Exact owned receipt identity; probing never activates a dormant installation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Human-readable composer pill label. + /// 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("title")] - public required string Title { get; set; } + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Parameters for session.extensions.sendAttachmentsToMessage. +/// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SendAttachmentsToMessageParams +public sealed class SessionMcpOauthCancelLoginResult { - /// 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; } + /// True after cancellation settles, false when the original login already connected successfully. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} - /// Optional canvas instance binding the push for provenance. When supplied, the runtime resolves the canvas, verifies it is owned by the calling extension, and stamps canvasId/instanceId onto each extension_context entry. When omitted, no resolution runs and those fields stay unset on the attachment. - [JsonPropertyName("instanceId")] - public string? InstanceId { get; set; } +/// Targets only the original prepared/applying owned login on this exact session requester. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; } -/// A tool name and arguments to execute through the session's native invocation pipeline. +/// Indicates whether the pending MCP OAuth response was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsExecuteRequest +public sealed class McpOauthRespondResult { - /// Arguments supplied to the tool. - [JsonPropertyName("arguments")] - public JsonElement Arguments { 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; } +} - /// Name of the currently offered tool to execute. - [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; +} - /// Optional identifier used to correlate this invocation with its tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } +/// Indicates whether the pending MCP headers refresh response was accepted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult +{ + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Custom grammar input format accepted by a built-in tool. +/// Host response: supply dynamic headers or decline this refresh. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolFormat +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestHeaders), "headers")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestNone), "none")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestError), "error")] +public partial class McpHeadersHandlePendingHeadersRefreshRequest { - /// Grammar definition accepted by the tool. - [JsonPropertyName("definition")] - public string Definition { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { 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; } +/// The headers variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHeadersHandlePendingHeadersRefreshRequest +{ + /// + [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; } } -/// JSON Schema object accepted by a built-in tool. +/// The none variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolInputSchema +public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeadersHandlePendingHeadersRefreshRequest { - /// Root type of the tool input schema. - [JsonPropertyName("type")] - public BuiltinToolInputSchemaType Type { get; set; } + /// + [JsonIgnore] + public override string Kind => "none"; } -/// Rust-owned metadata and input schema for a built-in tool. +/// The error variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolDescriptor +public partial class McpHeadersHandlePendingHeadersRefreshRequestError : McpHeadersHandlePendingHeadersRefreshRequest { - /// 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; } + /// + [JsonIgnore] + public override string Kind => "error"; - /// Whether the tool provides a specialized intention summary. - [JsonPropertyName("hasSummariseIntention")] - public bool HasSummariseIntention { get; set; } + /// Host credential broker failure, denial, or revocation reason. + [JsonPropertyName("message")] + public required string Message { get; set; } +} - /// JSON Schema for the tool input, or null when the tool uses a custom format. - [JsonPropertyName("inputSchema")] - public BuiltinToolInputSchema? InputSchema { get; set; } +/// MCP headers refresh request id and the host response. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest +{ + /// Headers refresh request identifier from mcp.headers_refresh_required. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Optional supplemental usage instructions for the tool. - [JsonPropertyName("instructions")] - public string? Instructions { get; set; } + /// Host response: supply dynamic headers or decline this refresh. + [JsonPropertyName("result")] + public McpHeadersHandlePendingHeadersRefreshRequest Result { get => field ??= new(); set; } - /// Whether the tool executes commands in a terminal. - [JsonPropertyName("isTerminal")] - public bool IsTerminal { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Stable name used to invoke the built-in tool. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// 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; } - /// Policy describing which tool metadata may be recorded without obfuscation. - [JsonPropertyName("safeForTelemetry")] - public JsonElement SafeForTelemetry { get; set; } + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } - /// Optional human-readable title for the tool. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Optional tool category discriminator. - [JsonPropertyName("type")] - public string? Type { 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; } -/// Rust-owned built-in tool descriptors for the session. +/// Resource contents returned by the MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsGetBuiltinDescriptorsResult +public sealed class McpAppsReadResourceResult { - /// Built-in tool descriptors materialized for the session. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } } -/// Shell-specific names and description lines used to materialize built-in shell tool descriptors. +/// MCP server and resource URI to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsShellDescriptorConfig +internal sealed class McpAppsReadResourceRequest { - /// Additional model-facing shell description lines. - [JsonPropertyName("descriptionLines")] - public IList DescriptionLines { get => field ??= []; set; } + /// Name of the MCP server hosting the resource. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Human-readable shell name. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Tool name used to list active shells. - [JsonPropertyName("listShellsToolName")] - public string ListShellsToolName { get; set; } = string.Empty; + /// Resource URI (typically ui://...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; +} - /// Tool name used to read shell output. - [JsonPropertyName("readShellToolName")] - public string ReadShellToolName { get; set; } = string.Empty; +/// App-callable tools from the named MCP server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpAppsListToolsResult +{ + /// App-callable tools from the server. + [JsonPropertyName("tools")] + public IList> Tools { get => field ??= []; set; } +} - /// Tool name used to start shell commands. - [JsonPropertyName("shellToolName")] - public string ShellToolName { get; set; } = string.Empty; +/// MCP server to list app-callable tools for. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpAppsListToolsRequest +{ + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("originServerName")] + public string OriginServerName { get; set; } = string.Empty; - /// Stable shell type identifier. - [JsonPropertyName("shellType")] - public string ShellType { 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; - /// Tool name used to stop shell commands. - [JsonPropertyName("stopShellToolName")] - public string StopShellToolName { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Options controlling how Rust-owned built-in tool descriptors are materialized. +/// MCP server, tool name, and arguments to invoke from an MCP App view. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsGetBuiltinDescriptorsRequest +internal sealed class McpAppsCallToolRequest { - /// Whether background task completion notifications are enabled. - [JsonPropertyName("backgroundTaskNotificationsEnabled")] - public bool? BackgroundTaskNotificationsEnabled { get; set; } + /// Tool arguments. + [JsonPropertyName("arguments")] + public IDictionary? Arguments { get; set; } - /// Whether tool descriptors should include authoring metadata. - [JsonPropertyName("includeAuthor")] - public bool? IncludeAuthor { 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; - /// Whether descriptors should favor fewer user-intervention prompts. - [JsonPropertyName("reduceUserIntervention")] - public bool? ReduceUserIntervention { 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; - /// 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; } + /// MCP tool name. + [JsonPropertyName("toolName")] + public string ToolName { get; set; } = string.Empty; } -/// Task completion notification with summary from the agent. +/// Host context advertised to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskCompleteData +public sealed class McpAppsSetHostContextDetails { - /// Structured blocker details when outcome is blocked. - [JsonPropertyName("blocker")] - public TaskBlocker? Blocker { get; set; } + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } - /// Active autopilot objective ID evaluated by the completion reviewer. - [JsonPropertyName("objectiveId")] - public long? ObjectiveId { get; set; } + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsSetHostContextDetailsDisplayMode? DisplayMode { get; set; } - /// Semantic completion decision. Absent on legacy events and invalid tool calls. - [JsonPropertyName("outcome")] - public TaskCompletionOutcome? Outcome { get; set; } + /// BCP-47 locale, e.g. 'en-US'. + [JsonPropertyName("locale")] + public string? Locale { 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; } + /// Platform type for responsive design. + [JsonPropertyName("platform")] + public McpAppsSetHostContextDetailsPlatform? Platform { 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; } + /// UI theme preference per SEP-1865. + [JsonPropertyName("theme")] + public McpAppsSetHostContextDetailsTheme? Theme { get; set; } - /// Summary of the completed task, provided by the agent. - [JsonPropertyName("summary")] - public string? Summary { 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; } } -/// Binary result returned by a tool for the model. +/// Host context to advertise to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm +internal sealed class McpAppsSetHostContextRequest { - /// Base64-encoded binary data. - [Base64String] - [JsonPropertyName("data")] - public string Data { get; set; } = string.Empty; + /// Host context advertised to MCP App guests. + [JsonPropertyName("context")] + public McpAppsSetHostContextDetails Context { get => field ??= new(); set; } - /// Human-readable description of the binary data. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Optional metadata from the producing tool. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } +/// Current host context. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpAppsHostContextDetails +{ + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } - /// MIME type of the binary data. - [JsonPropertyName("mimeType")] - public string MimeType { get; set; } = string.Empty; + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsHostContextDetailsDisplayMode? DisplayMode { get; set; } - /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. - [JsonPropertyName("type")] - public ExternalToolTextResultForLlmBinaryResultsForLlmType Type { get; set; } + /// BCP-47 locale, e.g. 'en-US'. + [JsonPropertyName("locale")] + public string? Locale { get; set; } + + /// Platform type for responsive design. + [JsonPropertyName("platform")] + public McpAppsHostContextDetailsPlatform? Platform { get; set; } + + /// UI theme preference per SEP-1865. + [JsonPropertyName("theme")] + public McpAppsHostContextDetailsTheme? Theme { get; set; } + + /// IANA timezone, e.g. 'America/New_York'. + [JsonPropertyName("timeZone")] + public string? TimeZone { get; set; } + + /// Host application identifier. + [JsonPropertyName("userAgent")] + public string? UserAgent { get; set; } } -/// A content block within a tool result, which may be text, terminal output, image, audio, or a resource. -/// Polymorphic base type discriminated by type. +/// Current host context advertised to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentText), "text")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentTerminal), "terminal")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentShellExit), "shell_exit")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentImage), "image")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentAudio), "audio")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentResourceLink), "resource_link")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentResource), "resource")] -public partial class ExternalToolTextResultForLlmContent +public sealed class McpAppsHostContext { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + /// Current host context. + [JsonPropertyName("context")] + public McpAppsHostContextDetails Context { get => field ??= new(); set; } } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpAppsGetHostContextRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} -/// Plain text content block. -/// The text variant of . +/// Capability negotiation snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentText : ExternalToolTextResultForLlmContent +public sealed class McpAppsDiagnoseCapability { - /// - [JsonIgnore] - public override string Type => "text"; + /// Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers. + [JsonPropertyName("advertised")] + public bool Advertised { get; set; } - /// The text content. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on. + [JsonPropertyName("featureFlagEnabled")] + public bool FeatureFlagEnabled { get; set; } + + /// Whether the session has the `mcp-apps` capability. + [JsonPropertyName("sessionHasMcpApps")] + public bool SessionHasMcpApps { get; set; } } -/// Terminal/shell output content block with optional exit code and working directory. -/// The terminal variant of . +/// What the server returned for this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentTerminal : ExternalToolTextResultForLlmContent +public sealed class McpAppsDiagnoseServer { - /// - [JsonIgnore] - public override string Type => "terminal"; + /// Whether the named server is currently connected. + [JsonPropertyName("connected")] + public bool Connected { get; set; } - /// Working directory where the command was executed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// Up to 5 tool names with `_meta.ui` for quick inspection. + [JsonPropertyName("sampleToolNames")] + public IList SampleToolNames { get => field ??= []; set; } - /// Process exit code, if the command has completed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("exitCode")] - public long? ExitCode { get; set; } + /// Total tools returned by the server's tools/list. + [JsonPropertyName("toolCount")] + public double ToolCount { get; set; } - /// Terminal/shell output text. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). + [JsonPropertyName("toolsWithUiMeta")] + public double ToolsWithUiMeta { get; set; } } -/// Shell command exit metadata with optional output preview. -/// The shell_exit variant of . +/// Diagnostic snapshot of MCP Apps wiring for the named server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentShellExit : ExternalToolTextResultForLlmContent +public sealed class McpAppsDiagnoseResult { - /// - [JsonIgnore] - public override string Type => "shell_exit"; + /// Capability negotiation snapshot. + [JsonPropertyName("capability")] + public McpAppsDiagnoseCapability Capability { get => field ??= new(); set; } - /// Working directory where the shell command was executed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// What the server returned for this session. + [JsonPropertyName("server")] + public McpAppsDiagnoseServer Server { get => field ??= new(); set; } +} - /// Exit code from the completed shell command. - [JsonPropertyName("exitCode")] - public required long ExitCode { get; set; } +/// MCP server to diagnose MCP Apps wiring for. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpAppsDiagnoseRequest +{ + /// 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; - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// 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; } +/// 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; } - /// Whether outputPreview is known to be incomplete or truncated. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("outputTruncated")] - public bool? OutputTruncated { get; set; } + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } - /// Shell id, as assigned by Copilot runtime. - [JsonPropertyName("shellId")] - public required string ShellId { get; set; } + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// Text content (e.g. HTML). + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// Image content block with base64-encoded data. -/// The image variant of . +/// Resource contents returned by the MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentImage : ExternalToolTextResultForLlmContent +public sealed class McpResourcesReadResult { - /// - [JsonIgnore] - public override string Type => "image"; + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } +} - /// Base64-encoded image data. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } +/// MCP server and resource URI to fetch. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpResourcesReadRequest +{ + /// 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; - /// MIME type of the image (e.g., image/png, image/jpeg). - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// Audio content block with base64-encoded data. -/// The audio variant of . +/// Standard MCP resource annotations plus preserved non-standard annotation fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentAudio : ExternalToolTextResultForLlmContent +public sealed class McpResourceAnnotations { - /// - [JsonIgnore] - public override string Type => "audio"; + /// Server-provided non-standard annotation fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Base64-encoded audio data. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } + /// Intended audience roles for this resource. + [JsonPropertyName("audience")] + public IList? Audience { get; set; } - /// MIME type of the audio (e.g., audio/wav, audio/mpeg). - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } + /// Last-modified timestamp hint. + [JsonPropertyName("lastModified")] + public string? LastModified { get; set; } + + /// Priority hint for model/client use. + [JsonPropertyName("priority")] + public double? Priority { get; set; } } -/// Icon image for a resource. +/// A resource icon descriptor plus preserved non-standard icon fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExternalToolTextResultForLlmContentResourceLinkIcon +public sealed class McpResourceIcon { - /// MIME type of the icon image. + /// Server-provided non-standard icon fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Icon MIME type, when known. [JsonPropertyName("mimeType")] public string? MimeType { get; set; } - /// Available icon sizes (e.g., ['16x16', '32x32']). + /// Icon sizes hint. [JsonPropertyName("sizes")] - public IList? Sizes { get; set; } + public string? Sizes { get; set; } - /// URL or path to the icon image. + /// Icon URI. [JsonPropertyName("src")] public string Src { get; set; } = string.Empty; - /// Theme variant this icon is intended for. + /// Theme hint for this icon. [JsonPropertyName("theme")] - public ExternalToolTextResultForLlmContentResourceLinkIconTheme? Theme { get; set; } + public string? Theme { get; set; } } -/// Resource link content block referencing an external resource. -/// The resource_link variant of . +/// An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, MIME type, size, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentResourceLink : ExternalToolTextResultForLlmContent +public sealed class McpResource { - /// - [JsonIgnore] - public override string Type => "resource_link"; + /// Resource-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Human-readable description of the resource. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Model/client annotations associated with this resource. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } + + /// Optional description of what this resource represents. [JsonPropertyName("description")] public string? Description { get; set; } /// Icons associated with this resource. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("icons")] - public IList? Icons { get; set; } + public IList? Icons { get; set; } - /// MIME type of the resource content. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// MIME type of the resource, if known. [JsonPropertyName("mimeType")] public string? MimeType { get; set; } - /// Resource name identifier. + /// The programmatic name of the resource. [JsonPropertyName("name")] - public required string Name { get; set; } + public string Name { get; set; } = string.Empty; - /// Size of the resource in bytes. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Resource size in bytes, when known. [JsonPropertyName("size")] public long? Size { get; set; } - /// Human-readable display title for the resource. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Optional human-readable display title. [JsonPropertyName("title")] public string? Title { get; set; } - /// URI identifying the resource. + /// The resource URI (e.g. ui://... or file:///...). [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; } + public string Uri { get; set; } = string.Empty; } -/// A message injected by a tool result. +/// One page of resources advertised by the named MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolResultNewMessage +public sealed class McpResourcesListResult { - /// Message content to inject after the tool result. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Opaque cursor for the next page, if the server has more resources. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } - /// Source attributed to the injected message. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; + /// Resources advertised by the server (proxied MCP `resources/list`). + [JsonPropertyName("resources")] + public IList Resources { get => field ??= []; set; } } -/// RPC data type for TaskCompletionDecision operations. +/// MCP server whose resources to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskCompletionDecision +internal sealed class McpResourcesListRequest { - /// 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; } + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Whether the rationale was derived from completion-reviewer output. - [JsonPropertyName("reviewerDerived")] - public bool? ReviewerDerived { 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; - /// Information-flow metadata captured from the completion reviewer. - [JsonPropertyName("reviewerResultMeta")] - public JsonElement? ReviewerResultMeta { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Expanded canonical result returned by a session tool. +/// 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 ToolResultExpanded +public sealed class McpResourceTemplate { - /// 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; } + /// Resource-template-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Execution outcome classification. - [JsonPropertyName("resultType")] - public ToolResultType ResultType { get; set; } + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Detailed log content available for session display. - [JsonPropertyName("sessionLog")] - public string? SessionLog { get; set; } + /// Model/client annotations associated with this template. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } - /// Skill invocation metadata produced by the tool. - [JsonPropertyName("skillInvocation")] - public JsonElement? SkillInvocation { get; set; } + /// Optional description of what this template is for. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Whether large-output post-processing should be skipped. - [JsonPropertyName("skipLargeOutputProcessing")] - public bool? SkipLargeOutputProcessing { get; set; } + /// Icons associated with resources matching this template. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } - /// Structured result content in addition to the model-facing text. - [JsonPropertyName("structuredContent")] - public JsonElement? StructuredContent { get; set; } + /// MIME type for resources matching this template, if uniform. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Completion-review decision produced by the task-completion tool. - [JsonPropertyName("taskCompletionDecision")] - public TaskCompletionDecision? TaskCompletionDecision { get; set; } + /// The programmatic name of the resource template. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Text result returned to the model. - [JsonPropertyName("textResultForLlm")] - public string TextResultForLlm { get; set; } = string.Empty; + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } - /// Deferred tool names made available by this result. - [JsonPropertyName("toolReferences")] - public IList? ToolReferences { get; set; } + /// An RFC 6570 URI template for constructing resource URIs. + [JsonPropertyName("uriTemplate")] + public string UriTemplate { get; set; } = string.Empty; +} - /// Tool-specific telemetry payload. - [JsonPropertyName("toolTelemetry")] - public JsonElement? ToolTelemetry { get; set; } +/// One page of resource templates advertised by the named MCP server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpResourcesListTemplatesResult +{ + /// Opaque cursor for the next page, if the server has more resource templates. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } - /// Optional UI resource produced by the tool. - [JsonPropertyName("uiResource")] - public JsonElement? UiResource { get; set; } + /// Resource templates advertised by the server (proxied MCP `resources/templates/list`). + [JsonPropertyName("resourceTemplates")] + public IList ResourceTemplates { get => field ??= []; set; } } -/// Task-completion tool arguments and final result used to build a label-safe session event payload. +/// MCP server whose resource templates to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsTaskCompleteEventDataRequest +internal sealed class McpResourcesListTemplatesRequest { - /// Final expanded result returned by the task_complete tool. - [JsonPropertyName("finalResult")] - public ToolResultExpanded FinalResult { get => field ??= new(); set; } + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + + /// Name of the MCP server whose resource templates to enumerate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// 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. +/// An argument accepted by an MCP prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HandlePendingToolCallResult +public sealed class McpPromptArgument { - /// Whether the tool call result was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Argument-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { 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; } + /// Server-provided non-standard argument fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Request ID of the pending tool call. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Description of the argument. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Tool call result (string or expanded result object). - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Name of the argument. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the argument is required; omission is distinct from false. + [JsonPropertyName("required")] + public bool? Required { get; set; } } -/// Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. +/// An MCP prompt icon with standard size hints and preserved non-standard fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsInitializeAndValidateResult +public sealed class McpPromptIcon { -} + /// Server-provided non-standard icon fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } -/// 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; + /// 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; } } -/// Lightweight metadata for a currently initialized session tool. +/// An MCP prompt descriptor. Server-provided non-standard fields are exposed under `additionalProperties`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CurrentToolMetadata +public sealed class McpPrompt { - /// Whether the tool is loaded on demand via tool search. - [JsonPropertyName("deferLoading")] - public bool? DeferLoading { get; set; } + /// Prompt-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Tool description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Server-provided non-standard descriptor fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// JSON Schema for tool input. - [JsonPropertyName("input_schema")] - public IDictionary? InputSchema { get; set; } + /// Arguments accepted by the prompt. + [JsonPropertyName("arguments")] + public IList? Arguments { get; set; } - /// MCP server name for MCP-backed tools. - [JsonPropertyName("mcpServerName")] - public string? McpServerName { get; set; } + /// Description of what this prompt provides. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Raw MCP tool name for MCP-backed tools. - [JsonPropertyName("mcpToolName")] - public string? McpToolName { get; set; } + /// Icons associated with this prompt. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } - /// Model-facing tool name. + /// The programmatic name of the prompt. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Optional MCP/config namespaced tool name. - [JsonPropertyName("namespacedName")] - public string? NamespacedName { get; set; } + /// Human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } } -/// Current lightweight tool metadata snapshot for the session. +/// One page of prompts advertised by the named MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsGetCurrentMetadataResult +public sealed class McpPromptsListResult { - /// Current tool metadata, or null when tools have not been initialized yet. - [JsonPropertyName("tools")] - public IList? Tools { get; set; } + /// MCP result metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { 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; } } -/// Identifies the target session. +/// MCP server whose prompts to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionToolsGetCurrentMetadataRequest +internal sealed class McpPromptsListRequest { + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + + /// Name of the MCP server whose prompts to enumerate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// 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. +/// An MCP prompt message with opaque JSON content preserved without flattening or content-type filtering. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolExternalToolDefinition +public sealed class McpPromptMessage { - /// 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; + /// Message-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Whether the tool executes commands in a terminal. - [JsonPropertyName("isTerminal")] - public bool? IsTerminal { get; set; } + /// Server-provided non-standard message fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Optional caller-defined metadata associated with the tool. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } + /// The original MCP content block, including nested metadata and unfamiliar content types. + [JsonPropertyName("content")] + public JsonElement Content { get; set; } - /// Unique model-visible tool name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// The role of the message sender. + [JsonPropertyName("role")] + public McpPromptRole Role { get; set; } +} - /// Whether this definition replaces a built-in tool with the same name. - [JsonPropertyName("overridesBuiltInTool")] - public bool? OverridesBuiltInTool { 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; } - /// JSON Schema describing the tool's input arguments. - [JsonPropertyName("parameters")] - public IDictionary? Parameters { get; set; } + /// Server-provided non-standard result fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Whether execution bypasses the normal tool permission prompt. - [JsonPropertyName("skipPermission")] - public bool? SkipPermission { get; set; } + /// Description of the prompt. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Ordered prompt messages. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; set; } } -/// Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. +/// MCP server, prompt name, and optional string-valued arguments. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsSetRequest +internal sealed class McpPromptsGetRequest { + /// String-valued arguments to pass to the prompt. + [JsonPropertyName("arguments")] + public IDictionary? Arguments { 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; + + /// Name of the MCP server hosting the prompt. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Complete replacement list for the calling connection. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } } -/// Empty result after applying subagent settings. +/// MCP diagnostic source configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsUpdateSubagentSettingsResult +public sealed class McpDiagnosticSourceConfiguration { + /// Threshold to apply to MCP diagnostic producers in this session. + [JsonPropertyName("level")] + public DiagnosticLogLevel Level { get; set; } } -/// Subagent model, reasoning effort, context tier, and auto-invocation settings. +/// Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SubagentSettingsEntry +public sealed class DiagnosticSourcesConfiguration { - /// 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; } + /// MCP diagnostic capture threshold. Omit to leave the current threshold unchanged. + [JsonPropertyName("mcp")] + public McpDiagnosticSourceConfiguration? Mcp { get; set; } } -/// Configured per-agent subagent overrides. -public sealed class UpdateSubagentSettingsRequestSubagents +/// Per-source session diagnostics configuration. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DiagnosticsConfiguration { - /// 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; } + /// Diagnostic thresholds keyed by supported source. + [JsonPropertyName("sources")] + public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } } -/// Subagent settings to apply to the current session. +/// Patch session diagnostic thresholds for explicitly supplied sources. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UpdateSubagentSettingsRequest +internal sealed class DiagnosticsConfigureRequest { /// 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; } + /// Sources to configure. At least one supported source must be supplied. + [JsonPropertyName("sources")] + public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } } -/// RPC data type for SessionCommandsList operations. +/// 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 SessionCommandsListRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "source", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(DiagnosticEntryMcp), "mcp")] +public partial class DiagnosticEntry { - /// Include runtime built-in commands. - [JsonPropertyName("includeBuiltins")] - public bool? IncludeBuiltins { get; set; } - - /// Include commands registered by protocol clients, including SDK clients and extensions. - [JsonPropertyName("includeClientCommands")] - public bool? IncludeClientCommands { get; set; } - - /// Include enabled user-invocable skills and commands. - [JsonPropertyName("includeSkills")] - public bool? IncludeSkills { get; set; } + /// The type discriminator. + [JsonPropertyName("source")] + public virtual string Source { get; set; } = string.Empty; } -/// RPC data type for SessionCommandsListRequestWithSession operations. + +/// MCP-specific detail for a source-discriminated diagnostic entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCommandsListRequestWithSession +public sealed class McpDiagnosticDetails { - /// 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; } + /// Fresh identifier for the MCP connection attempt, including failed starts. + [JsonPropertyName("connectionId")] + public string ConnectionId { get; set; } = string.Empty; - /// Include enabled user-invocable skills and commands. - [JsonPropertyName("includeSkills")] - public bool? IncludeSkills { get; set; } + /// Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. + [JsonPropertyName("data")] + public string? Data { get; set; } - /// 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; } -/// 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. + /// Diagnostic record category. [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + public McpDiagnosticKind Kind { get; set; } + /// Configured MCP server name. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; +} -/// Slash-command invocation result containing text output plus Markdown/ANSI rendering flags. -/// The text variant of . +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultText : SlashCommandInvocationResult +public partial class DiagnosticEntryMcp : DiagnosticEntry { /// [JsonIgnore] - public override string Kind => "text"; + public override string Source => "mcp"; - /// Whether text contains Markdown. + /// Agent identifier for a subagent host. Omitted for the root agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("markdown")] - public bool? Markdown { get; set; } + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } - /// Whether ANSI sequences should be preserved. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("preserveAnsi")] - public bool? PreserveAnsi { get; set; } + /// Typed MCP diagnostic detail. + [JsonPropertyName("details")] + public required McpDiagnosticDetails Details { 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; } + /// Severity of this emitted diagnostic record. + [JsonPropertyName("level")] + public required DiagnosticSeverity Level { 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. + /// Human-readable diagnostic summary. + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// Original byte count when a known-size message or data value was truncated. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("sandboxSessionChange")] - public SandboxSessionChange? SandboxSessionChange { get; set; } + [JsonPropertyName("originalBytes")] + public long? OriginalBytes { get; set; } - /// Text output for the client to render. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// UTC RFC 3339 timestamp captured at the diagnostic source. + [JsonPropertyName("timestamp")] + public required string Timestamp { get; set; } + + /// Whether message or data was truncated to the record-size bound. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("truncated")] + public bool? Truncated { get; set; } } -/// 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 . +/// One cursor-addressed page of retained session diagnostics. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultAgentPrompt : SlashCommandInvocationResult +public sealed class DiagnosticsReadResult { - /// - [JsonIgnore] - public override string Kind => "agent-prompt"; - - /// Prompt text to display to the user. - [JsonPropertyName("displayPrompt")] - public required string DisplayPrompt { get; set; } + /// Opaque cursor for the next independent read. + [JsonPropertyName("cursor")] + public string Cursor { get; set; } = string.Empty; - /// Optional target session mode for the agent prompt. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("mode")] - public SessionMode? Mode { get; set; } + /// Whether the requested cursor remained within the retained buffer window. + [JsonPropertyName("cursorStatus")] + public DiagnosticCursorStatus CursorStatus { get; set; } - /// Optional user-facing notice to show before the prompt is submitted. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("notice")] - public string? Notice { 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; } - /// Prompt to submit to the agent. - [JsonPropertyName("prompt")] - public required string Prompt { get; set; } + /// Retained records beginning at the requested cursor. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; 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; } + /// Whether additional retained records follow this page. + [JsonPropertyName("hasMore")] + public bool HasMore { get; set; } } -/// Slash-command invocation result indicating completion, with optional message and settings-change flag. -/// The completed variant of . +/// 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 partial class SlashCommandInvocationResultCompleted : SlashCommandInvocationResult +internal sealed class DiagnosticsReadRequest { - /// - [JsonIgnore] - public override string Kind => "completed"; + /// Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Optional user-facing message describing the completed command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Maximum number of records to return, from 1 through 500. Omit for 100. + [JsonPropertyName("max")] + public long? Max { get; set; } - /// Optional target session mode applied without submitting an agent prompt. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("mode")] - public SessionMode? Mode { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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; } + /// 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; } } -/// Selectable slash-command subcommand option with name, description, and optional group label. +/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandSelectSubcommandOption +public sealed class ConnectorCapabilities { - /// Human-readable description of the subcommand. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } - /// Optional group label for organizing options. - [JsonPropertyName("group")] - public string? Group { get; set; } + /// Current session availability. Disabled availability is reported without making a Connector request. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { get; set; } - /// Subcommand name to invoke. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; -} + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + [JsonPropertyName("consentContinuation")] + public bool ConsentContinuation { get; set; } -/// 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"; + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + [JsonPropertyName("maxDeadlineMs")] + public long MaxDeadlineMs { get; set; } - /// Parent command name that requires subcommand selection. - [JsonPropertyName("command")] - public required string Command { get; set; } + /// Maximum accepted polling attempts for one continuation call. + [JsonPropertyName("maxPollAttempts")] + public long MaxPollAttempts { get; set; } - /// Available subcommand options for the client to present. - [JsonPropertyName("options")] - public required IList Options { get; set; } + /// Maximum accepted delay in milliseconds between polling attempts. + [JsonPropertyName("maxPollIntervalMs")] + public long MaxPollIntervalMs { 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; } + /// 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; } - /// Human-readable title for the selection UI. - [JsonPropertyName("title")] - public required string Title { get; set; } + /// Whether getAccount is supported. Absence means false. + [JsonPropertyName("sessionAccountSelection")] + public bool? SessionAccountSelection { get; set; } + + /// Whether reconcile accepts forceConnectorName. Absence means false. + [JsonPropertyName("targetedReconcile")] + public bool? TargetedReconcile { get; set; } } -/// RPC data type for SlashCommandTimelineEntry operations. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandTimelineEntry +internal sealed class SessionConnectorsGetCapabilitiesRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The add-timeline-entry variant of . +/// Credential-free identity metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultAddTimelineEntry : SlashCommandInvocationResult +public sealed class AuthIdentityMetadata { - /// - [JsonIgnore] - public override string Kind => "add-timeline-entry"; - - /// Timeline entry the host should append. - [JsonPropertyName("entry")] - public required SlashCommandTimelineEntry Entry { get; set; } + /// Identity host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Optional text the host should prefill into the input editor. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("prefillInput")] - public string? PrefillInput { get; set; } + /// User login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Authentication type. + [JsonPropertyName("type")] + public AuthInfoType Type { get; set; } } -/// RPC data type for SlashCommandModelPickerDialog operations. +/// Session account selection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandModelPickerDialog +public sealed class ConnectorSessionAccount { - /// 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; } + /// Opaque session-scoped account selection ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// Model-selection target represented by the picker. - [JsonPropertyName("target")] - public string? Target { get; set; } + /// Credential-free identity metadata. + [JsonPropertyName("authInfo")] + public AuthIdentityMetadata AuthInfo { get => field ??= new(); set; } } -/// The show-dialog variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultShowDialog : SlashCommandInvocationResult +internal sealed class SessionConnectorsGetAccountRequest { - /// - [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; } + /// 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 +/// 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; } } -/// The set-model variant of . +/// Credential-free Connector catalog entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSetModel : SlashCommandInvocationResult +public sealed class ConnectorCatalogEntry { - /// - [JsonIgnore] - public override string Kind => "set-model"; + /// Untrusted service description, when present. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Model selected by the command. - [JsonPropertyName("model")] - public required string Model { get; set; } + /// Untrusted display label from the service. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Reasoning effort selected for the model. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + /// Optional catalog logo. + [JsonPropertyName("logo")] + public string? Logo { get; set; } - /// Repository settings scope modified by the command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("repoScope")] - public string? RepoScope { get; set; } + /// Canonical Connector name used by lifecycle methods. + [JsonPropertyName("name")] + public string Name { 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; } + /// Optional catalog release tag. + [JsonPropertyName("releaseTag")] + public string? ReleaseTag { get; set; } - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Opaque stable runtime IDs currently projected into the session for this Connector. + [JsonPropertyName("runtimeServerIds")] + public IList RuntimeServerIds { get => field ??= []; set; } - /// Settings scope modified by the command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("scope")] - public string? Scope { get; set; } + /// Current authoritative service connection state. + [JsonPropertyName("status")] + public ConnectorCatalogStatus Status { get; set; } - /// User-facing warning produced while selecting the model. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("warning")] - public string? Warning { get; set; } + /// Optional catalog tier. + [JsonPropertyName("tier")] + public string? Tier { get; set; } } -/// The set-plan-model variant of . +/// Validated Connector catalog snapshot cached by the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSetPlanModel : SlashCommandInvocationResult +public sealed class ConnectorCatalogResult { - /// - [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; } + /// Validated catalog entries in service order. + [JsonPropertyName("connectors")] + public IList Connectors { get => field ??= []; set; } - /// Dedicated model selected for plan mode. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("planModel")] - public string? PlanModel { get; set; } + /// Unix epoch milliseconds when this snapshot was accepted. + [JsonPropertyName("refreshedAtMs")] + public long RefreshedAtMs { get; set; } - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Monotonically increasing session-local catalog revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } } -/// Slash command name and optional raw input string to invoke. +/// Live status of one session-owned MCP projection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsInvokeRequest +public sealed class ConnectorRuntimeStatus { - /// 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; + /// Canonical Connector name that owns this server. + [JsonPropertyName("connectorName")] + public string ConnectorName { get; set; } = string.Empty; - /// Optional client surface that initiated the invocation. - [JsonPropertyName("origin")] - public CommandsInvocationOrigin? Origin { get; set; } + /// Opaque runtime server ID. + [JsonPropertyName("runtimeServerId")] + public string RuntimeServerId { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Current live MCP host status. + [JsonPropertyName("status")] + public ConnectorMcpStatus Status { get; set; } } -/// Whether finalizing the invocation effect succeeded, and the failure reason when it did not. +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsFinalizeInvocationEffectResult +public sealed class ConnectorStatus { - /// Failure reason when the invocation effect could not be finalized. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Opaque account selection pinned to this session, when one has been selected. + [JsonPropertyName("accountId")] + public string? AccountId { get; set; } - /// Whether the pending invocation effect was finalized successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } -/// The slash-command result object that produced the pending effect, echoed back unchanged. -public sealed class CommandsFinalizeInvocationEffectRequestEffect -{ -} + /// Exact selected account and stable scope requiring an authorization update, when proven by the Connector service. + [JsonPropertyName("authorizationRequirement")] + public ConnectorAuthorizationRequirement? AuthorizationRequirement { get; set; } -/// 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; } + /// Current feature and session availability. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { get; set; } - /// Whether the host applied or cancelled the pending invocation effect. - [JsonPropertyName("outcome")] - public CommandsInvocationEffectOutcome Outcome { get; set; } + /// Latest validated catalog snapshot, when available. + [JsonPropertyName("catalog")] + public ConnectorCatalogResult? Catalog { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Indicates whether the pending client-handled command was completed successfully. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CommandsHandlePendingCommandResult +internal sealed class SessionConnectorsGetStatusRequest { - /// Whether the command was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Pending command request ID and an optional error if the client handler failed. +/// 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 CommandsHandlePendingCommandRequest +internal sealed class ConnectorAccountRequest { - /// 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; + /// 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; } -/// Error message produced while executing the command, if any. +/// Typed result of initiating or continuing a Connector connection. +/// Polymorphic base type discriminated by kind. [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 +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ConnectorConnectResultConnected), "connected")] +[JsonDerivedType(typeof(ConnectorConnectResultConsentRequired), "consent_required")] +[JsonDerivedType(typeof(ConnectorConnectResultPending), "pending")] +public partial class ConnectorConnectResult { - /// 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; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Indicates whether the command was accepted into the local execution queue. -/// Data type discriminated by queued. + +/// The service is connected and the session MCP graph was reconciled. +/// The connected variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EnqueueCommandResult +public partial class ConnectorConnectResultConnected : ConnectorConnectResult { - /// The boolean discriminator. - [JsonPropertyName("queued")] - public bool Queued { get; set; } + /// + [JsonIgnore] + public override string Kind => "connected"; - /// Stable opaque ID of the queued command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("queueId")] - public string? QueueId { get; set; } + /// Fresh authoritative Connector state after MCP reconciliation. + [JsonPropertyName("status")] + public required ConnectorStatus Status { get; set; } } -/// Slash-prefixed command string to enqueue for FIFO processing. +/// Host-owned consent is required before bounded continuation can complete. +/// The consent_required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EnqueueCommandParams +public partial class ConnectorConnectResultConsentRequired : ConnectorConnectResult { - /// 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; + /// + [JsonIgnore] + public override string Kind => "consent_required"; - /// Optional user-facing text for the queue row. The command string is shown when omitted. - [JsonPropertyName("displayText")] - public string? DisplayText { get; set; } + /// Validated HTTPS consent URL. The runtime does not open it. + [JsonPropertyName("consentUrl")] + public required string ConsentUrl { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { get; set; } } -/// Indicates whether the queued-command response was matched to a pending request. +/// The service is still completing the connection without a consent URL. +/// The pending variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CommandsRespondToQueuedCommandResult +public partial class ConnectorConnectResultPending : ConnectorConnectResult { - /// 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 => "pending"; + + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { get; set; } } -/// Result of the queued command execution. -/// Data type discriminated by handled. +/// Selects one Connector and the pinned host-owned account used for its service and MCP authorization. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class QueuedCommandResult +internal sealed class ConnectorConnectRequest { - /// The boolean discriminator. - [JsonPropertyName("handled")] - public bool Handled { get; set; } + /// 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; - /// 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; } + /// Canonical Connector name from the current catalog. + [JsonPropertyName("connectorName")] + public string ConnectorName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). +/// Explicitly bounded continuation of a pending Connector connection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsRespondToQueuedCommandRequest +internal sealed class ConnectorContinueRequest { - /// Request ID from the `command.queued` event the host is responding to. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + [JsonPropertyName("continuationId")] + public string ContinuationId { get; set; } = string.Empty; - /// Result of the queued command execution. - [JsonPropertyName("result")] - public QueuedCommandResult Result { get => field ??= new(); 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; } + + /// Maximum catalog requests made by this call. Must be between one and the capability limit. + [JsonPropertyName("maxAttempts")] + public int MaxAttempts { get; set; } + + /// Delay in milliseconds between attempts. Must not exceed the capability limit. + [JsonPropertyName("pollIntervalMs")] + public int PollIntervalMs { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Telemetry engagement ID for the session, when available. +/// Authoritative result after disconnect and MCP reconciliation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionTelemetryEngagement +public sealed class ConnectorDisconnectResult { - /// Current telemetry engagement ID, when available. - [JsonPropertyName("engagementId")] - public string? EngagementId { get; set; } + /// Whether the service accepted the idempotent disconnect. + [JsonPropertyName("disconnected")] + public bool Disconnected { get; set; } + + /// Fresh authoritative session state after removing Connector-owned MCP servers. + [JsonPropertyName("status")] + public ConnectorStatus Status { get => field ??= new(); set; } } -/// Identifies the target session. +/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTelemetryGetEngagementIdRequest +public sealed class ConnectorReconcileRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + [JsonPropertyName("accountId")] + public required string AccountId { 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; } } -/// Feature override key/value pairs to attach to subsequent telemetry events from this session. +/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TelemetrySetFeatureOverridesRequest +internal sealed class ConnectorReconcileRequestWithSession { - /// 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; } + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Optional 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; } -/// Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIEphemeralQueryResult +internal sealed class SessionConnectorsWithdrawProjectionRequest { - /// Answer returned by the model. - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Transient question to answer without adding it to conversation history. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIEphemeralQueryRequest +internal sealed class SessionManagedSettingsGetRequest { - /// 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; } -/// MCP response metadata. -public sealed class UIElicitationResponseMeta -{ -} - -/// The elicitation response (accept with form values, decline, or cancel). +/// Session plugin metadata, with name, marketplace, optional version, and enabled state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIElicitationResponse +public sealed class Plugin { - /// MCP response metadata. - [JsonPropertyName("_meta")] - public UIElicitationResponseMeta? Meta { get; set; } + /// Opaque stable identity for a direct plugin source. + [JsonPropertyName("directSourceId")] + public string? DirectSourceId { get; set; } - /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). - [JsonPropertyName("action")] - public UIElicitationResponseAction Action { get; set; } + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// The form values submitted by the user (present when action is 'accept'). - [JsonPropertyName("content")] - public IDictionary? Content { get; set; } -} + /// Whether this managed desired plugin has an installed or live record. + [JsonPropertyName("installed")] + public bool? Installed { get; set; } -/// MCP request metadata. -public sealed class UIElicitationRequestMeta -{ -} + /// Absolute marketplace directory for a live plugin. + [JsonPropertyName("installedFrom")] + public string? InstalledFrom { 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; } + /// Whether enterprise managed settings control this plugin. + [JsonPropertyName("managed")] + public bool? Managed { get; set; } - /// List of required field names. - [JsonPropertyName("required")] - public IList? Required { get; set; } + /// Enabled state required by enterprise managed settings. + [JsonPropertyName("managedDesiredEnabled")] + public bool? ManagedDesiredEnabled { get; set; } - /// Schema type indicator (always 'object'). - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; + /// Marketplace the plugin came from. + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; + + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Runtime plugin provenance, such as "builtin". + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// Installed version. + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Metadata controlling an MCP task's lifetime. +/// Plugins installed for the session, with their enabled state and version metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpTaskMetadata +public sealed class PluginList { - /// Task time-to-live. - [JsonPropertyName("ttl")] - public long? Ttl { get; set; } + /// Installed plugins. + [JsonPropertyName("plugins")] + public IList Plugins { get => field ??= []; set; } } -/// Prompt message and JSON schema describing the form fields to elicit from the user. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIElicitationRequest +internal sealed class SessionPluginsListRequest { - /// MCP request metadata. - [JsonPropertyName("_meta")] - public UIElicitationRequestMeta? 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. +/// Plugin source resolved relative to the session's authoritative working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIElicitationResult +internal sealed class SessionPluginsInstallRequest { - /// Whether the response was accepted. False if the request was already resolved by another client. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Plugin install spec. Accepts the same forms as the CLI: "plugin@marketplace" (marketplace install), "owner/repo" or "owner/repo:subpath" (GitHub direct), an http/https/ssh URL, or a local path. Direct (non-marketplace) installs are deprecated and will produce a deprecationWarning in the result. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; } -/// Pending elicitation request ID and the user's response (accept/decline/cancel + form values). +/// Name (or spec) of the plugin to uninstall. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingElicitationRequest +internal sealed class PluginsUninstallRequestWithSession { - /// The unique request ID from the elicitation.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Stable source identity for a direct (non-marketplace) install. Disambiguates uninstall when multiple installed plugins share the same name. + [JsonPropertyName("directSourceId")] + public string? DirectSourceId { get; set; } - /// The elicitation response (accept with form values, decline, or cancel). - [JsonPropertyName("result")] - public UIElicitationResponse Result { get => field ??= new(); 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; } -/// Indicates whether the pending UI request was resolved by this call. +/// Name (or spec) of the plugin to update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIHandlePendingResult +internal sealed class PluginsUpdateRequestWithSession { - /// 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; } + /// 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; } -/// User response for a pending user-input request, with answer text and whether it was typed freeform. +/// Plugin names (or specs) to enable in the session's authoritative working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIUserInputResponse +internal sealed class SessionPluginsEnableRequest { - /// The user's answer text. - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; + /// Plugin names or "plugin@marketplace" specs to enable. Unknown names are ignored. Non-marketplace direct installs are always enabled and cannot be toggled via this API. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Request ID of a pending `user_input.requested` event and the user's response. +/// Plugin names (or specs) to disable in the session's authoritative working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingUserInputRequest +internal sealed class SessionPluginsDisableRequest { - /// 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; } + /// 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; } -/// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. +/// RPC data type for SessionPluginsReload operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIHandlePendingSamplingResponse +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; } } -/// Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). +/// RPC data type for SessionPluginsReloadRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingSamplingRequest +internal sealed class SessionPluginsReloadRequestWithSession { - /// The unique request ID from the sampling.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// 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; } - /// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. - [JsonPropertyName("response")] - public UIHandlePendingSamplingResponse? Response { 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; } -/// Request ID of a pending `auto_mode_switch.requested` event and the user's response. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingAutoModeSwitchRequest +internal sealed class SessionPluginsMarketplacesListRequest { - /// 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. +/// Marketplace source and optional working directory for relative-path resolution. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UISessionLimitsExhaustedResponse +internal sealed class PluginsMarketplacesAddRequestWithSession { - /// Action selected by the user. - [JsonPropertyName("action")] - public UISessionLimitsExhaustedResponseAction Action { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// AI Credits to add to the current max when action is 'add'. - [JsonPropertyName("additionalAiCredits")] - public double? AdditionalAiCredits { get; set; } + /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; - /// New absolute max AI Credits when action is 'set'. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; set; } + /// Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. +/// Name of the marketplace to remove and an optional force flag. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingSessionLimitsExhaustedRequest +internal sealed class PluginsMarketplacesRemoveRequestWithSession { - /// The unique request ID from the session_limits_exhausted.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// 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; } - /// The selected session-limit action. - [JsonPropertyName("response")] - public UISessionLimitsExhaustedResponse Response { get => field ??= new(); 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; } -/// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. +/// Name of the marketplace whose plugin catalog to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIExitPlanModeResponse +internal sealed class PluginsMarketplacesBrowseRequestWithSession { - /// 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; } + /// Marketplace name to browse. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// 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. +/// RPC data type for SessionPluginsMarketplacesRefresh operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingExitPlanModeRequest +public sealed class SessionPluginsMarketplacesRefreshRequest { - /// The unique request ID from the exit_plan_mode.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. + [JsonPropertyName("name")] + public string? Name { 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; } +/// 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; } -/// 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). +/// 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 UIRegisterDirectAutoModeSwitchHandlerResult +public sealed class ProviderSessionToken { - /// 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; + /// 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; } -/// Identifies the target session. +/// A snapshot of the provider endpoint the session is currently configured to talk to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUiRegisterDirectAutoModeSwitchHandlerRequest +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; } -/// Indicates whether the handle was active and the registration count was decremented. +/// The selectable model entries synthesized for the models added by this call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIUnregisterDirectAutoModeSwitchHandlerResult +public sealed class ProviderAddResult { - /// True if the handle was active and decremented the counter; false if the handle was unknown. - [JsonPropertyName("unregistered")] - public bool Unregistered { get; set; } + /// 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; } } -/// Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. +/// 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 UIUnregisterDirectAutoModeSwitchHandlerRequest +internal sealed class ProviderAddRequest { - /// Handle previously returned by `registerDirectAutoModeSwitchHandler`. - [JsonPropertyName("handle")] - public string Handle { get; set; } = string.Empty; + /// 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; } -/// Indicates whether the operation succeeded. +/// The selectable model entries and selection ids synthesized for the synchronized BYOK models. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureResult +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.permissions.configure` content-exclusion rule, with source name and type. +/// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRuleSource +public sealed class OptionsUpdateAdditionalContentExclusionPolicyRuleSource { /// Name of the policy source. [JsonPropertyName("name")] @@ -18193,9 +19317,9 @@ public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRuleSour public string Type { get; set; } = string.Empty; } -/// Single content-exclusion rule supplied to `session.permissions.configure`, with paths, match conditions, and source. +/// Single content-exclusion rule supplied to `session.options.update`, with paths, match conditions, and source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRule +public sealed class OptionsUpdateAdditionalContentExclusionPolicyRule { /// Conditions of which at least one must match. [JsonPropertyName("ifAnyMatch")] @@ -18209,14 +19333,14 @@ public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRule [JsonPropertyName("paths")] public IList Paths { get => field ??= []; set; } - /// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. + /// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. [JsonPropertyName("source")] - public PermissionsConfigureAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } + public OptionsUpdateAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } } -/// Content-exclusion policy supplied to `session.permissions.configure`, with rules, last-updated data, and scope. +/// Content-exclusion policy supplied to `session.options.update`, with rules, last-updated data, and scope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicy +public sealed class OptionsUpdateAdditionalContentExclusionPolicy { /// Opaque policy update timestamp supplied by the host. [JsonPropertyName("last_updated_at")] @@ -18224,1558 +19348,6552 @@ public sealed class PermissionsConfigureAdditionalContentExclusionPolicy /// Content-exclusion rules to apply. [JsonPropertyName("rules")] - public IList Rules { get => field ??= []; set; } + public IList Rules { get => field ??= []; set; } - /// Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. + /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. [JsonPropertyName("scope")] - public PermissionsConfigureAdditionalContentExclusionPolicyScope Scope { 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsConfig -{ - /// 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; } + public OptionsUpdateAdditionalContentExclusionPolicyScope Scope { get; set; } } -/// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. +/// Options scoped to the built-in CAPI (Copilot API) provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionRulesSet +public sealed class CapiSessionOptions { - /// Rules that auto-approve matching requests. - [JsonPropertyName("approved")] - public IList Approved { get => field ??= []; set; } + /// Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. `fast` is an integrator-only latency preset, not a first-party GitHub Copilot product preference. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } - /// Rules that auto-deny matching requests. - [JsonPropertyName("denied")] - public IList Denied { get => field ??= []; 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; } } -/// 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. +/// 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 PermissionUrlsConfig +public sealed class SessionInstalledPlugin { - /// Initial list of allowed URL/domain patterns. Patterns may include path components. Ignored when `unrestricted` is true. - [JsonPropertyName("initialAllowed")] - public IList? InitialAllowed { get; set; } + /// Path where the plugin is cached locally. + [JsonPropertyName("cache_path")] + public string? CachePath { 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; } -} + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } -/// Patch of permission policy fields to apply (omit a field to leave it unchanged). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsConfigureParams -{ - /// 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; } + /// Installation timestamp (ISO-8601). + [JsonPropertyName("installed_at")] + public string InstalledAt { get; set; } = string.Empty; - /// If specified, sets whether path/URL read permission requests are auto-approved. Omit to leave the current value unchanged. - [JsonPropertyName("approveAllReadPermissionRequests")] - public bool? ApproveAllReadPermissionRequests { get; set; } + /// 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; } - /// 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; } + /// Marketplace the plugin came from (empty string for direct repo installs). + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; - /// 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; } + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. - [JsonPropertyName("rules")] - public PermissionRulesSet? Rules { get; set; } + /// Source descriptor for direct repo installs (when marketplace is empty). + [JsonPropertyName("source")] + public JsonElement? Source { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } - /// 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; } + /// Installed version, if known. + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Indicates whether the permission decision was applied; false when the request was already resolved. +/// Custom model-provider configuration (BYOK). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionRequestResult +public sealed class ProviderConfig { - /// Whether the permission request was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// API key. Optional for local providers like Ollama. + [JsonPropertyName("apiKey")] + public string? ApiKey { 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(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; -} + /// Azure-specific provider options. + [JsonPropertyName("azure")] + public ProviderConfigAzure? Azure { get; set; } + /// API endpoint URL. + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = string.Empty; -/// Permission-decision request variant to approve only the current permission request. -/// The approve-once variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveOnce : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "approve-once"; + /// Bearer token for authentication. Sets the Authorization header directly. Takes precedence over apiKey when both are set. + [JsonPropertyName("bearerToken")] + public string? BearerToken { 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; } -} + /// 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; } -/// 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(PermissionDecisionApproveForSessionApprovalFactory), "factory")] -[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; -} + /// 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; } -/// 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"; + /// Maximum output tokens for the model. + [JsonPropertyName("maxOutputTokens")] + public double? MaxOutputTokens { get; set; } - /// Command identifiers covered by this approval. - [JsonPropertyName("commandIdentifiers")] - public required IList CommandIdentifiers { get; set; } -} + /// Maximum prompt/input tokens for the model. + [JsonPropertyName("maxPromptTokens")] + public double? MaxPromptTokens { get; set; } -/// Session-scoped approval details for read-only filesystem operations. -/// The read variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalRead : PermissionDecisionApproveForSessionApproval -{ - /// - [JsonIgnore] - public override string Kind => "read"; -} + /// Overrides for model capabilities when they cannot be inferred from modelId. + [JsonPropertyName("modelCapabilities")] + public ModelCapabilitiesOverride? ModelCapabilities { 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"; -} + /// 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; } -/// 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"; + /// 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; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Provider name used for model and telemetry attribution. + [JsonPropertyName("providerName")] + public string? ProviderName { get; set; } - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { 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; } } -/// Session-scoped approval details for MCP sampling requests from a server. -/// The mcp-sampling variant of . +/// +/// 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 partial class PermissionDecisionApproveForSessionApprovalMcpSampling : PermissionDecisionApproveForSessionApproval +public sealed class ShellCredentials { - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; + /// + /// 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; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { 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; } } -/// Session-scoped approval details for writes to long-term memory. -/// The memory variant of . +/// 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 partial class PermissionDecisionApproveForSessionApprovalMemory : PermissionDecisionApproveForSessionApproval +public sealed class ShellInitScript { - /// - [JsonIgnore] - public override string Kind => "memory"; + /// 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; } } -/// Session-scoped approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Per-session settings for built-in shell tools. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalCustomTool : PermissionDecisionApproveForSessionApproval +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 Kind => "custom-tool"; + 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; } + + /// The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. + [JsonPropertyName("selectedAction")] + public UIExitPlanModeAction? SelectedAction { get; set; } +} + +/// Request ID of a pending `exit_plan_mode.requested` event and the user's response. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class UIHandlePendingExitPlanModeRequest +{ + /// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UIRegisterDirectAutoModeSwitchHandlerResult +{ + /// 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; +} + +/// Indicates whether the handle was active and the registration count was decremented. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UIUnregisterDirectAutoModeSwitchHandlerResult +{ + /// True if the handle was active and decremented the counter; false if the handle was unknown. + [JsonPropertyName("unregistered")] + public bool Unregistered { get; set; } +} + +/// Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; +} + +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsConfigureResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRuleSource +{ + /// Name of the policy source. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Type of the policy source. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; +} + +/// 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; } + + /// Conditions none of which may match. + [JsonPropertyName("ifNoneMatch")] + public IList? IfNoneMatch { get; set; } + + /// Path patterns covered by this rule. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } + + /// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. + [JsonPropertyName("source")] + public PermissionsConfigureAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } +} + +/// 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; } + + /// Content-exclusion rules to apply. + [JsonPropertyName("rules")] + public IList Rules { get => field ??= []; set; } + + /// Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. + [JsonPropertyName("scope")] + public PermissionsConfigureAdditionalContentExclusionPolicyScope Scope { 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionPathsConfig +{ + /// 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; } +} + +/// 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 PermissionRulesSet +{ + /// Rules that auto-approve matching requests. + [JsonPropertyName("approved")] + public IList Approved { get => field ??= []; set; } + + /// Rules that auto-deny matching requests. + [JsonPropertyName("denied")] + public IList Denied { get => field ??= []; set; } +} + +/// If specified, replaces the session's URL-permission policy. The runtime constructs a fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy unchanged. +[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; } + + /// If true, the runtime allows access to all URLs without prompting. Initial allow-list is ignored when this is true. + [JsonPropertyName("unrestricted")] + public bool? Unrestricted { get; set; } +} + +/// Patch of permission policy fields to apply (omit a field to leave it unchanged). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsConfigureParams +{ + /// 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; } + + /// 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; } + + /// If specified, replaces the session's path-permission policy. The runtime constructs the appropriate PathManager based on these inputs (rooted at the session's working directory). Omit to leave the current path policy unchanged. + [JsonPropertyName("paths")] + public PermissionPathsConfig? Paths { get; set; } + + /// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. + [JsonPropertyName("rules")] + public PermissionRulesSet? Rules { get; set; } + + /// 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; } +} + +/// 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; } +} + +/// 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; +} + + +/// Permission-decision request variant to approve only the current permission request. +/// The approve-once variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveOnce : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approve-once"; + + /// 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; } +} + +/// 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; +} + + +/// 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"; + + /// Command identifiers covered by this approval. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } +} + +/// Session-scoped approval details for read-only filesystem operations. +/// The read variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForSessionApprovalRead : PermissionDecisionApproveForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "read"; +} + +/// 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"; +} + +/// 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"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + +/// 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"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// Session-scoped approval details for writes to long-term memory. +/// The memory variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForSessionApprovalMemory : PermissionDecisionApproveForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "memory"; +} + +/// 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 PermissionDecisionApproveForSessionApprovalCustomTool : PermissionDecisionApproveForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "custom-tool"; + + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } +} + +/// 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 PermissionDecisionApproveForSessionApprovalExtensionManagement : PermissionDecisionApproveForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "extension-management"; + + /// Optional operation identifier; when omitted, the approval covers all extension management operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operation")] + public string? Operation { get; set; } +} + +/// Session-scoped workflow approval, optionally narrowed by approval key. +/// The workflow variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForSessionApprovalWorkflow : PermissionDecisionApproveForSessionApproval +{ + /// + [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; } +} + +/// Session-scoped approval details for an extension's permission-gated capability access, keyed by extension name. +/// The extension-permission-access variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess : PermissionDecisionApproveForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "extension-permission-access"; + + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } +} + +/// Session-scoped approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. +/// The extension-env-access variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForSessionApprovalExtensionEnvAccess : PermissionDecisionApproveForSessionApproval +{ + /// + [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 . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForSession : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approve-for-session"; + + /// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("approval")] + public PermissionDecisionApproveForSessionApproval? Approval { 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; } +} + +/// Approve file-tool read access to specific directories for this logical session, including continuation or resume. +/// The approve-read-only-for-session variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveReadOnlyForSession : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approve-read-only-for-session"; + + /// Canonical directories covered by the read-only grant. + [JsonPropertyName("directories")] + public required IList Directories { get; set; } +} + +/// 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; +} + + +/// 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"; + + /// Command identifiers covered by this approval. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } +} + +/// Location-scoped approval details for read-only filesystem operations. +/// The read variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalRead : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "read"; +} + +/// Location-scoped approval details for filesystem write operations. +/// The write variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalWrite : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "write"; +} + +/// Location-scoped approval details for an MCP server tool, or all tools on the server when `toolName` is null. +/// The mcp variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalMcp : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "mcp"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + +/// 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"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// Location-scoped approval details for writes to long-term memory. +/// The memory variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalMemory : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "memory"; +} + +/// Location-scoped approval details for a custom tool, keyed by tool name. +/// The custom-tool variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalCustomTool : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "custom-tool"; + + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } +} + +/// Location-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 PermissionDecisionApproveForLocationApprovalExtensionManagement : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "extension-management"; + + /// Optional operation identifier; when omitted, the approval covers all extension management operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operation")] + public string? Operation { 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"; + + /// 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; } +} + +/// Location-scoped approval details for an extension's permission-gated capability access, keyed by extension name. +/// The extension-permission-access variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "extension-permission-access"; + + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { 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"; + + /// 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 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"; + + /// 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; } +} + +/// 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"; + + /// URL domain to approve permanently. + [JsonPropertyName("domain")] + public required string Domain { 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"; + + /// Optional feedback explaining the rejection. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("feedback")] + public string? Feedback { 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"; +} + +/// 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"; +} + +/// Permission-decision variant indicating approval was remembered for the session, with approval details. +/// The approved-for-session variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApprovedForSession : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approved-for-session"; + + /// The approval to add as a session-scoped rule. + [JsonPropertyName("approval")] + public required UserToolSessionApproval Approval { 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 . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApprovedForLocation : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approved-for-location"; + + /// The approval to persist for this location. + [JsonPropertyName("approval")] + public required UserToolSessionApproval Approval { get; set; } + + /// The location key (git root or cwd) to persist the approval to. + [JsonPropertyName("locationKey")] + public required string LocationKey { get; set; } +} + +/// Permission-decision variant indicating the request was cancelled before use, with an optional reason. +/// The cancelled variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionCancelled : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "cancelled"; + + /// Optional explanation of why the request was cancelled. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reason")] + public string? Reason { get; set; } +} + +/// Permission-decision variant indicating explicit denial by permission rules, with the matching rules. +/// The denied-by-rules variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionDeniedByRules : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "denied-by-rules"; + + /// Rules that denied the request. + [JsonPropertyName("rules")] + public required IList Rules { get; set; } +} + +/// Permission-decision variant indicating no approval rule matched and user confirmation was unavailable. +/// The denied-no-approval-rule-and-could-not-request-from-user variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "denied-no-approval-rule-and-could-not-request-from-user"; +} + +/// Permission-decision variant indicating the user denied an interactive prompt, with optional feedback and force-reject flag. +/// The denied-interactively-by-user variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionDeniedInteractivelyByUser : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "denied-interactively-by-user"; + + /// Optional feedback from the user explaining the denial. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } + + /// Whether to force-reject the current agent turn. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("forceReject")] + public bool? ForceReject { get; set; } +} + +/// Permission-decision variant indicating denial by content-exclusion policy, with path and message. +/// The denied-by-content-exclusion-policy variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionDeniedByContentExclusionPolicy : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "denied-by-content-exclusion-policy"; + + /// Human-readable explanation of why the path was excluded. + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// File path that triggered the exclusion. + [JsonPropertyName("path")] + public required string Path { get; set; } +} + +/// Permission-decision variant indicating denial by a permission request hook, with optional message and interrupt flag. +/// The denied-by-permission-request-hook variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionDeniedByPermissionRequestHook : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "denied-by-permission-request-hook"; + + /// Whether to interrupt the current agent turn. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interrupt")] + public bool? Interrupt { get; set; } + + /// Optional message from the hook explaining the denial. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +/// Pending permission request ID and the decision to apply (approve/reject and scope). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionDecisionRequest +{ + /// Optional informational context describing how and where this response was made. Omit it to preserve legacy behavior without attributing an origin. + [JsonPropertyName("decisionContext")] + public PermissionDecisionContext? DecisionContext { get; set; } + + /// Request ID of the pending permission request. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// The client's response to the pending permission prompt. + [JsonPropertyName("result")] + public PermissionDecision Result { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Pending permission prompt reconstructed from event history, with request ID and user-facing prompt details. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; +} + +/// List of pending permission requests reconstructed from event history. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PendingPermissionRequestList +{ + /// Pending permission prompts reconstructed from the session's event history. Equivalent to the set of `permission.requested` events that have not yet been followed by a matching `permission.completed` event. Used by clients (e.g. the CLI) to hydrate UI for prompts that were emitted before the client attached to the session. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } +} + +/// No parameters; returns currently-pending permission requests for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsPendingRequestsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsSetApproveAllResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Allow-all toggle for tool permission requests, with an optional telemetry source. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsSetApproveAllRequest +{ + /// Whether to auto-approve all tool permission requests. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. + [JsonPropertyName("source")] + public PermissionsSetApproveAllSource? Source { get; set; } +} + +/// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsSetModeResult +{ + /// Authoritative permission mode after the mutation. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } + + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Permission mode to apply for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsSetModeRequest +{ + /// 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; } + + /// Permission mode to apply. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Optional source for permission-mode telemetry. `organization_targeting` is reserved for startup selection after the authenticated account matches an organization targeting policy; SDK callers default to `rpc` and cannot claim targeting provenance. + [JsonPropertyName("source")] + public PermissionModeSource? Source { get; set; } +} + +/// Current permission mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsGetModeResult +{ + /// Current permission mode. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } +} + +/// No parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsGetModeRequest +{ + /// 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 PermissionsModifyRulesResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Scope and add/remove instructions for modifying session- or location-scoped permission rules. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsModifyRulesParams +{ + /// Rules to add to the scope. Applied before `remove`/`removeAll`. + [JsonPropertyName("add")] + public IList? Add { get; set; } + + /// Specific rules to remove from the scope. Ignored when `removeAll` is true. + [JsonPropertyName("remove")] + public IList? Remove { get; set; } + + /// When true, removes every rule currently in the scope (after any `add` is applied). Useful for clearing the location scope wholesale. + [JsonPropertyName("removeAll")] + public bool? RemoveAll { get; set; } + + /// 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; } + + /// 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 PermissionsSetRequiredResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Toggles whether permission prompts should be bridged into session events for this client. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; +} + +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsResetSessionApprovalsResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsResetSessionApprovalsRequest +{ + /// Whether location-scoped approvals and exact session-approved paths are cleared too. Defaults to `true`. + [JsonPropertyName("includeLocation")] + public bool? IncludeLocation { get; set; } + + /// 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 PermissionsNotifyPromptShownResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Notification payload describing the permission prompt that the client just rendered. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionPromptShownNotification +{ + /// Human-readable description of the prompt the user is being asked to approve. Used by the runtime to fire the registered `permission_prompt` notification hook (e.g. terminal bell, desktop notification). + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Snapshot of the session's recursive directory grants, exact session-approved paths, and primary working directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionPathsList +{ + /// All directories currently allowed for tool access on this session. + [JsonPropertyName("directories")] + public IList Directories { get => field ??= []; set; } + + /// The primary working directory for this session. + [JsonPropertyName("primary")] + public string Primary { get; set; } = string.Empty; + + /// Exact paths approved for this session without recursively allowing their descendants. + [JsonPropertyName("sessionApprovedPaths")] + public IList? SessionApprovedPaths { get; set; } +} + +/// No parameters; returns the session's recursive directory grants and exact session-approved paths. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsPathsListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionPathsAddParams +{ + /// Directory to add to the allow-list. The runtime resolves and validates the path before adding, then loads conventional `.github/skills/` and `.github/agents/` definitions under it when their subsystem gates are enabled. Adding the directory is therefore also a trust decision for configuration stored there. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// 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 PermissionsPathsUpdatePrimaryResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Directory path to set as the session's new primary working directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionPathsUpdatePrimaryParams +{ + /// Directory to set as the new primary working directory for the session's permission policy. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Indicates whether the supplied path is within the session's allowed directories. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionPathsAllowedCheckResult +{ + /// Whether the path is within the session's allowed directories. + [JsonPropertyName("allowed")] + public bool Allowed { get; set; } +} + +/// Path to evaluate against the session's allowed directories. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionPathsAllowedCheckParams +{ + /// 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. +[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; } +} + +/// Path to evaluate against the session's workspace (primary) directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionPathsWorkspaceCheckParams +{ + /// 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. +[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 the location is a git repo or directory. + [JsonPropertyName("locationType")] + public PermissionLocationType LocationType { get; set; } +} + +/// Working directory to resolve into a location-permissions key. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionLocationResolveParams +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Working directory whose permission location should be resolved. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; +} + +/// Summary of persisted location permissions applied to the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionLocationApplyResult +{ + /// 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; } +} + +/// Working directory to load persisted location permissions for. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionLocationApplyParams +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Working directory whose persisted location permissions should be applied. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; +} + +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsLocationsAddToolApprovalResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { 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; +} + + +/// 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"; + + /// Command identifiers covered by this approval. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } +} + +/// 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"; +} + +/// Location-persisted tool approval details for filesystem write operations. +/// The write variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionsLocationsAddToolApprovalDetailsWrite : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "write"; +} + +/// Location-persisted tool approval details for an MCP server tool, or all tools when `toolName` is null. +/// The mcp variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionsLocationsAddToolApprovalDetailsMcp : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "mcp"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + +/// 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"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// Location-persisted tool approval details for writes to long-term memory. +/// The memory variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionsLocationsAddToolApprovalDetailsMemory : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "memory"; +} + +/// 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"; + + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } +} + +/// 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 PermissionsLocationsAddToolApprovalDetailsExtensionManagement : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "extension-management"; + + /// Optional operation identifier; when omitted, the approval covers all extension management operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operation")] + public string? Operation { get; set; } +} + +/// Location-persisted workflow approval, optionally narrowed by approval key. +/// The workflow variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionsLocationsAddToolApprovalDetailsWorkflow : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "workflow"; + + /// 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; } +} + +/// 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 partial class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "extension-permission-access"; + + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } +} + +/// Location-persisted tool approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. +/// The extension-env-access variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess : PermissionsLocationsAddToolApprovalDetails +{ + /// + [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; } +} + +/// 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; } + + /// 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; +} + +/// 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; } +} + +/// 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; + + /// 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 PermissionsFolderTrustAddTrustedResult +{ + /// 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; +} + +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsUrlsSetUnrestrictedModeResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Whether the URL-permission policy should run in unrestricted mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionUrlsSetUnrestrictedModeParams +{ + /// 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 repository the remote session targets. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataSnapshotRemoteMetadataRepository +{ + /// 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; +} + +/// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataSnapshotRemoteMetadata +{ + /// The pull request number the remote session is associated with, if any. + [JsonPropertyName("pullRequestNumber")] + public long? PullRequestNumber { get; set; } + + /// 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 +{ + /// 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; } + + /// 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; + + /// Display name for the session, if set. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Repository identifier in 'owner/repo' or 'org/project/repo' format, if any. + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// ISO 8601 timestamp when the workspace was last updated. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Whether the display name was explicitly set by the user. + [JsonPropertyName("user_named")] + public bool? UserNamed { get; set; } +} + +/// Point-in-time snapshot of slow-changing session identifier and state fields. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionMetadataSnapshot +{ + /// True when the session was detected to be in use by another process at construction time. Local consumers may surface a confirmation prompt before fully attaching. Always false for new sessions. + [JsonPropertyName("alreadyInUse")] + public bool AlreadyInUse { get; set; } + + /// Runtime client name associated with the session (telemetry identifier). + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot'). + [JsonPropertyName("currentMode")] + public MetadataSnapshotCurrentMode CurrentMode { get; set; } + + /// Live indexed-search state for this session activation. Omitted by runtimes that do not expose indexed-search status; absence does not indicate enablement. + [JsonPropertyName("indexedSearch")] + public IndexedSearchState? IndexedSearch { get; set; } + + /// User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. + [JsonPropertyName("initialName")] + public string? InitialName { get; 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; } + + /// ISO 8601 timestamp of when the session's persisted state was last modified on disk. For new sessions, equals startTime. For resumed sessions, reflects the previous modification time at construction. + [JsonPropertyName("modifiedTime")] + public DateTimeOffset ModifiedTime { get; set; } + + /// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. + [JsonPropertyName("remoteMetadata")] + public MetadataSnapshotRemoteMetadata? RemoteMetadata { get; set; } + + /// Currently selected model identifier, if any. + [JsonPropertyName("selectedModel")] + public string? SelectedModel { get; set; } + + /// The unique identifier of the session. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMetadataSnapshotRequest +{ + /// 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 SessionMetadataGetClientMetadataRequest +{ + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class MetadataUpdateClientMetadataRequest +{ + /// Remove every existing client metadata entry before applying remove and set. Defaults to false. + [JsonPropertyName("clear")] + public bool? Clear { get; set; } + + /// Case-sensitive keys to remove. Missing keys are ignored. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. + [JsonPropertyName("remove")] + public IList? Remove { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// String entries to add or replace. Set wins when a key also appears in remove. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. Each value may contain at most 16 KiB of UTF-8 data. + [JsonPropertyName("set")] + public IDictionary? Set { get; set; } +} + +/// Indicates whether the local session is currently processing a turn or background continuation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataIsProcessingResult +{ + /// 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 SessionMetadataIsProcessingRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Current activity flags for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionActivity +{ + /// Whether an in-flight operation can currently be aborted. + [JsonPropertyName("abortable")] + public bool Abortable { get; set; } + + /// Whether the session currently has active work, including running turns or tasks. + [JsonPropertyName("hasActiveWork")] + public bool HasActiveWork { get; set; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMetadataActivityRequest +{ + /// 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 reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). + [JsonPropertyName("bufferTokens")] + public long BufferTokens { get; set; } + + /// 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; } + + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. + [JsonPropertyName("limit")] + public long Limit { get; set; } + + /// 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; + + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. + [JsonPropertyName("promptTokenLimit")] + public long PromptTokenLimit { get; set; } + + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataContextInfoResult +{ + /// 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; } +} + +/// Model identifier and token limits used to compute the context-info breakdown. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class MetadataContextInfoRequest +{ + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. + [JsonPropertyName("outputTokenLimit")] + public long OutputTokenLimit { get; set; } + + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. + [JsonPropertyName("promptTokenLimit")] + public long PromptTokenLimit { get; set; } + + /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. + [JsonPropertyName("selectedModel")] + public string? SelectedModel { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// 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 +{ + /// Overlapping output reservation 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; } +} + +/// Successful compaction history for the session. +public sealed class MetadataContextAttributionResultContextAttributionCompactions +{ + /// Number of successful compactions in this session. + [JsonPropertyName("count")] + public long Count { get; set; } +} + +/// RPC data type for MetadataContextAttributionResultContextAttributionEntry operations. +public sealed class MetadataContextAttributionResultContextAttributionEntry +{ + /// Supplementary per-entry metadata (e.g. `messageCount`, `role`, `evictable`, `pluginSource`). Values are stringified; parse as needed and ignore unrecognized keys. + [JsonPropertyName("attributes")] + public IDictionary? Attributes { get; set; } + + /// Identifier for this entry, formed by joining its `kind` and source name (e.g. `tool:bash`, `skill:tmux`, `toolDefinition:bash`); unique within the snapshot. Use it to match the same entry across snapshots, to correlate with other APIs (skill/agent/MCP registries), and as the `parentId` target for nesting. Distinct from the human-facing `label`. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Source category for this entry. Not a closed set — tolerate unknown values. Known values today: `skill`, `subagent`, `mcpServer`, `tool`, `system`, `toolDefinition`, `plugin`. + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; + + /// Human-readable display label, e.g. `bash` or `skill: tmux`. Presentation-only; may be localized/reformatted without notice — do not key off it. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; + + /// Optional `id` of the parent entry: e.g. a `plugin` entry parenting its `skill`/`mcpServer` entries, or the `system` entry parenting `toolDefinition` entries. Omitted for top-level entries. + [JsonPropertyName("parentId")] + public string? ParentId { get; set; } + + /// Token count currently in context attributable to this entry. + [JsonPropertyName("tokens")] + public long Tokens { get; set; } +} + +/// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. +public sealed class MetadataContextAttributionResultContextAttribution +{ + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + [JsonPropertyName("bufferTokens")] + public long BufferTokens { get; set; } + + /// 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; } + + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. + [JsonPropertyName("limit")] + public long Limit { get; set; } + + /// 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; + + /// Effective input budget after reserving requested output against the combined context ceiling. 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; } +} + +/// Per-source attribution breakdown for the session's current context window, or null if uninitialized. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataContextAttributionResult +{ + /// 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; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMetadataGetContextAttributionRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// A single large message currently in context. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ContextHeaviestMessage +{ + /// 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; } +} + +/// The heaviest individual messages in the session's context window, most-expensive first. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataContextHeaviestMessagesResult +{ + /// Heaviest messages, most-expensive first. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; set; } + + /// Total token count of the current context window, so callers can compute each message's share without a second call. + [JsonPropertyName("totalTokens")] + public long TotalTokens { get; set; } +} + +/// Parameters for the heaviest-messages query. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class MetadataContextHeaviestMessagesRequest +{ + /// Maximum number of messages to return, most-expensive first. Omit for the server default. + [JsonPropertyName("limit")] + public long? Limit { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// 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 sealed class MetadataRecordContextChangeResult +{ +} + +/// Updated working-directory/git context to record on the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class MetadataRecordContextChangeRequest +{ + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + [JsonPropertyName("context")] + public SessionWorkingDirectoryContext Context { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataSetWorkingDirectoryResult +{ + /// Working directory after the update. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class MetadataSetWorkingDirectoryRequest +{ + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataRecomputeContextTokensResult +{ + /// 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; } +} + +/// Model identifier to use when re-tokenizing the session's existing messages. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class MetadataRecomputeContextTokensRequest +{ + /// Model identifier used for tokenization. The runtime token-counts both chat-context and system-context messages against this model. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Availability of built-in job tools surfaced to boundary consumers. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSettingsBuiltInToolAvailabilitySnapshot +{ + /// 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; } +} + +/// Redacted job settings for a session. The job nonce is excluded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSettingsJobSnapshot +{ + /// Availability of job-specific built-in tools. + [JsonPropertyName("builtInToolAvailability")] + public SessionSettingsBuiltInToolAvailabilitySnapshot? BuiltInToolAvailability { get; set; } + + /// GitHub Actions event type for the job. + [JsonPropertyName("eventType")] + public string? EventType { get; set; } + + /// Whether this is the workflow's trigger job. + [JsonPropertyName("isTriggerJob")] + public bool? IsTriggerJob { get; set; } +} + +/// Redacted model routing settings for a session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSettingsModelSnapshot +{ + /// Agent service callback URL for job and progress updates. + [JsonPropertyName("callbackUrl")] + public string? CallbackUrl { get; set; } + + /// Default reasoning effort for the selected model. + [JsonPropertyName("defaultReasoningEffort")] + public string? DefaultReasoningEffort { get; set; } + + /// Agent job identifier for the session. + [JsonPropertyName("instanceId")] + public string? InstanceId { get; set; } + + /// Selected model identifier. + [JsonPropertyName("model")] + public string? Model { get; set; } +} + +/// Online-evaluation settings safe to expose across the SDK boundary. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSettingsOnlineEvaluationSnapshot +{ + /// Whether online evaluation is disabled. + [JsonPropertyName("disableOnlineEvaluation")] + public bool? DisableOnlineEvaluation { get; set; } + + /// Whether online-evaluation output-file generation is enabled. + [JsonPropertyName("enableOnlineEvaluationOutputFile")] + public bool? EnableOnlineEvaluationOutputFile { get; set; } +} + +/// Redacted repository and GitHub host settings for a session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSettingsRepoSnapshot +{ + /// Checked-out repository branch. + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + /// Checked-out commit SHA. + [JsonPropertyName("commit")] + public string? Commit { get; set; } + + /// GitHub server host name. + [JsonPropertyName("host")] + public string? Host { get; set; } + + /// Protocol used to access the GitHub host. + [JsonPropertyName("hostProtocol")] + public string? HostProtocol { get; set; } + + /// GitHub repository database ID. + [JsonPropertyName("id")] + public double? Id { get; set; } + + /// Repository name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// GitHub repository owner database ID. + [JsonPropertyName("ownerId")] + public double? OwnerId { get; set; } + + /// Repository owner login. + [JsonPropertyName("ownerName")] + public string? OwnerName { get; set; } + + /// Number of commits in the pull request. + [JsonPropertyName("prCommitCount")] + public double? PrCommitCount { get; set; } + + /// Whether the repository is writable. + [JsonPropertyName("readWrite")] + public bool? ReadWrite { get; set; } + + /// GitHub secret-scanning service URL. + [JsonPropertyName("secretScanningUrl")] + public string? SecretScanningUrl { get; set; } + + /// GitHub server base URL. + [JsonPropertyName("serverUrl")] + public string? ServerUrl { get; set; } +} + +/// 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; } + + /// 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; } +} + +/// 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)] +internal sealed class SessionSettingsSnapshot +{ + /// Name of the SDK client that created the session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// Redacted job settings. + [JsonPropertyName("job")] + public SessionSettingsJobSnapshot Job { get => field ??= new(); set; } + + /// Redacted model routing settings. + [JsonPropertyName("model")] + public SessionSettingsModelSnapshot Model { get => field ??= new(); set; } + + /// Online-evaluation settings safe for SDK consumers. + [JsonPropertyName("onlineEvaluation")] + public SessionSettingsOnlineEvaluationSnapshot OnlineEvaluation { get => field ??= new(); set; } + + /// Redacted repository and host settings. + [JsonPropertyName("repo")] + public SessionSettingsRepoSnapshot Repo { get => field ??= new(); set; } + + /// Session start time as Unix epoch milliseconds. + [JsonPropertyName("startTimeMs")] + public double? StartTimeMs { get; set; } + + /// Session timeout in milliseconds. + [JsonPropertyName("timeoutMs")] + public double? TimeoutMs { get; set; } + + /// Redacted validation and memory-tool settings. + [JsonPropertyName("validation")] + public SessionSettingsValidationSnapshot Validation { get => field ??= new(); set; } + + /// Agent runtime version selector copied from the session settings, such as `latest` or a runtime release identifier. + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionSettingsSnapshotRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Result of evaluating a Rust-owned settings predicate. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionSettingsEvaluatePredicateResult +{ + /// Whether the named settings predicate evaluated to enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } +} + +/// Named Rust-owned settings predicate to evaluate for this session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionSettingsEvaluatePredicateRequest +{ + /// Predicate name. The runtime owns the raw feature-flag names and composition logic. + [JsonPropertyName("name")] + public SessionSettingsPredicateName Name { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Tool name for tool-scoped predicates such as trivial-change handling. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + +/// Content-exclusion decision for one requested path. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ContentExclusionPathCheck +{ + /// Whether the session's complete content-exclusion policy excludes the path. + [JsonPropertyName("excluded")] + public bool Excluded { get; set; } + + /// The path supplied by the caller. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} + +/// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ContentExclusionCheckPathsResult +{ + /// Whether the session's policy service was available for the complete batch. When false, checks is empty and callers must treat every requested path as excluded. + [JsonPropertyName("available")] + public bool Available { get; set; } + + /// Per-path decisions in request order. Empty when available is false. + [JsonPropertyName("checks")] + public IList Checks { get => field ??= []; set; } +} + +/// 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 ContentExclusionCheckPathsRequest +{ + /// Local file system absolute paths within the session working directory to check. Results are returned in the same order, including duplicates. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Identifier of the spawned shell process, usable with shell.kill while the process is running. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ShellExecResult +{ + /// Identifier usable with shell.kill while the process is running. + [JsonPropertyName("processId")] + public string ProcessId { get; set; } = string.Empty; +} + +/// Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ShellExecRequest +{ + /// Shell command to execute. + [JsonPropertyName("command")] + public string Command { get; set; } = string.Empty; + + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ShellKillResult +{ + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ShellKillRequest +{ + /// Process identifier returned by shell.exec. + [JsonPropertyName("processId")] + public string ProcessId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Signal to send (default: SIGTERM). + [JsonPropertyName("signal")] + public ShellKillSignal? Signal { get; set; } +} + +/// Result of a user-requested shell command. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UserRequestedShellCommandResult +{ + /// Error output when the execution failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Process exit code, when available. + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Captured command output. + [JsonPropertyName("output")] + public string Output { get; set; } = string.Empty; + + /// Whether the command completed successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } + + /// Tool call id emitted for the shell execution. + [JsonPropertyName("toolCallId")] + public string ToolCallId { get; set; } = string.Empty; +} + +/// User-requested shell command and cancellation handle. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ShellExecuteUserRequestedRequest +{ + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CancelUserRequestedShellCommandResult +{ + /// Whether an in-flight execution was found and signalled to cancel. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} + +/// User-requested shell execution cancellation handle. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; +} + +/// Post-compaction context window usage breakdown. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HistoryCompactContextWindow +{ + /// 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; } - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { 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; } } -/// Session-scoped approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// 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 partial class PermissionDecisionApproveForSessionApprovalExtensionManagement : PermissionDecisionApproveForSessionApproval +public sealed class HistoryCompactResult { - /// - [JsonIgnore] - public override string Kind => "extension-management"; + /// Post-compaction context window usage breakdown. + [JsonPropertyName("contextWindow")] + public HistoryCompactContextWindow? ContextWindow { 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; } + /// Number of messages removed during compaction. + [JsonPropertyName("messagesRemoved")] + public long MessagesRemoved { get; set; } + + /// Whether compaction completed successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } + + /// Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). + [JsonPropertyName("summaryContent")] + public string? SummaryContent { get; set; } + + /// Number of tokens freed by compaction. + [JsonPropertyName("tokensRemoved")] + public long TokensRemoved { get; set; } } -/// Session-scoped factory approval, optionally narrowed by approval key. -/// The factory variant of . +/// RPC data type for SessionHistoryCompact operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalFactory : PermissionDecisionApproveForSessionApproval +public sealed class SessionHistoryCompactRequest { - /// - [JsonIgnore] - public override string Kind => "factory"; + /// 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; } - /// Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approvalKey")] - public string? ApprovalKey { 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; } } -/// Session-scoped approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . +/// RPC data type for SessionHistoryCompactRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess : PermissionDecisionApproveForSessionApproval +internal sealed class SessionHistoryCompactRequestWithSession { - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// 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; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { 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; } } -/// 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 . +/// Number of events that were removed by the truncation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionEnvAccess : PermissionDecisionApproveForSessionApproval +public sealed class HistoryTruncateResult { - /// - [JsonIgnore] - public override string Kind => "extension-env-access"; + /// Failure detail when checkpointCleanupFailed is true. + [JsonPropertyName("checkpointCleanupError")] + public string? CheckpointCleanupError { get; set; } - /// Names of the sensitive environment variables this approval covers. Values are never persisted. - [JsonPropertyName("environmentVariables")] - public required IList EnvironmentVariables { 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; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } + /// Number of events that were removed. + [JsonPropertyName("eventsRemoved")] + public long EventsRemoved { 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 . +/// Identifier of the event to truncate to; this event and all later events are removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSession : PermissionDecision +internal sealed class HistoryTruncateRequest { - /// - [JsonIgnore] - public override string Kind => "approve-for-session"; - - /// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approval")] - public PermissionDecisionApproveForSessionApproval? Approval { 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; - /// URL domain to approve for the rest of the session (URL prompts only). - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("domain")] - public string? Domain { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Approval to persist for this location. -/// Polymorphic base type discriminated by kind. +/// A root user turn that the session can rewind to. [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(PermissionDecisionApproveForLocationApprovalFactory), "factory")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess), "extension-permission-access")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionEnvAccess), "extension-env-access")] -public partial class PermissionDecisionApproveForLocationApproval +public sealed class HistoryRewindPoint { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// 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; -/// Location-scoped approval details for specific command identifiers. -/// The commands variant of . + /// 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; +} + +/// Rewind points and file-change-tracking availability for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalCommands : PermissionDecisionApproveForLocationApproval +public sealed class HistoryListRewindPointsResult { - /// - [JsonIgnore] - public override string Kind => "commands"; + /// Whether this session captured file changes from its first turn. + [JsonPropertyName("fileChangeTrackingEnabled")] + public bool FileChangeTrackingEnabled { get; set; } - /// Command identifiers covered by this approval. - [JsonPropertyName("commandIdentifiers")] - public required IList CommandIdentifiers { 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; } } -/// Location-scoped approval details for read-only filesystem operations. -/// The read variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalRead : PermissionDecisionApproveForLocationApproval +internal sealed class SessionHistoryListRewindPointsRequest { - /// - [JsonIgnore] - public override string Kind => "read"; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-scoped approval details for filesystem write operations. -/// The write variant of . +/// A file that a conversation-and-files rewind would restore. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalWrite : PermissionDecisionApproveForLocationApproval +public sealed class HistoryRewindFilePreview { - /// - [JsonIgnore] - public override string Kind => "write"; + /// 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; } -/// Location-scoped approval details for an MCP server tool, or all tools on the server when `toolName` is null. -/// The mcp variant of . +/// Files and aggregate changes for a prospective rewind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMcp : PermissionDecisionApproveForLocationApproval +public sealed class HistoryPreviewRewindResult { - /// - [JsonIgnore] - public override string Kind => "mcp"; + /// 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; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Number of unique files in the preview. + [JsonPropertyName("fileCount")] + public long FileCount { get; set; } - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { 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; } } -/// Location-scoped approval details for MCP sampling requests from a server. -/// The mcp-sampling variant of . +/// Event boundary to preview for conversation-and-files rewind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMcpSampling : PermissionDecisionApproveForLocationApproval +internal sealed class HistoryPreviewRewindRequest { - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; + /// ID of the user.message event that begins the discarded suffix. + [JsonPropertyName("eventId")] + public string EventId { 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 . +/// A captured file that rewind intentionally left unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMemory : PermissionDecisionApproveForLocationApproval +public sealed class HistorySkippedFileRestore { - /// - [JsonIgnore] - public override string Kind => "memory"; + /// 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; } } -/// Location-scoped approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Structured outcome of a rewind request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalCustomTool : PermissionDecisionApproveForLocationApproval +public sealed class HistoryRewindResult { - /// - [JsonIgnore] - public override string Kind => "custom-tool"; + /// 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; } - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { 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; } } -/// Location-scoped approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// Boundary and mode for rewinding session history. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionManagement : PermissionDecisionApproveForLocationApproval +internal sealed class HistoryRewindRequest { - /// - [JsonIgnore] - public override string Kind => "extension-management"; + /// ID of the user.message event that begins the discarded suffix. + [JsonPropertyName("eventId")] + public string EventId { get; set; } = string.Empty; - /// Optional operation identifier; when omitted, the approval covers all extension management operations. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("operation")] - public string? Operation { get; set; } + /// Whether 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; } -/// Location-scoped factory approval, optionally narrowed by approval key. -/// The factory variant of . +/// Indicates whether an in-progress background compaction was cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalFactory : PermissionDecisionApproveForLocationApproval +public sealed class HistoryCancelBackgroundCompactionResult { - /// - [JsonIgnore] - public override string Kind => "factory"; - - /// Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approvalKey")] - public string? ApprovalKey { 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; } } -/// Location-scoped approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess : PermissionDecisionApproveForLocationApproval +internal sealed class SessionHistoryCancelBackgroundCompactionRequest { - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } +/// Indicates whether an in-progress manual compaction was aborted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HistoryAbortManualCompactionResult +{ + /// Whether an in-progress manual compaction was aborted. False when no manual compaction was running, when its abort controller was already aborted, or when the session is remote. + [JsonPropertyName("aborted")] + public bool Aborted { get; set; } } -/// 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 . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionEnvAccess : PermissionDecisionApproveForLocationApproval +internal sealed class SessionHistoryAbortManualCompactionRequest { - /// - [JsonIgnore] - public override string Kind => "extension-env-access"; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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; } +/// Markdown summary of the conversation context (empty when not available). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HistorySummarizeForHandoffResult +{ + /// 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; +} - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionHistorySummarizeForHandoffRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision request variant to approve and persist a permission for a project location, with approval details and location key. -/// The approve-for-location variant of . +/// 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 partial class PermissionDecisionApproveForLocation : PermissionDecision +public sealed class HistoryClearContextResult { - /// - [JsonIgnore] - public override string Kind => "approve-for-location"; + /// 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; } +} - /// Approval to persist for this location. - [JsonPropertyName("approval")] - public required PermissionDecisionApproveForLocationApproval Approval { get; set; } +/// Parameters for clearing the conversation and seeding the window that replaces it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HistoryClearContextRequest +{ + /// First user message of the fresh context window. Required: a cleared window holding only system and developer messages is not a conversation a model can answer, so every clear seeds the window it creates. Delivered by the enclosing turn driver once the agentic loop exits, which is why the call must be made from inside a tool handler. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public required string LocationKey { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision request variant to permanently approve a URL domain across sessions. -/// The approve-permanently variant of . +/// 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 partial class PermissionDecisionApprovePermanently : PermissionDecision +public sealed class QueuePendingItems { - /// - [JsonIgnore] - public override string Kind => "approve-permanently"; + /// 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; } - /// URL domain to approve permanently. - [JsonPropertyName("domain")] - public required string Domain { 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; } } -/// Permission-decision request variant to reject a pending permission request, with optional feedback. -/// The reject variant of . +/// Snapshot of the session's pending queued items and immediate-steering messages. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionReject : PermissionDecision +public sealed class QueuePendingItemsResult { - /// - [JsonIgnore] - public override string Kind => "reject"; + /// 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; } - /// Optional feedback explaining the rejection. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("feedback")] - public string? Feedback { 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; } } -/// Permission-decision variant indicating no user was available to confirm the request. -/// The user-not-available variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionUserNotAvailable : PermissionDecision +internal sealed class SessionQueuePendingItemsRequest { - /// - [JsonIgnore] - public override string Kind => "user-not-available"; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision variant indicating the request was approved. -/// The approved variant of . +/// Internal snapshot of native queue state for local session orchestration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproved : PermissionDecision +internal sealed class QueueSnapshotResult { - /// - [JsonIgnore] - public override string Kind => "approved"; -} + /// Queue item identifier of a model switch that has been dequeued but not yet applied. + [JsonPropertyName("inFlightModelChangeId")] + public string? InFlightModelChangeId { get; set; } -/// Permission-decision variant indicating approval was remembered for the session, with approval details. -/// The approved-for-session variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApprovedForSession : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "approved-for-session"; + /// Insertion orders for queued items, aligned with `items`. + [JsonPropertyName("itemOrders")] + public IList? ItemOrders { get; set; } - /// The approval to add as a session-scoped rule. - [JsonPropertyName("approval")] - public required UserToolSessionApproval Approval { get; set; } + /// User-facing pending items in FIFO order. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } + + /// Insertion orders for immediate steering messages, aligned with `steeringMessages`. + [JsonPropertyName("steeringMessageOrders")] + public IList? SteeringMessageOrders { get; set; } + + /// Immediate steering messages waiting for an active turn. + [JsonPropertyName("steeringMessages")] + public IList SteeringMessages { get => field ??= []; set; } } -/// Permission-decision variant indicating approval was persisted for a project location, with approval details and location key. -/// The approved-for-location variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApprovedForLocation : PermissionDecision +internal sealed class SessionQueueSnapshotRequest { - /// - [JsonIgnore] - public override string Kind => "approved-for-location"; - - /// The approval to persist for this location. - [JsonPropertyName("approval")] - public required UserToolSessionApproval Approval { get; set; } - - /// The location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public required string LocationKey { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision variant indicating the request was cancelled before use, with an optional reason. -/// The cancelled variant of . +/// Result of moving a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionCancelled : PermissionDecision +public sealed class QueueMoveItemResult { - /// - [JsonIgnore] - public override string Kind => "cancelled"; - - /// Optional explanation of why the request was cancelled. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// True when the item changed position; false when it was already at the requested position. + [JsonPropertyName("changed")] + public bool Changed { get; set; } } -/// Permission-decision variant indicating explicit denial by permission rules, with the matching rules. -/// The denied-by-rules variant of . +/// Parameters for moving a queued item by stable id. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByRules : PermissionDecision +internal sealed class QueueMoveItemRequest { - /// - [JsonIgnore] - public override string Kind => "denied-by-rules"; + /// Stable opaque queued-item id. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// 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; + + /// Zero-based target position in the public visible queue. Values outside the queue clamp to an end. + [JsonPropertyName("toPosition")] + public long ToPosition { get; set; } } -/// Permission-decision variant indicating no approval rule matched and user confirmation was unavailable. -/// The denied-no-approval-rule-and-could-not-request-from-user variant of . +/// Result of inserting a queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser : PermissionDecision +public sealed class QueueInsertAtResult { - /// - [JsonIgnore] - public override string Kind => "denied-no-approval-rule-and-could-not-request-from-user"; + /// Fresh stable opaque id assigned to the inserted item. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; } -/// Permission-decision variant indicating the user denied an interactive prompt, with optional feedback and force-reject flag. -/// The denied-interactively-by-user variant of . +/// Serializable message fields accepted by queue.insertAt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedInteractivelyByUser : PermissionDecision +public sealed class QueueInsertMessage { - /// - [JsonIgnore] - public override string Kind => "denied-interactively-by-user"; + /// Optional explicit agent mode. When omitted, the session's current mode is assigned. + [JsonPropertyName("agentMode")] + public SendAgentMode? AgentMode { get; set; } - /// Optional feedback from the user explaining the denial. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } + /// Optional attachments for the message. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } - /// Whether to force-reject the current agent turn. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("forceReject")] - public bool? ForceReject { get; set; } -} + /// Whether the message is billable. + [JsonPropertyName("billable")] + public bool? Billable { get; set; } -/// Permission-decision variant indicating denial by content-exclusion policy, with path and message. -/// The denied-by-content-exclusion-policy variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByContentExclusionPolicy : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "denied-by-content-exclusion-policy"; + /// Accepted for internal SendOptions compatibility but ignored; delivery is derived from current session activity. + [JsonPropertyName("delivery")] + public string? Delivery { get; set; } - /// Human-readable explanation of why the path was excluded. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Optional user-facing display text. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } - /// File path that triggered the exclusion. - [JsonPropertyName("path")] - public required string Path { get; set; } -} + /// Accepted for SendOptions compatibility but ignored; inserted items always use queued delivery semantics. + [JsonPropertyName("mode")] + public SendMode? Mode { get; set; } -/// Permission-decision variant indicating denial by a permission request hook, with optional message and interrupt flag. -/// The denied-by-permission-request-hook variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByPermissionRequestHook : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "denied-by-permission-request-hook"; + /// Accepted for SendOptions compatibility but ignored; the requested public position controls placement. + [JsonPropertyName("prepend")] + public bool? Prepend { get; set; } - /// Whether to interrupt the current agent turn. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("interrupt")] - public bool? Interrupt { get; set; } + /// The user message text. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Optional message from the hook explaining the denial. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Per-turn request headers. + [JsonPropertyName("requestHeaders")] + public IDictionary? RequestHeaders { get; set; } + + /// Required tool name for the turn, when any. + [JsonPropertyName("requiredTool")] + public string? RequiredTool { get; set; } + + /// Optional provenance source. `system` is rejected: it would hide the inserted row from `pendingItems` and make it unaddressable while still executing, so inserted items must stay visible. + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// Accepted for SendOptions compatibility but ignored; insertion scheduling is controlled by the queue drain state. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } } -/// Pending permission request ID and the decision to apply (approve/reject and scope). +/// Parameters for inserting a queued message at a public visible position. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionDecisionRequest +internal sealed class QueueInsertAtRequest { - /// 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; + /// Queued message contents and delivery metadata. + [JsonPropertyName("message")] + public QueueInsertMessage Message { get => field ??= new(); set; } - /// The client's response to the pending permission prompt. - [JsonPropertyName("result")] - public PermissionDecision Result { 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; } -/// Pending permission prompt reconstructed from event history, with request ID and user-facing prompt details. +/// Result of removing a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PendingPermissionRequest +public sealed class QueueRemoveAtResult { - /// Permission-recovery episode that authorized this request to surface for interactive attention. - [JsonPropertyName("recoveryEpisodeId")] - public string? RecoveryEpisodeId { get; set; } - - /// The user-facing permission prompt details (commands, write, read, mcp, url, memory, custom-tool, path, hook). - [JsonPropertyName("request")] - public PermissionPromptRequest Request { get; set; } = null!; - - /// Unique identifier for the pending permission request. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// True when the addressed item was removed. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// List of pending permission requests reconstructed from event history. +/// Parameters for removing a queued item by stable id. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PendingPermissionRequestList +internal sealed class QueueRemoveAtRequest { - /// 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; } -} + /// Stable opaque ID of the queued item to remove. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; -/// 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; } -/// Indicates whether the operation succeeded. +/// Result of editing a queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetApproveAllResult +public sealed class QueueUpdateTextResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// True when the stored text changed. + [JsonPropertyName("updated")] + public bool Updated { get; set; } } -/// Allow-all toggle for tool permission requests, with an optional telemetry source. +/// Parameters for editing a single queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetApproveAllRequest +internal sealed class QueueUpdateTextRequest { - /// Whether to auto-approve all tool permission requests. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Optional replacement prompt displayed to the user. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Stable opaque ID of the queued item to edit. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Replacement prompt sent to the model. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. - [JsonPropertyName("source")] - public PermissionsSetApproveAllSource? Source { get; set; } } -/// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. +/// Result of withdrawing a user message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetModeResult +public sealed class QueueWithdrawMessageResult { - /// Authoritative permission mode after the mutation. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } + /// True when the running turn was interrupted to withdraw the message. With removed false, the turn was interrupted but its events could not be removed, for example because the model answered first, so the message stays in the interrupted turn. + [JsonPropertyName("interrupted")] + public bool Interrupted { get; set; } - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// True when the message left the queue or, for a running turn, history. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// Permission mode to apply for the session. +/// Conditional withdrawal of a single user message, from its queue or from the running turn it started. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetModeRequest +internal sealed class QueueWithdrawMessageRequest { - /// Optional judge model id for assisted mode. When omitted, the session resolves the provider default: `gpt-5.5` for CAPI sessions and the active session model for BYOK sessions. - [JsonPropertyName("assistedApprovalModel")] - public string? AssistedApprovalModel { get; set; } + /// The 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; - /// Permission mode to apply. - [JsonPropertyName("mode")] - public PermissionMode Mode { 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; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. - [JsonPropertyName("source")] - public PermissionModeSource? Source { get; set; } } -/// Current permission mode. +/// Append to one pending steering message without changing its identity or delivery position. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsGetModeResult +internal sealed class QueueAppendSteeringRequest { - /// Current permission mode. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } -} + /// Mode captured at submission. Only steering messages in the same mode may be combined. + [JsonPropertyName("agentMode")] + public SendAgentMode AgentMode { get; set; } + + /// Attachments to add after the message's existing attachments. An empty list preserves the existing attachments. + [JsonPropertyName("attachments")] + public IList Attachments { get => field ??= []; set; } + + /// Display text to append to the existing preview after a blank line. + [JsonPropertyName("displayPrompt")] + public string DisplayPrompt { get; set; } = string.Empty; + + /// Expected current prompt, including any previous appends. The runtime applies plan-mode normalization before comparing and refuses a changed message. + [JsonPropertyName("expectedPrompt")] + public string ExpectedPrompt { get; set; } = string.Empty; + + /// Message identity returned by send, not the queue item id. Only unclaimed user steering messages are eligible. + [JsonPropertyName("messageId")] + public string MessageId { get; set; } = string.Empty; + + /// Text to append after a blank line. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; -/// No parameters. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsGetModeRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// Result of duplicating a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsModifyRulesResult +public sealed class QueueDuplicateAtResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Fresh stable opaque id assigned to the duplicate. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; } -/// Scope and add/remove instructions for modifying session- or location-scoped permission rules. +/// Parameters for duplicating a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsModifyRulesParams +internal sealed class QueueDuplicateAtRequest { - /// Rules to add to the scope. Applied before `remove`/`removeAll`. - [JsonPropertyName("add")] - public IList? Add { get; set; } - - /// Specific rules to remove from the scope. Ignored when `removeAll` is true. - [JsonPropertyName("remove")] - public IList? Remove { get; set; } - - /// When true, removes every rule currently in the scope (after any `add` is applied). Useful for clearing the location scope wholesale. - [JsonPropertyName("removeAll")] - public bool? RemoveAll { get; set; } - - /// 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; } + /// 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; } -/// Indicates whether the operation succeeded. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetRequiredResult -{ - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} - -/// Toggles whether permission prompts should be bridged into session events for this client. +/// 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 PermissionsSetRequiredRequest +internal sealed class QueueSetDrainPausedRequest { - /// 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; } + /// 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; } -/// Indicates whether the operation succeeded. +/// Result of trying to steer a queued message into a live turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsResetSessionApprovalsResult +public sealed class QueueSendNowResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { 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; } } -/// Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. +/// Parameters for steering a queued message into a live turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsResetSessionApprovalsRequest +internal sealed class QueueSendNowRequest { - /// Whether location-scoped approvals and exact session-approved paths are cleared too. Defaults to `true`. - [JsonPropertyName("includeLocation")] - public bool? IncludeLocation { get; set; } + /// 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; } -/// Indicates whether the operation succeeded. +/// Whether the native queue has pending work. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsNotifyPromptShownResult +internal sealed class QueueHasPendingResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// True when queued or immediate native work is pending. + [JsonPropertyName("hasPending")] + public bool HasPending { get; set; } } -/// Notification payload describing the permission prompt that the client just rendered. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPromptShownNotification +internal sealed class SessionQueueHasPendingRequest { - /// Human-readable description of the prompt the user is being asked to approve. Used by the runtime to fire the registered `permission_prompt` notification hook (e.g. terminal bell, desktop notification). - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Snapshot of the session's recursive directory grants, exact session-approved paths, and primary working directory. +/// Whether a deferred-idle drain should run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsList +internal sealed class QueueBeginDeferredIdleDrainResult { - /// All directories currently allowed for tool access on this session. - [JsonPropertyName("directories")] - public IList Directories { get => field ??= []; set; } - - /// The primary working directory for this session. - [JsonPropertyName("primary")] - public string Primary { get; set; } = string.Empty; - - /// Exact paths approved for this session without recursively allowing their descendants. - [JsonPropertyName("sessionApprovedPaths")] - public IList? SessionApprovedPaths { get; set; } + /// True when the host should run finishDeferredIdleDrain asynchronously. + [JsonPropertyName("shouldDrain")] + public bool ShouldDrain { get; set; } } -/// No parameters; returns the session's recursive directory grants and exact session-approved paths. +/// Inputs for starting a deferred-idle drain. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsPathsListRequest +internal sealed class QueueBeginDeferredIdleDrainRequest { + /// 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; } -/// Indicates whether the operation succeeded. +/// Action selected by the native deferred-idle drain. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsPathsAddResult +internal sealed class QueueFinishDeferredIdleDrainResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Whether the deferred idle was caused by an aborted foreground turn. + [JsonPropertyName("aborted")] + public bool Aborted { get; set; } + + /// One of none, processQueue, or emitSessionIdle. + [JsonPropertyName("action")] + public string Action { get; set; } = string.Empty; } -/// Directory path to add to the session's allowed directories. +/// Inputs for completing a deferred-idle drain. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsAddParams +internal sealed class QueueFinishDeferredIdleDrainRequest { - /// 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; + /// Whether the host still has active background work. + [JsonPropertyName("activeBackgroundWork")] + public bool ActiveBackgroundWork { get; set; } + + /// Whether native queued work remains. + [JsonPropertyName("hasPending")] + public bool HasPending { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsPathsUpdatePrimaryResult -{ - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} - -/// Directory path to set as the session's new primary working directory. +/// Inputs for marking session.idle deferred in native state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsUpdatePrimaryParams +internal sealed class QueueDeferSessionIdleRequest { - /// 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 the deferred idle was caused by an aborted foreground turn. + [JsonPropertyName("aborted")] + public bool Aborted { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the supplied path is within the session's allowed directories. +/// Indicates whether a user-facing pending item was removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsAllowedCheckResult +public sealed class QueueRemoveMostRecentResult { - /// Whether the path is within the session's allowed directories. - [JsonPropertyName("allowed")] - public bool Allowed { 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; } } -/// 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 SessionQueueRemoveMostRecentRequest { - /// 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. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsWorkspaceCheckResult +internal sealed class SessionQueueClearRequest { - /// Whether the path is within the session workspace directory. - [JsonPropertyName("allowed")] - public bool Allowed { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Path to evaluate against the session's workspace (primary) directory. +/// Internal filter for consuming queued system notifications. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsWorkspaceCheckParams +internal sealed class QueueConsumeSystemNotificationsRequest { - /// Path to check against the session workspace directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Opaque runtime-owned filter object. + [JsonPropertyName("filter")] + public JsonElement Filter { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Resolved location-permissions key and type. +/// Result of enqueueing the resume-pending wake item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionLocationResolveResult +internal sealed class QueueEnqueueResumePendingResult { - /// 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; } + /// True when a wake item was newly queued. + [JsonPropertyName("queued")] + public bool Queued { get; set; } } -/// 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 SessionQueueEnqueueResumePendingRequest { /// 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; +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionQueueProcessRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Summary of persisted location permissions applied to the session. +/// Cursor, batch size, and optional long-poll/filter parameters for reading session events. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionLocationApplyResult +internal sealed class EventLogReadRequest { - /// Number of persisted allowed directories added to the live path manager. - [JsonPropertyName("appliedDirectoryCount")] - public long AppliedDirectoryCount { 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; } - /// Number of location-scoped rules added to the live permission service. - [JsonPropertyName("appliedRuleCount")] - public long AppliedRuleCount { 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; } - /// Location-scoped rules applied to the live permission service. - [JsonPropertyName("appliedRules")] - public IList AppliedRules { get => field ??= []; set; } + /// Opaque cursor returned by a previous read. Omit on the first call to start from the beginning of the session's persisted history. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Whether a different location was applied since the previous apply call. - [JsonPropertyName("changed")] - public bool Changed { 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; } - /// Location key used in the location-permissions store. - [JsonPropertyName("locationKey")] - public string LocationKey { get; set; } = string.Empty; + /// When false, skip ephemeral events entirely and return only durable (persisted) events. History-backfill callers that discard ephemerals anyway should set this so the read is bounded by the durable log length instead of racing the ephemeral ring on a busy session. Defaults to true (ephemerals are interleaved with durable events in creation order). Ignored by backward reads, which always cover persisted history only. + [JsonPropertyName("includeEphemeral")] + public bool? IncludeEphemeral { get; set; } - /// Whether the location is a git repo or directory. - [JsonPropertyName("locationType")] - public PermissionLocationType LocationType { get; set; } -} + /// Maximum number of events to return in this batch (1–1000, default 200). + [JsonPropertyName("max")] + public long? Max { get; set; } -/// Working directory to load persisted location permissions for. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationApplyParams -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Working directory whose persisted location permissions should be applied. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; + /// 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; } } -/// Indicates whether the operation succeeded. +/// Snapshot of the current tail cursor without returning any events. Use this when a consumer wants to subscribe to live events going forward without first paginating through the entire persisted history (which would happen if `read` were called without a cursor on a long-lived session). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsLocationsAddToolApprovalResult +public sealed class EventLogTailResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; 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; } -/// Tool approval to persist and apply. -/// Polymorphic base type discriminated by kind. +/// Identifies the target session. [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(PermissionsLocationsAddToolApprovalDetailsFactory), "factory")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess), "extension-permission-access")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess), "extension-env-access")] -public partial class PermissionsLocationsAddToolApprovalDetails +internal sealed class SessionEventLogTailRequest { - /// 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; } - -/// Location-persisted tool approval details for specific command identifiers. -/// The commands variant of . +/// Opaque handle representing an event-type interest registration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsCommands : PermissionsLocationsAddToolApprovalDetails +public sealed class RegisterEventInterestResult { - /// - [JsonIgnore] - public override string Kind => "commands"; - - /// Command identifiers covered by this approval. - [JsonPropertyName("commandIdentifiers")] - public required IList CommandIdentifiers { get; set; } + /// Opaque handle for this registration. Pass to releaseInterest to release. Each call to registerInterest produces a fresh handle, even when the same eventType is registered multiple times. + [JsonPropertyName("handle")] + public string Handle { get; set; } = string.Empty; } -/// Location-persisted tool approval details for read-only filesystem operations. -/// The read variant of . +/// Event type to register consumer interest for, used by runtime gating logic. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsRead : PermissionsLocationsAddToolApprovalDetails +internal sealed class RegisterEventInterestParams { - /// - [JsonIgnore] - public override string Kind => "read"; + /// 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; } -/// Location-persisted tool approval details for filesystem write operations. -/// The write variant of . +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsWrite : PermissionsLocationsAddToolApprovalDetails +public sealed class EventLogReleaseInterestResult { - /// - [JsonIgnore] - public override string Kind => "write"; + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Location-persisted tool approval details for an MCP server tool, or all tools when `toolName` is null. -/// The mcp variant of . +/// Opaque handle previously returned by `registerInterest` to release. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMcp : PermissionsLocationsAddToolApprovalDetails +internal sealed class ReleaseEventInterestParams { - /// - [JsonIgnore] - public override string Kind => "mcp"; - - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// 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; - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-persisted tool approval details for MCP sampling requests from a server. -/// The mcp-sampling variant of . +/// Request count and cost metrics for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMcpSampling : PermissionsLocationsAddToolApprovalDetails +public sealed class UsageMetricsModelMetricRequests { - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; + /// User-initiated premium request cost (with multiplier applied). + [JsonPropertyName("cost")] + public double Cost { get; set; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Number of API requests made with this model. + [JsonPropertyName("count")] + public long Count { get; set; } } -/// Location-persisted tool approval details for writes to long-term memory. -/// The memory variant of . +/// Per-model token-detail entry containing the accumulated token count for one token type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMemory : PermissionsLocationsAddToolApprovalDetails +public sealed class UsageMetricsModelMetricTokenDetail { - /// - [JsonIgnore] - public override string Kind => "memory"; + /// Accumulated token count for this token type. + [JsonPropertyName("tokenCount")] + public long TokenCount { get; set; } } -/// Location-persisted tool approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Token usage metrics for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsCustomTool : PermissionsLocationsAddToolApprovalDetails +public sealed class UsageMetricsModelMetricUsage { - /// - [JsonIgnore] - public override string Kind => "custom-tool"; + /// Total tokens read from prompt cache. + [JsonPropertyName("cacheReadTokens")] + public long CacheReadTokens { get; set; } - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { get; set; } -} + /// Total tokens written to prompt cache. + [JsonPropertyName("cacheWriteTokens")] + public long CacheWriteTokens { get; set; } -/// 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 PermissionsLocationsAddToolApprovalDetailsExtensionManagement : PermissionsLocationsAddToolApprovalDetails -{ - /// - [JsonIgnore] - public override string Kind => "extension-management"; + /// Total input tokens consumed. + [JsonPropertyName("inputTokens")] + public long InputTokens { 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; } + /// Total output tokens produced. + [JsonPropertyName("outputTokens")] + public long OutputTokens { get; set; } + + /// Total output tokens used for reasoning. + [JsonPropertyName("reasoningTokens")] + public long? ReasoningTokens { get; set; } } -/// Location-persisted factory approval, optionally narrowed by approval key. -/// The factory variant of . +/// 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 partial class PermissionsLocationsAddToolApprovalDetailsFactory : PermissionsLocationsAddToolApprovalDetails +public sealed class UsageMetricsModelMetric { - /// - [JsonIgnore] - public override string Kind => "factory"; + /// 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; } - /// Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approvalKey")] - public string? ApprovalKey { get; set; } -} + /// Request count and cost metrics for this model. + [JsonPropertyName("requests")] + public UsageMetricsModelMetricRequests Requests { get => field ??= new(); set; } -/// 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 partial class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess : PermissionsLocationsAddToolApprovalDetails -{ - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// Token count details per type. + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { 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; } } -/// 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 . +/// Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess : PermissionsLocationsAddToolApprovalDetails +public sealed class UsageMetricsAgentMetric { - /// - [JsonIgnore] - public override string Kind => "extension-env-access"; + /// 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; } - /// Names of the sensitive environment variables this approval covers. Values are never persisted. - [JsonPropertyName("environmentVariables")] - public required IList EnvironmentVariables { get; set; } + /// Configured agent name, when this is a subagent. + [JsonPropertyName("agentName")] + public string? AgentName { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { 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; } } -/// Location-scoped tool approval to persist. +/// Aggregated code change metrics. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationAddToolApprovalParams +public sealed class UsageMetricsCodeChanges { - /// Tool approval to persist and apply. - [JsonPropertyName("approval")] - public PermissionsLocationsAddToolApprovalDetails Approval { get => field ??= new(); set; } + /// Distinct file paths modified during the session. + [JsonPropertyName("filesModified")] + public IList FilesModified { get => field ??= []; set; } - /// Location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public string LocationKey { get; set; } = string.Empty; + /// Number of distinct files modified. + [JsonPropertyName("filesModifiedCount")] + public long FilesModifiedCount { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Total lines of code added. + [JsonPropertyName("linesAdded")] + public long LinesAdded { get; set; } + + /// Total lines of code removed. + [JsonPropertyName("linesRemoved")] + public long LinesRemoved { get; set; } } -/// Folder trust check result. +/// Session-wide token-detail entry containing the accumulated token count for one token type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FolderTrustCheckResult +public sealed class UsageMetricsTokenDetail { - /// Whether the folder is trusted. - [JsonPropertyName("trusted")] - public bool Trusted { get; set; } + /// Accumulated token count for this token type. + [JsonPropertyName("tokenCount")] + public long TokenCount { get; set; } } -/// Folder path to check for trust. +/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FolderTrustCheckParams +public sealed class UsageGetMetricsResult { - /// Folder path to check. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. + [JsonPropertyName("agentMetrics")] + public IDictionary? AgentMetrics { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Aggregated code change metrics. + [JsonPropertyName("codeChanges")] + public UsageMetricsCodeChanges CodeChanges { get => field ??= new(); 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; } + /// Currently active model identifier. + [JsonPropertyName("currentModel")] + public string? CurrentModel { get; set; } + + /// Input tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallInputTokens")] + public long LastCallInputTokens { get; set; } + + /// Output tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallOutputTokens")] + public long LastCallOutputTokens { get; set; } + + /// Per-model token and request metrics, keyed by model identifier. + [JsonPropertyName("modelMetrics")] + public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + + /// ISO 8601 timestamp when the session started. + [JsonPropertyName("sessionStartTime")] + public DateTimeOffset SessionStartTime { get; set; } + + /// Session-wide per-token-type accumulated token counts. + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } + + /// Total time spent in model API calls (milliseconds). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("totalApiDurationMs")] + public TimeSpan TotalApiDuration { get; set; } + + /// Session-wide accumulated nano-AI units cost. + [JsonPropertyName("totalNanoAiu")] + public double? TotalNanoAiu { get; set; } + + /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). + [JsonPropertyName("totalPremiumRequestCost")] + public double TotalPremiumRequestCost { get; set; } + + /// Raw count of user-initiated API requests. + [JsonPropertyName("totalUserRequests")] + public long TotalUserRequests { get; set; } } -/// Folder path to add to trusted folders. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FolderTrustAddParams +internal sealed class SessionUsageGetMetricsRequest { - /// 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; } -/// Indicates whether the operation succeeded. +/// Prediction result. Available results include prediction details; unavailable results include an explicit reason. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsUrlsSetUnrestrictedModeResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SessionLimitPredictionResultAvailable), "available")] +[JsonDerivedType(typeof(SessionLimitPredictionResultUnavailable), "unavailable")] +public partial class SessionLimitPredictionResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Whether the URL-permission policy should run in unrestricted mode. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionUrlsSetUnrestrictedModeParams -{ - /// 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 repository the remote session targets. +/// Baseline data provenance for a prediction. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSnapshotRemoteMetadataRepository +public sealed class SessionLimitPredictionBaselineData { - /// 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; + /// End of the baseline data slice. + [JsonPropertyName("windowEnd")] + public string WindowEnd { get; set; } = string.Empty; - /// The GitHub owner (user or organization) of the target repository. - [JsonPropertyName("owner")] - public string Owner { get; set; } = string.Empty; + /// Start of the baseline data slice. + [JsonPropertyName("windowStart")] + public string WindowStart { get; set; } = string.Empty; } -/// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. +/// Semantic usage tier and its AI-credit cap. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSnapshotRemoteMetadata +public sealed class SessionLimitPredictionTierOption { - /// 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; } + /// AI-credit cap for this tier. + [JsonPropertyName("cap")] + public double Cap { get; set; } - /// Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. - [JsonPropertyName("taskType")] - public MetadataSnapshotRemoteMetadataTaskType? TaskType { get; set; } + /// Semantic usage tier. + [JsonPropertyName("tier")] + public SessionLimitPredictionTier Tier { get; set; } } -/// Public-facing projection of workspace metadata for SDK / TUI consumers. -public sealed class SessionMetadataSnapshotWorkspace +/// Explainable AI-credit session-limit prediction. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionLimitPredictionDetails { - /// 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; } + /// Baseline data provenance. + [JsonPropertyName("baselineData")] + public SessionLimitPredictionBaselineData BaselineData { get => field ??= new(); set; } - /// Current working directory at session start. - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// Client population used for the prediction. + [JsonPropertyName("clientType")] + public SessionLimitPredictionClientType ClientType { get; set; } - /// Resolved git root for cwd, if any. - [JsonPropertyName("git_root")] - public string? GitRoot { get; set; } + /// Resolved model family when known. + [JsonPropertyName("family")] + public string? Family { get; set; } - /// Repository host type, if known. - [JsonPropertyName("host_type")] - public WorkspaceSummaryHostType? HostType { get; set; } + /// Model identifier used for lookup. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; - /// 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; + /// Recommended maximum AI credits for this session. + [JsonPropertyName("recommendedCap")] + public double RecommendedCap { get; set; } - /// Display name for the session, if set. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Tier chosen as the recommended cap. + [JsonPropertyName("recommendedTier")] + public SessionLimitPredictionTier RecommendedTier { get; set; } - /// Repository identifier in 'owner/repo' or 'org/project/repo' format, if any. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// Baseline fallback level used to create the prediction. + [JsonPropertyName("source")] + public SessionLimitPredictionSource Source { get; set; } - /// ISO 8601 timestamp when the workspace was last updated. - [JsonPropertyName("updated_at")] - public DateTimeOffset? UpdatedAt { 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; - /// Whether the display name was explicitly set by the user. - [JsonPropertyName("user_named")] - public bool? UserNamed { get; set; } + /// Ordered usage tiers and their AI-credit caps. + [JsonPropertyName("tiers")] + public IList Tiers { get => field ??= []; set; } } -/// Point-in-time snapshot of slow-changing session identifier and state fields. +/// The available variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionMetadataSnapshot +public partial class SessionLimitPredictionResultAvailable : SessionLimitPredictionResult { - /// True when the session was detected to be in use by another process at construction time. Local consumers may surface a confirmation prompt before fully attaching. Always false for new sessions. - [JsonPropertyName("alreadyInUse")] - public bool AlreadyInUse { get; set; } - - /// Runtime client name associated with the session (telemetry identifier). - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// + [JsonIgnore] + public override string Kind => "available"; - /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot'). - [JsonPropertyName("currentMode")] - public MetadataSnapshotCurrentMode CurrentMode { get; set; } + /// Predicted session limit details. + [JsonPropertyName("prediction")] + public required SessionLimitPredictionDetails Prediction { get; set; } +} - /// Live indexed-search state for this session activation. Omitted by runtimes that do not expose indexed-search status; absence does not indicate enablement. - [JsonPropertyName("indexedSearch")] - public IndexedSearchState? IndexedSearch { get; set; } +/// The unavailable variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SessionLimitPredictionResultUnavailable : SessionLimitPredictionResult +{ + /// + [JsonIgnore] + public override string Kind => "unavailable"; - /// User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. - [JsonPropertyName("initialName")] - public string? InitialName { get; set; } + /// Reason no prediction is available. + [JsonPropertyName("reason")] + public required SessionLimitPredictionUnavailableReason Reason { get; 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; } +/// 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; } - /// 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 model identifier override. If omitted, the session's current model is used. + [JsonPropertyName("modelId")] + public string? ModelId { 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; } +/// 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; } - /// Currently selected model identifier, if any. - [JsonPropertyName("selectedModel")] - public string? SelectedModel { get; set; } + /// Optional model identifier override. If omitted, the session's current model is used. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } - /// 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; } +/// GitHub URL for the session and a flag indicating whether remote steering is enabled. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class RemoteEnableResult +{ + /// Whether remote steering is enabled. + [JsonPropertyName("remoteSteerable")] + public bool RemoteSteerable { get; set; } - /// 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; } + /// GitHub frontend URL for this session. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// Identifies the target session. +/// 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 SessionMetadataSnapshotRequest +internal sealed class RemoteEnableRequest { + /// Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. + [JsonPropertyName("mode")] + public RemoteSessionMode? Mode { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -19783,3566 +25901,3531 @@ internal sealed class SessionMetadataSnapshotRequest /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataGetClientMetadataRequest +internal sealed class SessionRemoteDisableRequest { /// 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. +/// 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)] -internal sealed class MetadataUpdateClientMetadataRequest +public sealed class RemoteNotifySteerableChangedResult { - /// 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; } +/// 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; } /// 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. +/// Current sharing status and shareable GitHub URL for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataIsProcessingResult +public sealed class VisibilityGetResult { - /// 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; } + /// Shareable GitHub URL for the session. Present when the session is synced and the URL can be resolved. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("shareUrl")] + public string? ShareUrl { get; set; } + + /// Current sharing status. Absent when the session is not synced or the status could not be retrieved (e.g. the user is not authenticated). + [JsonPropertyName("status")] + public SessionVisibilityStatus? Status { get; set; } + + /// Whether the session has been synced to Mission Control (i.e. has a GitHub task). When false, the session cannot be shared and `status`/`shareUrl` are absent. + [JsonPropertyName("synced")] + public bool Synced { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataIsProcessingRequest +internal sealed class SessionVisibilityGetRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Current activity flags for the session. +/// Effective sharing status and shareable GitHub URL after updating session visibility. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionActivity +public sealed class VisibilitySetResult { - /// Whether an in-flight operation can currently be aborted. - [JsonPropertyName("abortable")] - public bool Abortable { 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; } - /// Whether the session currently has active work, including running turns or tasks. - [JsonPropertyName("hasActiveWork")] - public bool HasActiveWork { 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; } } -/// Identifies the target session. +/// Desired sharing status for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataActivityRequest +internal sealed class VisibilitySetRequest { /// 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; } } -/// Token-usage breakdown for the session's current context window. -public sealed class MetadataContextInfoResultContextInfo +/// 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 ScheduleEntry { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%). - [JsonPropertyName("bufferTokens")] - public long BufferTokens { get; set; } + /// Absolute fire time (epoch milliseconds) for a one-shot calendar schedule. + [JsonPropertyName("at")] + public long? At { get; set; } - /// Token count at which background compaction starts (configurable percentage of promptTokenLimit). - [JsonPropertyName("compactionThreshold")] - public long CompactionThreshold { get; set; } + /// 5-field cron expression for a recurring calendar schedule, evaluated in `tz`. + [JsonPropertyName("cron")] + public string? Cron { get; set; } - /// Tokens consumed by user/assistant/tool messages. - [JsonPropertyName("conversationTokens")] - public long ConversationTokens { 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; } - /// Prompt token limit plus the model's full output token limit. - [JsonPropertyName("limit")] - public long Limit { 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; } - /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools). - [JsonPropertyName("mcpToolsTokens")] - public long McpToolsTokens { get; set; } + /// Interval between scheduled ticks, in milliseconds (relative-interval schedules). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("intervalMs")] + public TimeSpan? Interval { get; set; } - /// The model used for token counting. - [JsonPropertyName("modelName")] - public string ModelName { get; set; } = string.Empty; + /// ISO 8601 timestamp when the next tick is scheduled to fire. + [JsonPropertyName("nextRunAt")] + public DateTimeOffset NextRunAt { get; set; } - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified). - [JsonPropertyName("promptTokenLimit")] - public long PromptTokenLimit { get; set; } + /// Prompt text that gets enqueued on every tick. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Tokens consumed by the system prompt. - [JsonPropertyName("systemTokens")] - public long SystemTokens { get; set; } + /// Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`). + [JsonPropertyName("recurring")] + public bool Recurring { get; set; } - /// Tokens consumed by tool definitions sent to the model (excludes deferred tools). - [JsonPropertyName("toolDefinitionsTokens")] - public long ToolDefinitionsTokens { 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; } - /// Sum of system, conversation and tool-definition tokens. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// IANA timezone the `cron` expression is evaluated in. + [JsonPropertyName("tz")] + public string? Tz { get; set; } } -/// Token breakdown for the session's current context window, or null if uninitialized. +/// Snapshot of the currently active recurring prompts for this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextInfoResult +public sealed class ScheduleList { - /// 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; } + /// Active scheduled prompts, ordered by id. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } } -/// Model identifier and token limits used to compute the context-info breakdown. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataContextInfoRequest +internal sealed class SessionScheduleListRequest { - /// 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; } - /// 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 +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionScheduleHydrateRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Successful compaction history for the session. -public sealed class MetadataContextAttributionResultContextAttributionCompactions +/// Whether the session currently has an active self-paced schedule. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ScheduleHasSelfPacedResult { - /// Number of successful compactions in this session. - [JsonPropertyName("count")] - public long Count { get; set; } + /// True when at least one active schedule is self-paced. + [JsonPropertyName("hasSelfPaced")] + public bool HasSelfPaced { get; set; } } -/// RPC data type for MetadataContextAttributionResultContextAttributionEntry operations. -public sealed class MetadataContextAttributionResultContextAttributionEntry +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionScheduleHasSelfPacedRequest { - /// 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; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Optional `id` of the parent entry: e.g. a `plugin` entry parenting its `skill`/`mcpServer` entries, or the `system` entry parenting `toolDefinition` entries. Omitted for top-level entries. - [JsonPropertyName("parentId")] - public string? ParentId { get; set; } +/// 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; } - /// Token count currently in context attributable to this entry. - [JsonPropertyName("tokens")] - public long Tokens { get; set; } + /// User-facing validation error, when registration failed. + [JsonPropertyName("error")] + public string? Error { get; set; } } -/// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. -public sealed class MetadataContextAttributionResultContextAttribution +/// Register a relative-interval scheduled prompt. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ScheduleAddRequest { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. - [JsonPropertyName("bufferTokens")] - public long BufferTokens { get; set; } + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { 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; } + /// Human-readable interval such as `30s`, `5m`, or `2h`. + [JsonPropertyName("interval")] + public string Interval { get; set; } = string.Empty; - /// Successful compaction history for the session. - [JsonPropertyName("compactions")] - public MetadataContextAttributionResultContextAttributionCompactions Compactions { get => field ??= new(); set; } + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. - [JsonPropertyName("compactionThreshold")] - public long CompactionThreshold { get; set; } + /// Whether the schedule should re-arm after each tick. Defaults to true. + [JsonPropertyName("recurring")] + public bool? Recurring { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// 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; } +/// 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; - /// 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; + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// 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; + /// Whether the schedule should re-arm after each tick. Defaults to true. + [JsonPropertyName("recurring")] + public bool? Recurring { get; set; } - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. - [JsonPropertyName("promptTokenLimit")] - public long PromptTokenLimit { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// IANA timezone for evaluating the cron expression. + [JsonPropertyName("tz")] + public string? Tz { get; set; } } -/// Per-source attribution breakdown for the session's current context window, or null if uninitialized. +/// Register an absolute-time scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextAttributionResult +internal sealed class ScheduleAddAtRequest { - /// 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; } + /// Epoch milliseconds when the prompt should fire. + [JsonPropertyName("at")] + public long At { get; set; } + + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// Whether the schedule should re-arm after each tick. Defaults to false. + [JsonPropertyName("recurring")] + public bool? Recurring { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Register a self-paced scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataGetContextAttributionRequest +internal sealed class ScheduleAddSelfPacedRequest { + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single large message currently in context. +/// Re-arm a self-paced scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContextHeaviestMessage +internal sealed class ScheduleRearmSelfPacedRequest { - /// 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; + /// Epoch milliseconds when the prompt should next fire. + [JsonPropertyName("at")] + public long At { get; set; } - /// Role of the chat message (`user`, `assistant`, or `tool`). - [JsonPropertyName("role")] - public string Role { get; set; } = string.Empty; + /// Id of the self-paced scheduled prompt. + [JsonPropertyName("id")] + public long Id { get; set; } - /// Token count currently in context for this individual message. - [JsonPropertyName("tokens")] - public long Tokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The heaviest individual messages in the session's context window, most-expensive first. +/// 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 MetadataContextHeaviestMessagesResult +public sealed class ScheduleStopResult { - /// Heaviest messages, most-expensive first. - [JsonPropertyName("messages")] - public IList Messages { get => field ??= []; set; } - - /// Total token count of the current context window, so callers can compute each message's share without a second call. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// The removed entry, or omitted if no entry matched. + [JsonPropertyName("entry")] + public ScheduleEntry? Entry { get; set; } } -/// Parameters for the heaviest-messages query. +/// Identifier of the scheduled prompt to remove. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataContextHeaviestMessagesRequest +internal sealed class ScheduleStopRequest { - /// Maximum number of messages to return, most-expensive first. Omit for the server default. - [JsonPropertyName("limit")] - public long? Limit { get; set; } + /// 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; } -/// 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. +/// 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 MetadataRecordContextChangeResult +public sealed class ProviderTokenAcquireResult { + /// The bearer token value (without the `Bearer ` prefix). + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; } -/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. +/// 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 SessionWorkingDirectoryContext +public sealed class ProviderTokenAcquireRequest { - /// Merge-base commit SHA (fork point from the remote default branch). - [JsonPropertyName("baseCommit")] - public string? BaseCommit { get; set; } + /// 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; - /// Current git branch name. - [JsonPropertyName("branch")] - public string? Branch { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Current working directory path. - [JsonPropertyName("cwd")] - public string Cwd { get; set; } = string.Empty; +/// Result returned by an extension workflow closure. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowExecuteResult +{ + /// Workflow result value. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} - /// Root directory of the git repository, resolved via git rev-parse. - [JsonPropertyName("gitRoot")] - public string? GitRoot { get; set; } +/// Parameters sent to the owning extension to execute a workflow closure. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowExecuteRequest +{ + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } - /// Head commit of the current git branch. - [JsonPropertyName("headCommit")] - public string? HeadCommit { get; set; } + /// Opaque token identifying this workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; - /// Hosting platform type of the repository. - [JsonPropertyName("hostType")] - public SessionWorkingDirectoryContextHostType? HostType { get; set; } + /// Registered workflow name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// 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; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Updated working-directory/git context to record on the session. +/// Parameters for cooperatively aborting a workflow body. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataRecordContextChangeRequest +public sealed class WorkflowAbortRequest { - /// 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 token identifying the execution attempt to abort. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// 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. +/// Whether the client authoritatively confirmed its external work stopped. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSetWorkingDirectoryResult +public sealed class ClientTaskCancelResult { - /// Working directory after the update. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; + /// True only when the owner confirms that external work stopped before responding. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. +/// Runtime-to-owner cancellation request for a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataSetWorkingDirectoryRequest +public sealed class ClientTaskCancelRequest { - /// Target session identifier. + /// Opaque identifier shared by coalesced cancellation callers. + [JsonPropertyName("cancellationId")] + public string CancellationId { get; set; } = string.Empty; + + /// Owner-scoped task key included for correlation. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Reason the runtime requests cancellation. + [JsonPropertyName("reason")] + public ClientTaskCancelReason Reason { get; set; } + + /// Session that owns the client task. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// 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. +/// Describes a filesystem error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataRecomputeContextTokensResult +public sealed class SessionFsError { - /// Tokens contributed by user/assistant/tool messages (excludes system/developer prompts). - [JsonPropertyName("messagesTokenCount")] - public long MessagesTokenCount { get; set; } + /// Error classification. + [JsonPropertyName("code")] + public SessionFsErrorCode Code { get; set; } - /// Tokens contributed by system/developer prompt snapshots. - [JsonPropertyName("systemTokenCount")] - public long SystemTokenCount { get; set; } + /// Free-form detail about the error, for logging/diagnostics. + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Sum of tokens across chat-context and system-context messages currently held by the session. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + [JsonPropertyName("writeChanged")] + public bool? WriteChanged { get; set; } } -/// Model identifier to use when re-tokenizing the session's existing messages. +/// File content as a UTF-8 string, or a filesystem error if the read failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataRecomputeContextTokensRequest +public sealed class SessionFsReadFileResult { - /// 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; + /// File content as UTF-8 string. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } +} + +/// Path of the file to read from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReadFileRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Availability of built-in job tools surfaced to boundary consumers. +/// File bytes as standard base64, or a filesystem error if the read failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsBuiltInToolAvailabilitySnapshot +public sealed class SessionFsReadFileBytesResult { - /// Whether the create-pull-request tool is available. - [JsonPropertyName("createPullRequest")] - public bool? CreatePullRequest { get; set; } + /// Exact file bytes encoded as standard base64. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Whether the report-progress tool is available. - [JsonPropertyName("reportProgress")] - public bool? ReportProgress { get; set; } + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } } -/// Redacted job settings for a session. The job nonce is excluded. +/// Path of the binary file to read from the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsJobSnapshot +public sealed class SessionFsReadFileBytesRequest { - /// Availability of job-specific built-in tools. - [JsonPropertyName("builtInToolAvailability")] - public SessionSettingsBuiltInToolAvailabilitySnapshot? BuiltInToolAvailability { get; set; } - - /// GitHub Actions event type for the job. - [JsonPropertyName("eventType")] - public string? EventType { get; set; } + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Whether this is the workflow's trigger job. - [JsonPropertyName("isTriggerJob")] - public bool? IsTriggerJob { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Redacted model routing settings for a session. +/// File path, content to write, and optional mode for the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsModelSnapshot +public sealed class SessionFsWriteFileRequest { - /// Agent service callback URL for job and progress updates. - [JsonPropertyName("callbackUrl")] - public string? CallbackUrl { get; set; } + /// Content to write. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Default reasoning effort for the selected model. - [JsonPropertyName("defaultReasoningEffort")] - public string? DefaultReasoningEffort { get; set; } + /// Optional POSIX-style mode for newly created files. + [JsonPropertyName("mode")] + public long? Mode { get; set; } - /// Agent job identifier for the session. - [JsonPropertyName("instanceId")] - public string? InstanceId { get; set; } + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Selected model identifier. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Online-evaluation settings safe to expose across the SDK boundary. +/// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsOnlineEvaluationSnapshot +public sealed class SessionFsWriteFileBytesRequest { - /// Whether online evaluation is disabled. - [JsonPropertyName("disableOnlineEvaluation")] - public bool? DisableOnlineEvaluation { get; set; } + /// Exact file bytes encoded as standard base64. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Whether online-evaluation output-file generation is enabled. - [JsonPropertyName("enableOnlineEvaluationOutputFile")] - public bool? EnableOnlineEvaluationOutputFile { 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; } -/// Redacted repository and GitHub host settings for a session. +/// 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 SessionSettingsRepoSnapshot +public sealed class SessionFsAppendFileRequest { - /// Checked-out repository branch. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// Checked-out commit SHA. - [JsonPropertyName("commit")] - public string? Commit { get; set; } - - /// GitHub server host name. - [JsonPropertyName("host")] - public string? Host { get; set; } - - /// Protocol used to access the GitHub host. - [JsonPropertyName("hostProtocol")] - public string? HostProtocol { get; set; } - - /// GitHub repository database ID. - [JsonPropertyName("id")] - public double? Id { get; set; } - - /// Repository name. - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// GitHub repository owner database ID. - [JsonPropertyName("ownerId")] - public double? OwnerId { get; set; } - - /// Repository owner login. - [JsonPropertyName("ownerName")] - public string? OwnerName { get; set; } - - /// Number of commits in the pull request. - [JsonPropertyName("prCommitCount")] - public double? PrCommitCount { get; set; } + /// Content to append. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Whether the repository is writable. - [JsonPropertyName("readWrite")] - public bool? ReadWrite { get; set; } + /// Optional POSIX-style mode for newly created files. + [JsonPropertyName("mode")] + public long? Mode { get; set; } - /// GitHub secret-scanning service URL. - [JsonPropertyName("secretScanningUrl")] - public string? SecretScanningUrl { get; set; } + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// GitHub server base URL. - [JsonPropertyName("serverUrl")] - public string? ServerUrl { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Redacted validation and memory-tool settings for a session. +/// Indicates whether the requested path exists in the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsValidationSnapshot +public sealed class SessionFsExistsResult { - /// Whether advisory validation is enabled. - [JsonPropertyName("advisoryEnabled")] - public bool? AdvisoryEnabled { get; set; } - - /// Whether CodeQL validation is enabled. - [JsonPropertyName("codeqlEnabled")] - public bool? CodeqlEnabled { get; set; } - - /// Whether code-review validation is enabled. - [JsonPropertyName("codeReviewEnabled")] - public bool? CodeReviewEnabled { get; set; } - - /// Model used for code-review validation. - [JsonPropertyName("codeReviewModel")] - public string? CodeReviewModel { get; set; } - - /// Dependabot validation timeout budget in seconds. - [JsonPropertyName("dependabotTimeout")] - public double? DependabotTimeout { get; set; } - - /// Whether the memory-store tool is enabled. - [JsonPropertyName("memoryStoreEnabled")] - public bool? MemoryStoreEnabled { get; set; } - - /// Whether the memory-vote tool is enabled. - [JsonPropertyName("memoryVoteEnabled")] - public bool? MemoryVoteEnabled { get; set; } + /// Whether the path exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } +} - /// Whether secret-scanning validation is enabled. - [JsonPropertyName("secretScanningEnabled")] - public bool? SecretScanningEnabled { get; set; } +/// Path to test for existence in the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsExistsRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// General validation timeout budget in seconds. - [JsonPropertyName("timeout")] - public double? Timeout { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Redacted, serializable view of session runtime settings for SDK boundary consumers. Secrets and raw feature flags are intentionally excluded. +/// Filesystem metadata for the requested path, or a filesystem error if the stat failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsSnapshot +public sealed class SessionFsStatResult { - /// Name of the SDK client that created the session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } - - /// Redacted job settings. - [JsonPropertyName("job")] - public SessionSettingsJobSnapshot Job { get => field ??= new(); set; } + /// ISO 8601 timestamp of creation. + [JsonPropertyName("birthtime")] + public DateTimeOffset Birthtime { get; set; } - /// Redacted model routing settings. - [JsonPropertyName("model")] - public SessionSettingsModelSnapshot Model { get => field ??= new(); set; } + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } - /// Online-evaluation settings safe for SDK consumers. - [JsonPropertyName("onlineEvaluation")] - public SessionSettingsOnlineEvaluationSnapshot OnlineEvaluation { get => field ??= new(); set; } + /// Whether the path is a directory. + [JsonPropertyName("isDirectory")] + public bool IsDirectory { get; set; } - /// Redacted repository and host settings. - [JsonPropertyName("repo")] - public SessionSettingsRepoSnapshot Repo { get => field ??= new(); set; } + /// Whether the path is a file. + [JsonPropertyName("isFile")] + public bool IsFile { get; set; } - /// Session start time as Unix epoch milliseconds. - [JsonPropertyName("startTimeMs")] - public double? StartTimeMs { get; set; } + /// ISO 8601 timestamp of last modification. + [JsonPropertyName("mtime")] + public DateTimeOffset Mtime { get; set; } - /// Session timeout in milliseconds. - [JsonPropertyName("timeoutMs")] - public double? TimeoutMs { get; set; } + /// File size in bytes. + [JsonPropertyName("size")] + public long Size { get; set; } +} - /// Redacted validation and memory-tool settings. - [JsonPropertyName("validation")] - public SessionSettingsValidationSnapshot Validation { get => field ??= new(); set; } +/// Path whose metadata should be returned from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsStatRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Agent runtime version selector copied from the session settings, such as `latest` or a runtime release identifier. - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// 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 SessionSettingsSnapshotRequest +public sealed class SessionFsMkdirRequest { + /// Optional POSIX-style mode for newly created directories. + [JsonPropertyName("mode")] + public long? Mode { get; set; } + + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Create parent directories as needed. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of evaluating a Rust-owned settings predicate. +/// Names of entries in the requested directory, or a filesystem error if the read failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsEvaluatePredicateResult +public sealed class SessionFsReaddirResult { - /// Whether the named settings predicate evaluated to enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Entry names in the directory. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } } -/// Named Rust-owned settings predicate to evaluate for this session. +/// Directory path whose entries should be listed from the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsEvaluatePredicateRequest +public sealed class SessionFsReaddirRequest { - /// Predicate name. The runtime owns the raw feature-flag names and composition logic. - [JsonPropertyName("name")] - public SessionSettingsPredicateName Name { 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; - - /// Tool name for tool-scoped predicates such as trivial-change handling. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } } -/// Content-exclusion decision for one requested path. +/// Directory entry returned by session filesystem `readdirWithTypes`, with name and entry type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContentExclusionPathCheck +public sealed class SessionFsReaddirWithTypesEntry { - /// Whether the session's complete content-exclusion policy excludes the path. - [JsonPropertyName("excluded")] - public bool Excluded { get; set; } + /// Entry name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// The path supplied by the caller. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Entry type. + [JsonPropertyName("type")] + public SessionFsReaddirWithTypesEntryType Type { get; set; } } -/// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. +/// 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 ContentExclusionCheckPathsResult +public sealed class SessionFsReaddirWithTypesResult { - /// 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; } + /// Directory entries with type information. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } - /// Per-path decisions in request order. Empty when available is false. - [JsonPropertyName("checks")] - public IList Checks { get => field ??= []; set; } + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } } -/// Local file system absolute paths within the session working directory to check against its content-exclusion policy. +/// 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 ContentExclusionCheckPathsRequest +public sealed class SessionFsReaddirWithTypesRequest { - /// 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; } + /// 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; } -/// Identifier of the spawned process, used to correlate streamed output and exit notifications. +/// 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 ShellExecResult +public sealed class SessionFsRmRequest { - /// Unique identifier for tracking streamed output. - [JsonPropertyName("processId")] - public string ProcessId { get; set; } = string.Empty; -} + /// Ignore errors if the path does not exist. + [JsonPropertyName("force")] + public bool? Force { get; set; } -/// Shell command to run, with optional working directory and timeout in milliseconds. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellExecRequest -{ - /// Shell command to execute. - [JsonPropertyName("command")] - public string Command { get; set; } = string.Empty; + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Working directory (defaults to session working directory). - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// 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; - - /// 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellKillResult -{ - /// 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. +/// 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 ShellKillRequest +public sealed class SessionFsRenameRequest { - /// Process identifier returned by shell.exec. - [JsonPropertyName("processId")] - public string ProcessId { get; set; } = string.Empty; + /// 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; - /// Signal to send (default: SIGTERM). - [JsonPropertyName("signal")] - public ShellKillSignal? Signal { get; set; } + /// Source path using SessionFs conventions. + [JsonPropertyName("src")] + public string Src { get; set; } = string.Empty; } -/// Result of a user-requested shell command. +/// Query results including rows, columns, and rows affected, or a filesystem error if execution failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserRequestedShellCommandResult +public sealed class SessionFsSqliteQueryResult { - /// Error output when the execution failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Column names from the result set. + [JsonPropertyName("columns")] + public IList Columns { get => field ??= []; set; } - /// Process exit code, when available. - [JsonPropertyName("exitCode")] - public long? ExitCode { get; set; } + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } - /// Captured command output. - [JsonPropertyName("output")] - public string Output { get; set; } = string.Empty; + /// SQLite last_insert_rowid() value for INSERT. + [JsonPropertyName("lastInsertRowid")] + public long? LastInsertRowid { get; set; } - /// Whether the command completed successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// For SELECT: array of row objects. For others: empty array. + [JsonPropertyName("rows")] + public IList> Rows { get => field ??= []; set; } - /// Tool call id emitted for the shell execution. - [JsonPropertyName("toolCallId")] - public string ToolCallId { get; set; } = string.Empty; + /// Number of rows affected (for INSERT/UPDATE/DELETE). + [JsonPropertyName("rowsAffected")] + public long RowsAffected { get; set; } } -/// User-requested shell command and cancellation handle. +/// 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 ShellExecuteUserRequestedRequest +public sealed class SessionFsSqliteQueryRequest { - /// 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; -} + /// Optional named bind parameters. + [JsonPropertyName("params")] + public IDictionary? Params { get; set; } -/// Cancellation result for a user-requested shell command. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CancelUserRequestedShellCommandResult -{ - /// Whether an in-flight execution was found and signalled to cancel. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } -} + /// SQL query to execute. + [JsonPropertyName("query")] + public string Query { get; set; } = string.Empty; -/// User-requested shell execution cancellation handle. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellCancelUserRequestedRequest -{ - /// Request ID previously passed to executeUserRequested. - [JsonPropertyName("requestId")] - public string RequestId { 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; } -/// Post-compaction context window usage breakdown. +/// Classified SQLite transaction failure. busyOrLocked guarantees rollback; postCommitAmbiguous must never be retried. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryCompactContextWindow +public sealed class SessionFsSqliteTransactionError { - /// 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; } + /// Machine-readable classification of the transaction failure. + [JsonPropertyName("errorClass")] + public SessionFsSqliteTransactionErrorClass ErrorClass { get; set; } - /// Token count from tool definitions. - [JsonPropertyName("toolDefinitionsTokens")] - public long? ToolDefinitionsTokens { get; set; } + /// Human-readable transaction failure message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; } -/// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. +/// Per-statement results, or a classified transaction error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryCompactResult +public sealed class SessionFsSqliteTransactionResult { - /// Post-compaction context window usage breakdown. - [JsonPropertyName("contextWindow")] - public HistoryCompactContextWindow? ContextWindow { get; set; } - - /// Number of messages removed during compaction. - [JsonPropertyName("messagesRemoved")] - public long MessagesRemoved { get; set; } - - /// Whether compaction completed successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } - - /// Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). - [JsonPropertyName("summaryContent")] - public string? SummaryContent { get; set; } + /// Classified transaction failure, when execution did not succeed. + [JsonPropertyName("error")] + public SessionFsSqliteTransactionError? Error { get; set; } - /// Number of tokens freed by compaction. - [JsonPropertyName("tokensRemoved")] - public long TokensRemoved { get; set; } + /// Per-statement query results in input order. + [JsonPropertyName("results")] + public IList Results { get => field ??= []; set; } } -/// RPC data type for SessionHistoryCompact operations. +/// One statement in an atomic SQLite transaction. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionHistoryCompactRequest +public sealed class SessionFsSqliteTransactionStatement { - /// 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; } + /// Optional named bind parameters. + [JsonPropertyName("params")] + public IDictionary? Params { 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; } + /// SQL statement to execute. + [JsonPropertyName("query")] + public string Query { get; set; } = string.Empty; - /// 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; } + /// How to execute the statement. + [JsonPropertyName("queryType")] + public SessionFsSqliteQueryType QueryType { get; set; } } -/// RPC data type for SessionHistoryCompactRequestWithSession operations. +/// Statements to execute atomically. Providers apply busy handling for every call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryCompactRequestWithSession +public sealed class SessionFsSqliteTransactionRequest { - /// 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; } + /// Ordered SQL statements to execute in one transaction. + [JsonPropertyName("statements")] + public IList Statements { get => field ??= []; set; } } -/// Number of events that were removed by the truncation. +/// Indicates whether the per-session SQLite database already exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryTruncateResult +public sealed class SessionFsSqliteExistsResult { - /// Failure detail when checkpointCleanupFailed is true. - [JsonPropertyName("checkpointCleanupError")] - public string? CheckpointCleanupError { get; set; } - - /// True when conversation truncation succeeded but post-truncation workspace checkpoint cleanup failed. History is already truncated; callers may still prune snapshots but should report a checkpoint-cleanup rather than a truncation failure. - [JsonPropertyName("checkpointCleanupFailed")] - public bool? CheckpointCleanupFailed { get; set; } - - /// Number of events that were removed. - [JsonPropertyName("eventsRemoved")] - public long EventsRemoved { get; set; } + /// Whether the session database already exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } } -/// Identifier of the event to truncate to; this event and all later events are removed. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryTruncateRequest +/// Identifies the target session. +public sealed class SessionFsSqliteExistsRequest { - /// 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; } -/// A root user turn that the session can rewind to. +/// Canvas open result returned by the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindPoint +public sealed class CanvasProviderOpenResult { - /// 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; + /// Provider-supplied status text. + [JsonPropertyName("status")] + public string? Status { get; set; } - /// Whether this turn itself captured any file changes. - [JsonPropertyName("turnChangedFiles")] - public bool TurnChangedFiles { get; set; } + /// Provider-supplied title. + [JsonPropertyName("title")] + public string? Title { get; set; } - /// User-visible message text for the turn. - [JsonPropertyName("userMessage")] - public string UserMessage { get; set; } = string.Empty; + /// URL for web-rendered canvases. + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// Rewind points and file-change-tracking availability for the session. +/// Host capabilities. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryListRewindPointsResult +public sealed class CanvasHostContextCapabilities { - /// 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; } + /// Whether canvas rendering is supported. + [JsonPropertyName("canvases")] + public bool? Canvases { get; set; } +} - /// Why the listed points could not be produced, when applicable; the points list is empty whenever it is set. `unsupported-remote-session` is permanent for the session and comes with `fileChangeTrackingEnabled: false`. `session-busy` is transient and only ever reported by a session that *is* tracking (`fileChangeTrackingEnabled: true`), because the file-change captures cannot be read while work that may still mutate them is in flight; the same request succeeds once the session settles, so a client that wants points should retry rather than treat it as a failure. It is never `file-change-tracking-disabled`: an untracked local session still lists conversation-only points and reports that through `fileChangeTrackingEnabled: false`. - [JsonPropertyName("unavailableReason")] - public HistoryRewindUnavailableReason? UnavailableReason { get; set; } +/// 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; } } -/// Identifies the target session. +/// Session context supplied by the runtime. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryListRewindPointsRequest +public sealed class CanvasSessionContext { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Active session working directory, when known. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// A file that a conversation-and-files rewind would restore. +/// Canvas open parameters sent to the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindFilePreview +public sealed class CanvasProviderOpenRequest { - /// Aggregate change made across the discarded turns. - [JsonPropertyName("changeType")] - public HistoryRewindChangeType ChangeType { get; set; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Lines added across the discarded turns. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Lines removed across the discarded turns. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } + /// Host context supplied by the runtime. + [JsonPropertyName("host")] + public CanvasHostContext? Host { get; set; } - /// Absolute path of the captured file. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// 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; } -/// Files and aggregate changes for a prospective rewind. +/// Canvas close parameters sent to the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryPreviewRewindResult +public sealed class CanvasProviderCloseRequest { - /// 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; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Number of unique files in the preview. - [JsonPropertyName("fileCount")] - public long FileCount { get; set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Files ordered by path. - [JsonPropertyName("files")] - public IList Files { get => field ??= []; set; } + /// Host context supplied by the runtime. + [JsonPropertyName("host")] + public CanvasHostContext? Host { 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; } -} + /// Canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { 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; + /// 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; } -/// 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. +/// Canvas action invocation parameters sent to the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindResult +public sealed class CanvasProviderInvokeActionRequest { - /// 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; } + /// Action name to invoke. + [JsonPropertyName("actionName")] + public string ActionName { get; set; } = string.Empty; - /// 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; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// 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; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Absolute paths restored to their captured preimages. Always empty for conversation-only rewinds and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`); only conversation-and-files outcomes that reached the file-restore stage populate it. - [JsonPropertyName("restoredFiles")] - public IList RestoredFiles { get => field ??= []; set; } + /// Host context supplied by the runtime. + [JsonPropertyName("host")] + public CanvasHostContext? Host { get; set; } - /// Captured files intentionally left unchanged. Always empty for conversation-only rewinds and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`); only conversation-and-files outcomes that reached the file-restore stage populate it. - [JsonPropertyName("skippedFiles")] - public IList SkippedFiles { get => field ??= []; set; } -} + /// Action input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } -/// Boundary and mode for rewinding session history. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryRewindRequest -{ - /// ID of the user.message event that begins the discarded suffix. - [JsonPropertyName("eventId")] - public string EventId { get; set; } = string.Empty; + /// Canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; - /// Whether to rewind only conversation history or also restore captured files. - [JsonPropertyName("mode")] - public HistoryRewindMode Mode { get; set; } + /// 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; } -/// Indicates whether an in-progress background compaction was cancelled. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryCancelBackgroundCompactionResult +/// Reports a supervised listener's hosting-task termination and cleanup outcome. +public sealed class HostExitedRequest { - /// 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; } + /// Explicit startup or teardown failure, when present. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Process exit status when available; absent for in-process listener tasks. + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Listener UUID. + [JsonPropertyName("hostId")] + public string HostId { get; set; } = string.Empty; + + /// Cause of termination. + [JsonPropertyName("reason")] + public HostExitReason Reason { get; set; } } -/// Identifies the target session. +/// Opaque integrator-owned process launch profile for one extension entrypoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryCancelBackgroundCompactionRequest +public sealed class ExtensionLaunchProfile { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } -/// Indicates whether an in-progress manual compaction was aborted. +/// 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 HistoryAbortManualCompactionResult +public sealed class ExtensionLaunchProviderResolveResult { - /// 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; } + /// Opaque launch profile, omitted when this provider does not support the entrypoint. + [JsonPropertyName("launch")] + public ExtensionLaunchProfile? Launch { get; set; } } -/// Identifies the target session. +/// A discovered extension entrypoint that the registered integrator may classify and resolve to an opaque launch profile. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryAbortManualCompactionRequest +public sealed class ExtensionLaunchProviderResolveRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } } -/// Markdown summary of the conversation context (empty when not available). +/// 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 HistorySummarizeForHandoffResult +public sealed class LlmInferenceHttpRequestStartResult { - /// Markdown summary of the conversation context produced by an LLM. Empty string when there are no messages or when the session does not support local summarization. - [JsonPropertyName("summary")] - public string Summary { get; set; } = string.Empty; } -/// Identifies the target session. +/// The head of an outbound model-layer HTTP request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistorySummarizeForHandoffRequest +public sealed class LlmInferenceHttpRequestStartRequest { - /// Target session identifier. + /// 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; } -/// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. +/// 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 HistoryClearContextResult +public sealed class LlmInferenceHttpRequestChunkResult { - /// 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; } } -/// Parameters for clearing the conversation and seeding the window that replaces it. +/// A request body chunk or cancellation signal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryClearContextRequest +public sealed class LlmInferenceHttpRequestChunkRequest { - /// 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; + /// 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text. + [JsonPropertyName("binary")] + public bool? Binary { get; set; } + + /// When true, the runtime is cancelling the in-flight request (e.g. upstream consumer aborted). `data` is ignored. Implies end-of-request. + [JsonPropertyName("cancel")] + public bool? Cancel { get; set; } + + /// Optional human-readable reason for the cancellation, propagated for logging. + [JsonPropertyName("cancelReason")] + public string? CancelReason { get; set; } + + /// Body byte range. UTF-8 text when `binary` is absent or false; base64-encoded bytes when `binary` is true. May be empty. + [JsonPropertyName("data")] + public string Data { get; set; } = string.Empty; + + /// When true, this is the final body chunk for the request. The SDK may rely on having received an end-marked chunk before treating the request body as complete. + [JsonPropertyName("end")] + public bool? End { get; set; } + + /// Matches the requestId from the originating httpRequestStart frame. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; } -/// User-facing pending queue entry, with kind and display text for a queued message, slash command, or model change. +/// Client environment metadata describing the process that produced a telemetry event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueuePendingItems +public sealed class GitHubTelemetryClientInfo { - /// 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; } + /// Copilot CLI version string. + [JsonPropertyName("cli_version")] + public string CliVersion { 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; + /// Name of the client application. + [JsonPropertyName("client_name")] + public string? ClientName { get; set; } - /// Stable opaque id for the canonical queued item. Batch rows share one id. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Type of client. + [JsonPropertyName("client_type")] + public string? ClientType { get; set; } - /// Whether this item is a queued user message or a queued slash command / model change. - [JsonPropertyName("kind")] - public QueuePendingItemsKind Kind { get; set; } + /// Copilot subscription plan, when known. + [JsonPropertyName("copilot_plan")] + public string? CopilotPlan { 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; } + /// Number of logical CPU cores on the host. + [JsonPropertyName("cpu_count")] + public long? CpuCount { 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; } -} + /// Distinct CPU model names for the host, comma-separated. + [JsonPropertyName("cpu_model")] + public string? CpuModel { get; set; } -/// Snapshot of the session's pending queued items and immediate-steering messages. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueuePendingItemsResult -{ - /// How many leading entries of `steeringMessages` have already been folded into the running turn (and so have an emitted `user.message`), as opposed to still waiting for one. Absent for hosts that do not distinguish the two. - [JsonPropertyName("inFlightSteeringCount")] - public long? InFlightSteeringCount { get; set; } + /// Stable machine identifier for the device. + [JsonPropertyName("dev_device_id")] + public string? DevDeviceId { 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; } + /// Whether the user is a GitHub/Microsoft staff member. + [JsonPropertyName("is_staff")] + public bool? IsStaff { get; 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; } + /// Node.js runtime version string. + [JsonPropertyName("node_version")] + public string NodeVersion { 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; } -} + /// Operating system architecture (e.g. arm64, x64). + [JsonPropertyName("os_arch")] + public string OsArch { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueuePendingItemsRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Operating system platform (e.g. darwin, linux, win32). + [JsonPropertyName("os_platform")] + public string OsPlatform { get; set; } = string.Empty; + + /// Operating system version string. + [JsonPropertyName("os_version")] + public string OsVersion { get; set; } = string.Empty; } -/// Internal snapshot of native queue state for local session orchestration. +/// 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)] -internal sealed class QueueSnapshotResult +public sealed class GitHubTelemetryEvent { - /// Queue item identifier of a model switch that has been dequeued but not yet applied. - [JsonPropertyName("inFlightModelChangeId")] - public string? InFlightModelChangeId { get; set; } + /// Client environment metadata. + [JsonPropertyName("client")] + public GitHubTelemetryClientInfo? Client { get; set; } - /// Insertion orders for queued items, aligned with `items`. - [JsonPropertyName("itemOrders")] - public IList? ItemOrders { get; set; } + /// Copilot tracking ID for user-level attribution. + [JsonPropertyName("copilot_tracking_id")] + public string? CopilotTrackingId { get; set; } - /// User-facing pending items in FIFO order. - [JsonPropertyName("items")] - public IList Items { get => field ??= []; set; } + /// Timestamp when the event was created (ISO 8601 format). + [JsonPropertyName("created_at")] + public string? CreatedAt { get; set; } - /// Insertion orders for immediate steering messages, aligned with `steeringMessages`. - [JsonPropertyName("steeringMessageOrders")] - public IList? SteeringMessageOrders { get; set; } + /// Experiment assignment context. + [JsonPropertyName("exp_assignment_context")] + public string? ExpAssignmentContext { get; set; } - /// Immediate steering messages waiting for an active turn. - [JsonPropertyName("steeringMessages")] - public IList SteeringMessages { get => field ??= []; set; } -} + /// Feature flags enabled for this session, as a map from flag to value. + [JsonPropertyName("features")] + public IDictionary? Features { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueSnapshotRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; -/// Result of moving a queued item. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueMoveItemResult -{ - /// True when the item changed position; false when it was already at the requested position. - [JsonPropertyName("changed")] - public bool Changed { get; set; } + /// Numeric metrics as a map from key to value. + [JsonPropertyName("metrics")] + public IDictionary Metrics { get => field ??= new Dictionary(); set; } + + /// Reference to the model call that produced this event. + [JsonPropertyName("model_call_id")] + public string? ModelCallId { get; set; } + + /// String-valued properties as a map from key to value. + [JsonPropertyName("properties")] + public IDictionary Properties { get => field ??= new Dictionary(); set; } + + /// Session identifier the event belongs to. + [JsonPropertyName("session_id")] + public string? SessionId { get; set; } } -/// Parameters for moving a queued item by stable id. +/// 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)] -internal sealed class QueueMoveItemRequest +public sealed class GitHubTelemetryNotification { - /// Stable opaque queued-item id. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. + [JsonPropertyName("event")] + public GitHubTelemetryEvent Event { get => field ??= new(); set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } - /// Zero-based target position in the public visible queue. Values outside the queue clamp to an end. - [JsonPropertyName("toPosition")] - public long ToPosition { 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; } } -/// Result of inserting a queued message. +/// SDK host response to a GitHub credential request. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueInsertAtResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(GitHubTokenAcquireResultToken), "token")] +[JsonDerivedType(typeof(GitHubTokenAcquireResultCancelled), "cancelled")] +public partial class GitHubTokenAcquireResult { - /// Fresh stable opaque id assigned to the inserted item. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Serializable message fields accepted by queue.insertAt. + +/// The token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueInsertMessage +public partial class GitHubTokenAcquireResultToken : GitHubTokenAcquireResult { - /// 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; } - - /// Accepted for SendOptions compatibility but ignored; inserted items always use queued delivery semantics. - [JsonPropertyName("mode")] - public SendMode? Mode { get; set; } - - /// Accepted for SendOptions compatibility but ignored; the requested public position controls placement. - [JsonPropertyName("prepend")] - public bool? Prepend { get; set; } - - /// The user message text. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "token"; - /// Per-turn request headers. - [JsonPropertyName("requestHeaders")] - public IDictionary? RequestHeaders { get; set; } + /// GitHub access token acquired by the SDK host. + [JsonPropertyName("accessToken")] + public required string AccessToken { get; set; } - /// Required tool name for the turn, when any. - [JsonPropertyName("requiredTool")] - public string? RequiredTool { 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; } - /// 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; } + /// OAuth token type. Defaults to bearer when omitted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenType")] + public string? TokenType { get; set; } +} - /// Accepted for SendOptions compatibility but ignored; insertion scheduling is controlled by the queue drain state. - [JsonPropertyName("wait")] - public bool? Wait { get; set; } +/// The cancelled variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class GitHubTokenAcquireResultCancelled : GitHubTokenAcquireResult +{ + /// + [JsonIgnore] + public override string Kind => "cancelled"; } -/// Parameters for inserting a queued message at a public visible position. +/// Asks the SDK client to acquire a GitHub access token from an opaque callback registration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueInsertAtRequest +public sealed class GitHubTokenAcquireRequest { - /// Queued message contents and delivery metadata. - [JsonPropertyName("message")] - public QueueInsertMessage Message { get => field ??= new(); set; } + /// Effective GitHub host for which the callback must return a token. + [JsonPropertyName("host")] + public string Host { 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; } + /// Why the runtime is requesting a GitHub credential. + [JsonPropertyName("reason")] + public GitHubTokenAcquireReason Reason { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Opaque identifier generated by the SDK for this callback registration. + [JsonPropertyName("registrationId")] + public string RegistrationId { get; set; } = string.Empty; -/// Result of removing a queued item. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueRemoveAtResult + /// Session receiving the token. Absent only before a cloud session has been assigned its id. + [JsonPropertyName("sessionId")] + public string? SessionId { get; set; } +} + +/// A response is meaningful only on the connection and request that issued its challenge. +public sealed class InstallationsConfirmResult { - /// True when the addressed item was removed. - [JsonPropertyName("removed")] - public bool Removed { get; set; } + /// Exact challenge from the request. + [JsonPropertyName("confirmationId")] + public string ConfirmationId { get; set; } = string.Empty; + + /// Fresh explicit user decision. There is no default. + [JsonPropertyName("decision")] + public InstallationDecision Decision { get; set; } + + /// Exact review commitment from the request. + [JsonPropertyName("reviewFingerprint")] + public string ReviewFingerprint { get; set; } = string.Empty; } -/// Parameters for removing a queued item by stable id. +/// 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 QueueRemoveAtRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "resource", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(InstallationReviewMcp), "mcp")] +[JsonDerivedType(typeof(InstallationReviewSkill), "skill")] +public partial class InstallationReview { - /// 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 type discriminator. + [JsonPropertyName("resource")] + public virtual string Resource { get; set; } = string.Empty; } -/// Result of editing a queued message. + +/// 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)] -public sealed class QueueUpdateTextResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "action", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpInstallationReviewInstall), "install")] +[JsonDerivedType(typeof(McpInstallationReviewUninstall), "uninstall")] +public partial class McpInstallationReview { - /// True when the stored text changed. - [JsonPropertyName("updated")] - public bool Updated { get; set; } + /// The type discriminator. + [JsonPropertyName("action")] + public virtual string Action { get; set; } = string.Empty; } -/// Parameters for editing a single queued message. + +/// +/// 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)] -internal sealed class QueueUpdateTextRequest +public sealed class McpInstallationRemoteConfiguration { - /// Optional replacement prompt displayed to the user. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// + /// 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; } - /// Stable opaque ID of the queued item to edit. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Configured tool selection, not permission to invoke those tools. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } - /// Replacement prompt sent to the model. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Transport in the effective persisted remote configuration. + [JsonPropertyName("transport")] + public McpPlanRemoteTransport Transport { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Exact resolved endpoint, without templates or secret placeholders. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; } -/// Result of withdrawing a user message. +/// The install variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueWithdrawMessageResult +public partial class McpInstallationReviewInstall : McpInstallationReview { - /// 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; } + /// + [JsonIgnore] + public override string Action => "install"; - /// True when the message left the queue or, for a running turn, history. - [JsonPropertyName("removed")] - public bool Removed { get; set; } -} + /// Catalogue identity retained from the bound candidate when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("catalogue")] + public InstallationCatalogueIdentity? Catalogue { get; set; } -/// Conditional withdrawal of a single user message, from its queue or from the running turn it started. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueWithdrawMessageRequest -{ - /// 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; + /// Original catalogue trust metadata, not a verification claim. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("catalogueTrust")] + public CatalogTrustSnapshot? CatalogueTrust { 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 configuration change for the selected alternative only. + [JsonPropertyName("configurationChange")] + public required McpPlanConfigurationChange ConfigurationChange { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = 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; } -/// 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; } + /// Identity from the retained plan, not caller display text. + [JsonPropertyName("identity")] + public required McpPlanResourceIdentity Identity { 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; } + /// Non-secret values supplied for this selected alternative. + [JsonPropertyName("inputs")] + public required IList Inputs { get; set; } - /// Display text to append to the existing preview after a blank line. - [JsonPropertyName("displayPrompt")] - public string DisplayPrompt { get; set; } = string.Empty; + /// Policy decision bound to this plan. + [JsonPropertyName("policy")] + public required McpPlanPolicyResult Policy { 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; + /// Original source identity and content commitment. + [JsonPropertyName("provenance")] + public required McpPlanProvenance Provenance { 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; + /// Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. + [JsonPropertyName("secretStorage")] + public required McpInstallationSecretStorage SecretStorage { get; set; } - /// Text to append after a blank line. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Only the selected alternative is applied. + [JsonPropertyName("selectedChoice")] + public required McpPlanTransportChoice SelectedChoice { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Exact reviewed placeholders supplied separately. Never secret values. + [JsonPropertyName("suppliedSecrets")] + public required IList SuppliedSecrets { get; set; } -/// Result of duplicating a queued item. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueDuplicateAtResult -{ - /// Fresh stable opaque id assigned to the duplicate. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Exact reviewed user-scope destination. + [JsonPropertyName("target")] + public required McpPlanTarget Target { get; set; } } -/// Parameters for duplicating a queued item. +/// The uninstall variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueDuplicateAtRequest +public partial class McpInstallationReviewUninstall : McpInstallationReview { - /// Stable opaque ID of the queued item to duplicate. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Action => "uninstall"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Identity from the installed receipt. + [JsonPropertyName("identity")] + public required McpPlanResourceIdentity Identity { get; set; } -/// 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 QueueSetDrainPausedRequest -{ - /// Whether queued-lane draining should be paused. - [JsonPropertyName("paused")] - public bool Paused { get; set; } + /// Receipt-owned installation being removed. + [JsonPropertyName("installationId")] + public required string InstallationId { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Exact planner-owned secret slots to remove, excluding shared OAuth grants. + [JsonPropertyName("ownedSecretCount")] + public required long OwnedSecretCount { get; set; } -/// Result of trying to steer a queued message into a live turn. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueSendNowResult -{ - /// True when the item was accepted into the steering lane; false when no main turn was live. - [JsonPropertyName("steered")] - public bool Steered { get; set; } -} + /// Current removal policy, independent of permission to activate the server. + [JsonPropertyName("policy")] + public required McpPlanPolicyResult Policy { get; set; } -/// Parameters for steering a queued message into a live turn. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -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; + /// 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 the native queue has pending work. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueHasPendingResult -{ - /// True when queued or immediate native work is pending. - [JsonPropertyName("hasPending")] - public bool HasPending { get; set; } -} + /// Whether uninstall restores a protected pre-install configuration. + [JsonPropertyName("restoresPreviousConfiguration")] + public required bool RestoresPreviousConfiguration { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueHasPendingRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Exact destination, checked for intervening changes before mutation. + [JsonPropertyName("target")] + public required McpPlanTarget Target { get; set; } } -/// Whether a deferred-idle drain should run. +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueBeginDeferredIdleDrainResult +public partial class InstallationReviewMcp : InstallationReview { - /// True when the host should run finishDeferredIdleDrain asynchronously. - [JsonPropertyName("shouldDrain")] - public bool ShouldDrain { get; set; } + /// + [JsonIgnore] + public override string Resource => "mcp"; + + /// The exact MCP action and its reviewed changes. + [JsonPropertyName("review")] + public required McpInstallationReview Review { get; set; } } -/// Inputs for starting a deferred-idle drain. +/// The skill variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueBeginDeferredIdleDrainRequest +public partial class InstallationReviewSkill : InstallationReview { - /// Whether the host still has active background work. - [JsonPropertyName("activeBackgroundWork")] - public bool ActiveBackgroundWork { 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; } } -/// Action selected by the native deferred-idle drain. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueFinishDeferredIdleDrainResult +/// One connection-owned, expiring request for a trusted host's explicit user decision. +public sealed class InstallationsConfirmRequest { - /// Whether the deferred idle was caused by an aborted foreground turn. - [JsonPropertyName("aborted")] - public bool Aborted { get; set; } + /// Opaque one-use challenge. Return unchanged; never log or persist. + [JsonPropertyName("confirmationId")] + public string ConfirmationId { get; set; } = string.Empty; - /// One of none, processQueue, or emitSessionIdle. - [JsonPropertyName("action")] - public string Action { get; set; } = string.Empty; + /// Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. + [JsonPropertyName("expiresAt")] + public string ExpiresAt { get; set; } = string.Empty; + + /// Random identifier of this installation operation, not a plan handle. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; + + /// + /// Original engine-resolved selector for a bound operation, never a dispatch default. + /// Bound MCP confirmation always includes it; correlate it with the original pending action. + /// + [JsonPropertyName("policySessionId")] + public string? PolicySessionId { get; set; } + + /// Resource-specific review to present before collecting the user's decision. + [JsonPropertyName("review")] + public InstallationReview Review { get => field ??= new(); set; } + + /// Opaque commitment to the exact review and inputs. Return unchanged; never log. + [JsonPropertyName("reviewFingerprint")] + public string ReviewFingerprint { get; set; } = string.Empty; } -/// Inputs for completing a deferred-idle drain. +/// Closed set of public task kinds a connection can negotiate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueFinishDeferredIdleDrainRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskKind : IEquatable { - /// Whether the host still has active background work. - [JsonPropertyName("activeBackgroundWork")] - public bool ActiveBackgroundWork { get; set; } + private readonly string? _value; - /// Whether native queued work remains. - [JsonPropertyName("hasPending")] - public bool HasPending { 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; + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runtime-owned background agent task. + public static TaskKind Agent { get; } = new("agent"); + + /// Runtime-owned shell task. + public static TaskKind Shell { get; } = new("shell"); + + /// Client-owned externally executed task. + public static TaskKind Client { get; } = new("client"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskKind left, TaskKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskKind left, TaskKind right) => !(left == right); + + /// + 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)); + } + } } -/// Inputs for marking session.idle deferred in native state. + +/// GitHub Mission Control compute kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueDeferSessionIdleRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EnvironmentKind : IEquatable { - /// Whether the deferred idle was caused by an aborted foreground turn. - [JsonPropertyName("aborted")] - public bool Aborted { get; set; } + 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 EnvironmentKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A user-managed environment on a local machine. + public static EnvironmentKind UserLocal { get; } = new("user-local"); + + /// A user-managed environment in a GitHub Codespace. + public static EnvironmentKind UserCodespace { get; } = new("user-codespace"); + + /// A GitHub-managed environment backed by GitHub Actions. + public static EnvironmentKind ManagedActions { get; } = new("managed-actions"); + + /// A GitHub-managed sandbox environment. + public static EnvironmentKind ManagedSandbox { get; } = new("managed-sandbox"); + + /// A GitHub-managed cloud coding agent environment. + public static EnvironmentKind ManagedCca { get; } = new("managed-cca"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EnvironmentKind left, EnvironmentKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EnvironmentKind left, EnvironmentKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EnvironmentKind other && Equals(other); + + /// + public bool Equals(EnvironmentKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override EnvironmentKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EnvironmentKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EnvironmentKind)); + } + } } -/// Indicates whether a user-facing pending item was removed. + +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueRemoveMostRecentResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookType : IEquatable { - /// 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; } + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runs before a tool is invoked. + public static HookType PreToolUse { get; } = new("preToolUse"); + + /// Runs before an MCP tool is invoked. + public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); + + /// Runs after a tool completes successfully. + public static HookType PostToolUse { get; } = new("postToolUse"); + + /// Runs after a tool fails. + public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + + /// Runs after the user submits a prompt. + public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + public static HookType UserPromptTransformed { get; } = new("userPromptTransformed"); + + /// Runs when a session starts. + public static HookType SessionStart { get; } = new("sessionStart"); + + /// Runs when a session ends. + public static HookType SessionEnd { get; } = new("sessionEnd"); + + /// Runs after an agent result is produced. + public static HookType PostResult { get; } = new("postResult"); + + /// Runs before a pull request description is generated. + public static HookType PrePRDescription { get; } = new("prePRDescription"); + + /// Runs when the agent encounters an error. + public static HookType ErrorOccurred { get; } = new("errorOccurred"); + + /// Runs when the agent stops. + public static HookType AgentStop { get; } = new("agentStop"); + + /// Runs when a subagent starts. + public static HookType SubagentStart { get; } = new("subagentStart"); + + /// Runs when a subagent stops. + public static HookType SubagentStop { get; } = new("subagentStop"); + + /// Runs before conversation context is compacted. + public static HookType PreCompact { get; } = new("preCompact"); + + /// Runs when the agent requests permission. + public static HookType PermissionRequest { get; } = new("permissionRequest"); + + /// Runs when the agent emits a notification. + public static HookType Notification { get; } = new("notification"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookType left, HookType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookType left, HookType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookType other && Equals(other); + + /// + public bool Equals(HookType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override HookType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + } + } } -/// Identifies the target session. + +/// Configuration tier that contributed a discovered hook action. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueRemoveMostRecentRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookOrigin : IEquatable { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookOrigin(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Hook loaded from user settings or the user's hook directory. + public static HookOrigin User { get; } = new("user"); + + /// Hook loaded from repository settings or the repository hook directory. + public static HookOrigin Repository { get; } = new("repository"); + + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + public static HookOrigin Plugin { get; } = new("plugin"); + + /// Hook enforced by centrally managed policy. + public static HookOrigin Policy { get; } = new("policy"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookOrigin left, HookOrigin right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookOrigin left, HookOrigin right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookOrigin other && Equals(other); + + /// + public bool Equals(HookOrigin other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override HookOrigin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookOrigin value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + } + } } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueClearRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} -/// Internal filter for consuming queued system notifications. +/// Resolved Anthropic adaptive-thinking capability for a model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueConsumeSystemNotificationsRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AdaptiveThinkingSupport : IEquatable { - /// Opaque runtime-owned filter object. - [JsonPropertyName("filter")] - public JsonElement Filter { get; set; } + 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 AdaptiveThinkingSupport(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// Result of enqueueing the resume-pending wake item. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueEnqueueResumePendingResult -{ - /// True when a wake item was newly queued. - [JsonPropertyName("queued")] - public bool Queued { 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 SessionQueueEnqueueResumePendingRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// The model does not accept thinking.type='adaptive'. + public static AdaptiveThinkingSupport Unsupported { get; } = new("unsupported"); -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueProcessRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// The model accepts adaptive thinking but also accepts thinking.type='enabled'. + public static AdaptiveThinkingSupport Optional { get; } = new("optional"); -/// Cursor, batch size, and optional long-poll/filter parameters for reading session events. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EventLogReadRequest -{ - /// 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; } + /// 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"); - /// 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; } + /// 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"); - /// 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; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => left.Equals(right); - /// 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; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => !(left == right); - /// 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; } + /// + public override bool Equals(object? obj) => obj is AdaptiveThinkingSupport other && Equals(other); - /// Maximum number of events to return in this batch (1–1000, default 200). - [JsonPropertyName("max")] - public long? Max { get; set; } + /// + public bool Equals(AdaptiveThinkingSupport 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); - /// Either '*' to receive all event types, or a non-empty list of event types to receive. - [JsonPropertyName("types")] - public JsonElement? Types { get; set; } + /// + public override string ToString() => Value; - /// 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; } -} + /// 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)); + } -/// Snapshot of the current tail cursor without returning any events. Use this when a consumer wants to subscribe to live events going forward without first paginating through the entire persisted history (which would happen if `read` were called without a cursor on a long-lived session). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class EventLogTailResult -{ - /// 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; + /// + public override void Write(Utf8JsonWriter writer, AdaptiveThinkingSupport value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AdaptiveThinkingSupport)); + } + } } -/// 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; -} -/// Opaque handle representing an event-type interest registration. +/// Model capability category for grouping in the model picker. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RegisterEventInterestResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPickerCategory : IEquatable { - /// 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; -} + private readonly string? _value; -/// Event type to register consumer interest for, used by runtime gating logic. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -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; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPickerCategory(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// Indicates whether the operation succeeded. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class EventLogReleaseInterestResult -{ - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Lightweight model category optimized for faster, lower-cost interactions. + public static ModelPickerCategory Lightweight { get; } = new("lightweight"); -/// Opaque handle previously returned by `registerInterest` to release. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ReleaseEventInterestParams -{ - /// Handle returned by a previous `registerInterest` call. Idempotent: releasing an unknown or already-released handle is a no-op (returns success). When the last outstanding handle for an event type is released, the runtime reverts to its 'no consumer' code path for that event type. - [JsonPropertyName("handle")] - public string Handle { get; set; } = string.Empty; + /// Versatile model category suitable for a broad range of tasks. + public static ModelPickerCategory Versatile { get; } = new("versatile"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Powerful model category optimized for complex tasks. + public static ModelPickerCategory Powerful { get; } = new("powerful"); -/// Request count and cost metrics for this model. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricRequests -{ - /// User-initiated premium request cost (with multiplier applied). - [JsonPropertyName("cost")] - public double Cost { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPickerCategory left, ModelPickerCategory right) => left.Equals(right); - /// Number of API requests made with this model. - [JsonPropertyName("count")] - public long Count { get; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPickerCategory left, ModelPickerCategory right) => !(left == right); -/// 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; } -} + /// + public override bool Equals(object? obj) => obj is ModelPickerCategory other && Equals(other); -/// 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; } + /// + public bool Equals(ModelPickerCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Total tokens written to prompt cache. - [JsonPropertyName("cacheWriteTokens")] - public long CacheWriteTokens { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Total input tokens consumed. - [JsonPropertyName("inputTokens")] - public long InputTokens { get; set; } + /// + public override string ToString() => Value; - /// Total output tokens produced. - [JsonPropertyName("outputTokens")] - public long OutputTokens { 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)); + } - /// Total output tokens used for reasoning. - [JsonPropertyName("reasoningTokens")] - public long? ReasoningTokens { get; set; } + /// + public override void Write(Utf8JsonWriter writer, ModelPickerCategory value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerCategory)); + } + } } -/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. + +/// Relative cost tier for token-based billing users. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetric +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPickerPriceCategory : IEquatable { - /// 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; } + private readonly string? _value; - /// Request count and cost metrics for this model. - [JsonPropertyName("requests")] - public UsageMetricsModelMetricRequests Requests { get => field ??= new(); set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPickerPriceCategory(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Token count details per type. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Accumulated nano-AI units cost for this model. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { get; set; } + /// Lowest relative token cost tier. + public static ModelPickerPriceCategory Low { get; } = new("low"); - /// Token usage metrics for this model. - [JsonPropertyName("usage")] - public UsageMetricsModelMetricUsage Usage { get => field ??= new(); set; } -} + /// Medium relative token cost tier. + public static ModelPickerPriceCategory Medium { get; } = new("medium"); -/// 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; } + /// High relative token cost tier. + public static ModelPickerPriceCategory High { get; } = new("high"); - /// Configured agent name, when this is a subagent. - [JsonPropertyName("agentName")] - public string? AgentName { get; set; } + /// Highest relative token cost tier. + public static ModelPickerPriceCategory VeryHigh { get; } = new("very_high"); - /// Per-model usage for this agent, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => left.Equals(right); - /// Time spent in model API calls by this agent, in milliseconds. - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => !(left == right); - /// Accumulated nano-AI units cost for this agent. - [JsonPropertyName("totalNanoAiu")] - public double TotalNanoAiu { get; set; } -} + /// + public override bool Equals(object? obj) => obj is ModelPickerPriceCategory other && Equals(other); -/// Aggregated code change metrics. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsCodeChanges -{ - /// Distinct file paths modified during the session. - [JsonPropertyName("filesModified")] - public IList FilesModified { get => field ??= []; set; } + /// + public bool Equals(ModelPickerPriceCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Number of distinct files modified. - [JsonPropertyName("filesModifiedCount")] - public long FilesModifiedCount { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Total lines of code added. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } + /// + public override string ToString() => Value; - /// Total lines of code removed. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } -} + /// 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)); + } -/// 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; } + /// + public override void Write(Utf8JsonWriter writer, ModelPickerPriceCategory value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerPriceCategory)); + } + } } -/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. + +/// Current policy state for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageGetMetricsResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPolicyState : IEquatable { - /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. - [JsonPropertyName("agentMetrics")] - public IDictionary? AgentMetrics { get; set; } + private readonly string? _value; - /// Aggregated code change metrics. - [JsonPropertyName("codeChanges")] - public UsageMetricsCodeChanges CodeChanges { get => field ??= new(); set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPolicyState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Currently active model identifier. - [JsonPropertyName("currentModel")] - public string? CurrentModel { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Input tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallInputTokens")] - public long LastCallInputTokens { get; set; } + /// The model is enabled by policy. + public static ModelPolicyState Enabled { get; } = new("enabled"); - /// Output tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallOutputTokens")] - public long LastCallOutputTokens { get; set; } + /// The model is disabled by policy. + public static ModelPolicyState Disabled { get; } = new("disabled"); - /// Per-model token and request metrics, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + /// No explicit policy is configured for the model. + public static ModelPolicyState Unconfigured { get; } = new("unconfigured"); - /// ISO 8601 timestamp when the session started. - [JsonPropertyName("sessionStartTime")] - public DateTimeOffset SessionStartTime { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPolicyState left, ModelPolicyState right) => left.Equals(right); - /// Session-wide per-token-type accumulated token counts. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPolicyState left, ModelPolicyState right) => !(left == right); - /// Total time spent in model API calls (milliseconds). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// + public override bool Equals(object? obj) => obj is ModelPolicyState other && Equals(other); - /// Session-wide accumulated nano-AI units cost. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { get; set; } + /// + public bool Equals(ModelPolicyState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). - [JsonPropertyName("totalPremiumRequestCost")] - public double TotalPremiumRequestCost { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Raw count of user-initiated API requests. - [JsonPropertyName("totalUserRequests")] - public long TotalUserRequests { get; set; } -} + /// + public override string ToString() => Value; -/// 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; -} + /// 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)); + } -/// 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; + /// + public override void Write(Utf8JsonWriter writer, ModelPolicyState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPolicyState)); + } + } } -/// Baseline data provenance for a prediction. +/// 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 SessionLimitPredictionBaselineData +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelProviderKind : IEquatable { - /// End of the baseline data slice. - [JsonPropertyName("windowEnd")] - public string WindowEnd { get; set; } = string.Empty; + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelProviderKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// 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"); + + /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. + public static ModelProviderKind Loki { get; } = new("loki"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderKind left, ModelProviderKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderKind left, ModelProviderKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelProviderKind other && Equals(other); + + /// + public bool Equals(ModelProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Start of the baseline data slice. - [JsonPropertyName("windowStart")] - public string WindowStart { get; set; } = string.Empty; -} + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); -/// 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; } + /// + public override string ToString() => Value; - /// Semantic usage tier. - [JsonPropertyName("tier")] - public SessionLimitPredictionTier Tier { 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)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelProviderKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderKind)); + } + } } -/// Explainable AI-credit session-limit prediction. + +/// State of the persistent certificate authority of the sandbox credential proxy. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLimitPredictionDetails +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SandboxProxyCaState : IEquatable { - /// Baseline data provenance. - [JsonPropertyName("baselineData")] - public SessionLimitPredictionBaselineData BaselineData { get => field ??= new(); set; } + private readonly string? _value; - /// Client population used for the prediction. - [JsonPropertyName("clientType")] - public SessionLimitPredictionClientType ClientType { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SandboxProxyCaState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Resolved model family when known. - [JsonPropertyName("family")] - public string? Family { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Model identifier used for lookup. - [JsonPropertyName("modelId")] - public string ModelId { get; set; } = string.Empty; + /// This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. + public static SandboxProxyCaState Unsupported { get; } = new("unsupported"); - /// Recommended maximum AI credits for this session. - [JsonPropertyName("recommendedCap")] - public double RecommendedCap { get; set; } + /// OS trust does not include the certificate authority, or none is stored. + public static SandboxProxyCaState NotInstalled { get; } = new("notInstalled"); - /// Tier chosen as the recommended cap. - [JsonPropertyName("recommendedTier")] - public SessionLimitPredictionTier RecommendedTier { get; set; } + /// OS trust includes the stored certificate authority. + public static SandboxProxyCaState Installed { get; } = new("installed"); - /// Baseline fallback level used to create the prediction. - [JsonPropertyName("source")] - public SessionLimitPredictionSource Source { get; set; } + /// The runtime could not read the certificate authority or the OS trust store. + public static SandboxProxyCaState Error { get; } = new("error"); - /// Key matched at the source level, such as a model id, family id, or `global`. - [JsonPropertyName("sourceKey")] - public string SourceKey { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SandboxProxyCaState left, SandboxProxyCaState right) => left.Equals(right); - /// Ordered usage tiers and their AI-credit caps. - [JsonPropertyName("tiers")] - public IList Tiers { get => field ??= []; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SandboxProxyCaState left, SandboxProxyCaState right) => !(left == right); -/// The available variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionLimitPredictionResultAvailable : SessionLimitPredictionResult -{ /// - [JsonIgnore] - public override string Kind => "available"; + public override bool Equals(object? obj) => obj is SandboxProxyCaState other && Equals(other); - /// Predicted session limit details. - [JsonPropertyName("prediction")] - public required SessionLimitPredictionDetails Prediction { get; set; } -} + /// + public bool Equals(SandboxProxyCaState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// The unavailable variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionLimitPredictionResultUnavailable : SessionLimitPredictionResult -{ /// - [JsonIgnore] - public override string Kind => "unavailable"; + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Reason no prediction is available. - [JsonPropertyName("reason")] - public required SessionLimitPredictionUnavailableReason Reason { get; set; } -} + /// + public override string ToString() => Value; -/// 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; } + /// 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)); + } - /// Optional model identifier override. If omitted, the session's current model is used. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// + public override void Write(Utf8JsonWriter writer, SandboxProxyCaState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SandboxProxyCaState)); + } + } } -/// RPC data type for SessionLimitPredictionPredictRequestWithSession operations. + +/// Server transport type: stdio, http, sse (deprecated), or memory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionLimitPredictionPredictRequestWithSession +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct DiscoveredMcpServerType : IEquatable { - /// Client type to size for. Defaults to `cli-interactive`. - [JsonPropertyName("clientType")] - public SessionLimitPredictionClientType? ClientType { get; set; } + private readonly string? _value; - /// Optional model identifier override. If omitted, the session's current model is used. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public DiscoveredMcpServerType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// GitHub URL for the session and a flag indicating whether remote steering is enabled. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteEnableResult -{ - /// Whether remote steering is enabled. - [JsonPropertyName("remoteSteerable")] - public bool RemoteSteerable { get; set; } + /// Server communicates over stdio with a local child process. + public static DiscoveredMcpServerType Stdio { get; } = new("stdio"); - /// GitHub frontend URL for this session. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("url")] - public string? Url { get; set; } -} + /// Server communicates over streamable HTTP. + public static DiscoveredMcpServerType Http { get; } = new("http"); -/// Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class RemoteEnableRequest -{ - /// Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. - [JsonPropertyName("mode")] - public RemoteSessionMode? Mode { get; set; } + /// Server communicates over Server-Sent Events (deprecated). + public static DiscoveredMcpServerType Sse { get; } = new("sse"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Server is backed by an in-memory runtime implementation. + public static DiscoveredMcpServerType Memory { get; } = new("memory"); -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionRemoteDisableRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => left.Equals(right); -/// 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 -{ -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => !(left == right); -/// 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; } + /// + public override bool Equals(object? obj) => obj is DiscoveredMcpServerType other && Equals(other); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(DiscoveredMcpServerType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// Current sharing status and shareable GitHub URL for a session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class VisibilityGetResult -{ - /// Shareable GitHub URL for the session. Present when the session is synced and the URL can be resolved. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("shareUrl")] - public string? ShareUrl { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(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; } + /// + public override string ToString() => 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; } -} + /// 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)); + } -/// 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; + /// + public override void Write(Utf8JsonWriter writer, DiscoveredMcpServerType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredMcpServerType)); + } + } } -/// Effective sharing status and shareable GitHub URL after updating session visibility. + +/// 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 VisibilitySetResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CatalogCapability : 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; - /// 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; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CatalogCapability(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 visibility change could not be applied and `status`/`shareUrl` are absent. - [JsonPropertyName("synced")] - public bool Synced { get; set; } -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// 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; + /// Understands the current `application/mcp-server-card+json` media type. + public static CatalogCapability McpServerCard { get; } = new("mcp-server-card"); - /// 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; } -} + /// Understands the legacy `application/mcp-server+json` media type. + public static CatalogCapability LegacyMcpServerCard { get; } = new("legacy-mcp-server-card"); -/// 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 ScheduleEntry -{ - /// Absolute fire time (epoch milliseconds) for a one-shot calendar schedule. - [JsonPropertyName("at")] - public long? At { get; set; } + /// Understands `application/ai-skill` candidates as discovery-only and typed non-installable. + public static CatalogCapability AiSkillDiscovery { get; } = new("ai-skill-discovery"); - /// 5-field cron expression for a recurring calendar schedule, evaluated in `tz`. - [JsonPropertyName("cron")] - public string? Cron { 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"); - /// 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; } + /// 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"); - /// Sequential id assigned by the runtime within the session. Stable across resumes (rebuilt from the event log). - [JsonPropertyName("id")] - public long Id { get; set; } + /// Understands plans that enumerate every eligible transport rather than a single preferred one. + public static CatalogCapability MultipleTransportChoice { get; } = new("multiple-transport-choice"); - /// Interval between scheduled ticks, in milliseconds (relative-interval schedules). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("intervalMs")] - public TimeSpan? Interval { get; set; } + /// 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"); - /// ISO 8601 timestamp when the next tick is scheduled to fire. - [JsonPropertyName("nextRunAt")] - public DateTimeOffset NextRunAt { 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"); - /// Prompt text that gets enqueued on every tick. - [JsonPropertyName("prompt")] - public string Prompt { 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"); - /// Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`). - [JsonPropertyName("recurring")] - public bool Recurring { 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"); - /// 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; } + /// 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"); - /// IANA timezone the `cron` expression is evaluated in. - [JsonPropertyName("tz")] - public string? Tz { get; set; } -} + /// 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"); -/// 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; } -} + /// 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"); -/// 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; -} + /// 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"); -/// 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; -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogCapability left, CatalogCapability right) => left.Equals(right); -/// 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; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogCapability left, CatalogCapability right) => !(left == right); -/// 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 bool Equals(object? obj) => obj is CatalogCapability other && Equals(other); + + /// + public bool Equals(CatalogCapability 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 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)); + } + } } -/// Result of registering or re-arming a scheduled prompt. + +/// Whether a planned configuration change would create or modify an entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanConfigurationOperation : IEquatable { - /// The registered or updated schedule entry. - [JsonPropertyName("entry")] - public ScheduleEntry? Entry { get; set; } + private readonly string? _value; - /// User-facing validation error, when registration failed. - [JsonPropertyName("error")] - public string? Error { 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; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Creates a configuration entry that does not exist yet. + public static McpPlanConfigurationOperation Add { get; } = new("add"); + + /// Modifies a configuration entry that already exists. + public static McpPlanConfigurationOperation Update { get; } = new("update"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpPlanConfigurationOperation other && Equals(other); + + /// + public bool Equals(McpPlanConfigurationOperation 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 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)); + } + } } -/// Register a relative-interval scheduled prompt. + +/// Configuration scope an MCP install plan targets. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanScope : 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 McpPlanScope(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; + + /// The user's own MCP configuration. + public static McpPlanScope User { get; } = new("user"); - /// Whether the schedule should re-arm after each tick. Defaults to true. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanScope left, McpPlanScope 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 !=(McpPlanScope left, McpPlanScope right) => !(left == right); -/// 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; + /// + public override bool Equals(object? obj) => obj is McpPlanScope other && Equals(other); - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// + public bool Equals(McpPlanScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Whether the schedule should re-arm after each tick. Defaults to true. - [JsonPropertyName("recurring")] - public bool? Recurring { 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 McpPlanScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// IANA timezone for evaluating the cron expression. - [JsonPropertyName("tz")] - public string? Tz { get; set; } + /// + public override void Write(Utf8JsonWriter writer, McpPlanScope value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScope)); + } + } } -/// Register an absolute-time scheduled prompt. + +/// What policy decided for a planned server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddAtRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanPolicyDecision : IEquatable { - /// Epoch milliseconds when the prompt should fire. - [JsonPropertyName("at")] - public long At { get; set; } - - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + private readonly string? _value; - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanPolicyDecision(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Whether the schedule should re-arm after each tick. Defaults to false. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Policy permits the server. + public static McpPlanPolicyDecision Allowed { get; } = new("allowed"); -/// 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; } + /// Policy forbids the server, so the plan cannot be applied. + public static McpPlanPolicyDecision Blocked { get; } = new("blocked"); - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Policy permits the server only after an explicit approval. + public static McpPlanPolicyDecision RequiresApproval { get; } = new("requires-approval"); - /// 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 ==(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => left.Equals(right); -/// 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; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => !(left == right); - /// Id of the self-paced scheduled prompt. - [JsonPropertyName("id")] - public long Id { get; set; } + /// + public override bool Equals(object? obj) => obj is McpPlanPolicyDecision other && Equals(other); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(McpPlanPolicyDecision other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// 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 int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); -/// 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 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 McpPlanPolicyDecision Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } -/// 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 void Write(Utf8JsonWriter writer, McpPlanPolicyDecision value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicyDecision)); + } + } } -/// Asks the SDK client to acquire a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request; the runtime does no caching, so this is sent once per request. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderTokenAcquireRequest -{ - /// Name of the BYOK provider needing a token. For the legacy whole-session provider this is the implicit provider name; for named providers it is the configured provider name. - [JsonPropertyName("providerName")] - public string ProviderName { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} -/// Result returned by an extension factory closure. +/// Which authority produced a policy decision. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryExecuteResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanPolicySource : IEquatable { - /// Factory result value. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } -} + private readonly string? _value; -/// Parameters sent to the owning extension to execute a factory closure. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryExecuteRequest -{ - /// Factory input value. - [JsonPropertyName("args")] - public JsonElement Args { 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 token identifying this factory execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Registered factory name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// No policy applied, so the server is permitted by default. + public static McpPlanPolicySource None { get; } = new("none"); - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// An enterprise allowlist evaluated the server. + public static McpPlanPolicySource EnterpriseAllowlist { get; } = new("enterprise-allowlist"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// The registry the card came from evaluated the server. + public static McpPlanPolicySource RegistryPolicy { get; } = new("registry-policy"); -/// Parameters for cooperatively aborting a factory body. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryAbortRequest -{ - /// Opaque token identifying the execution attempt to abort. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Local trust settings evaluated the server. + public static McpPlanPolicySource LocalTrust { get; } = new("local-trust"); - /// Factory run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanPolicySource left, McpPlanPolicySource 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 !=(McpPlanPolicySource left, McpPlanPolicySource right) => !(left == right); -/// 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; } -} + /// + public override bool Equals(object? obj) => obj is McpPlanPolicySource other && Equals(other); -/// Parameters sent to the owning extension to execute a workflow closure. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowExecuteRequest -{ - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + /// + public bool Equals(McpPlanPolicySource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Opaque token identifying this workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Registered workflow name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// + public override string ToString() => Value; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// 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)); + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, McpPlanPolicySource value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicySource)); + } + } } -/// Parameters for cooperatively aborting a workflow body. + +/// Canonical digest algorithm for a validated MCP card. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAbortRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CardDigestAlgorithm : IEquatable { - /// Opaque token identifying the execution attempt to abort. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; - - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + 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 CardDigestAlgorithm(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// 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; } -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// 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; + /// SHA-256 over RFC 8785 canonical JSON encoded as UTF-8. + public static CardDigestAlgorithm Sha256Rfc8785 { get; } = new("sha256-rfc8785"); - /// Owner-scoped task key included for correlation. - [JsonPropertyName("clientTaskId")] - public string ClientTaskId { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CardDigestAlgorithm left, CardDigestAlgorithm right) => left.Equals(right); - /// Canonical runtime-generated task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CardDigestAlgorithm left, CardDigestAlgorithm right) => !(left == right); - /// Reason the runtime requests cancellation. - [JsonPropertyName("reason")] - public ClientTaskCancelReason Reason { get; set; } + /// + public override bool Equals(object? obj) => obj is CardDigestAlgorithm other && Equals(other); - /// Session that owns the client task. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(CardDigestAlgorithm other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// Describes a filesystem error. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsError -{ - /// Error classification. - [JsonPropertyName("code")] - public SessionFsErrorCode Code { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Free-form detail about the error, for logging/diagnostics. - [JsonPropertyName("message")] - public string? Message { get; set; } -} + /// + public override string ToString() => 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; + /// 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)); + } - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } + /// + public override void Write(Utf8JsonWriter writer, CardDigestAlgorithm value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CardDigestAlgorithm)); + } + } } -/// Path of the file to read from the client-provided session filesystem. + +/// JSON MCP card media type accepted for install planning. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReadFileRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpServerCardMediaType : 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 McpServerCardMediaType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// 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; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Optional POSIX-style mode for newly created files. - [JsonPropertyName("mode")] - public long? Mode { get; set; } + /// The current MCP server card media type. + public static McpServerCardMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// The legacy MCP server card media type, accepted for compatibility. + public static McpServerCardMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); - /// 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 ==(McpServerCardMediaType left, McpServerCardMediaType right) => left.Equals(right); -/// 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; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpServerCardMediaType left, McpServerCardMediaType right) => !(left == right); - /// Optional POSIX-style mode for newly created files. - [JsonPropertyName("mode")] - public long? Mode { get; set; } + /// + public override bool Equals(object? obj) => obj is McpServerCardMediaType other && Equals(other); - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + public bool Equals(McpServerCardMediaType 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); -/// 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 string ToString() => Value; -/// Path to test for existence in the client-provided session filesystem. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsExistsRequest -{ - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// 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)); + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, McpServerCardMediaType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpServerCardMediaType)); + } + } } -/// Filesystem metadata for the requested path, or a filesystem error if the stat failed. + +/// Where a required value is applied when the planned server is launched. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsStatResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanValueCategory : IEquatable { - /// ISO 8601 timestamp of creation. - [JsonPropertyName("birthtime")] - public DateTimeOffset Birthtime { get; 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 McpPlanValueCategory(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Whether the path is a directory. - [JsonPropertyName("isDirectory")] - public bool IsDirectory { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Whether the path is a file. - [JsonPropertyName("isFile")] - public bool IsFile { get; set; } + /// Set as an environment variable on the launched process. + public static McpPlanValueCategory EnvironmentVariable { get; } = new("environment-variable"); - /// ISO 8601 timestamp of last modification. - [JsonPropertyName("mtime")] - public DateTimeOffset Mtime { get; set; } + /// Passed to the runtime that launches the package. + public static McpPlanValueCategory RuntimeArgument { get; } = new("runtime-argument"); - /// File size in bytes. - [JsonPropertyName("size")] - public long Size { get; set; } -} + /// Passed to the packaged server itself. + public static McpPlanValueCategory PackageArgument { get; } = new("package-argument"); -/// 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; + /// Sent as a request header to a remote endpoint. + public static McpPlanValueCategory Header { get; } = new("header"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Substituted into the remote endpoint URL. + public static McpPlanValueCategory UrlVariable { get; } = new("url-variable"); -/// 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; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanValueCategory left, McpPlanValueCategory 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 !=(McpPlanValueCategory left, McpPlanValueCategory right) => !(left == right); - /// Create parent directories as needed. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// + public override bool Equals(object? obj) => obj is McpPlanValueCategory other && Equals(other); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(McpPlanValueCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// Names of entries in the requested directory, or a filesystem error if the read failed. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirResult -{ - /// Entry names in the directory. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } -} + /// + public override string ToString() => 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; + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpPlanValueCategory 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, McpPlanValueCategory value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanValueCategory)); + } + } } -/// Directory entry returned by session filesystem `readdirWithTypes`, with name and entry type. + +/// Scalar type a required value must conform to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirWithTypesEntry +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanScalarValueType : IEquatable { - /// Entry name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + private readonly string? _value; - /// Entry type. - [JsonPropertyName("type")] - public SessionFsReaddirWithTypesEntryType Type { get; set; } -} + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanScalarValueType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// 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; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } -} + /// Free text. + public static McpPlanScalarValueType String { get; } = new("string"); -/// 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; + /// A number. + public static McpPlanScalarValueType Number { get; } = new("number"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// A boolean. + public static McpPlanScalarValueType Boolean { get; } = new("boolean"); -/// 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; } + /// A filesystem path. + public static McpPlanScalarValueType Path { get; } = new("path"); - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanScalarValueType left, McpPlanScalarValueType right) => left.Equals(right); - /// Remove directories and their contents recursively. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanScalarValueType left, McpPlanScalarValueType right) => !(left == right); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override bool Equals(object? obj) => obj is McpPlanScalarValueType other && Equals(other); -/// Source and destination paths for renaming or moving an entry in the client-provided session filesystem. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsRenameRequest -{ - /// Destination path using SessionFs conventions. - [JsonPropertyName("dest")] - public string Dest { get; set; } = string.Empty; + /// + public bool Equals(McpPlanScalarValueType 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); - /// Source path using SessionFs conventions. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; + /// + public override string ToString() => Value; + + /// 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)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpPlanScalarValueType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScalarValueType)); + } + } } -/// Query results including rows, columns, and rows affected, or a filesystem error if execution failed. + +/// Discriminator for an enumerated required value. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteQueryResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanEnumValueType : IEquatable { - /// 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; } + private readonly string? _value; - /// SQLite last_insert_rowid() value for INSERT. - [JsonPropertyName("lastInsertRowid")] - public long? LastInsertRowid { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanEnumValueType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// For SELECT: array of row objects. For others: empty array. - [JsonPropertyName("rows")] - public IList> Rows { get => field ??= []; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Number of rows affected (for INSERT/UPDATE/DELETE). - [JsonPropertyName("rowsAffected")] - public long RowsAffected { get; set; } -} + /// One of a fixed, non-empty set of permitted values. + public static McpPlanEnumValueType Enum { get; } = new("enum"); -/// 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; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanEnumValueType left, McpPlanEnumValueType right) => left.Equals(right); - /// SQL query to execute. - [JsonPropertyName("query")] - public string Query { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanEnumValueType left, McpPlanEnumValueType right) => !(left == right); - /// 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 bool Equals(object? obj) => obj is McpPlanEnumValueType other && Equals(other); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(McpPlanEnumValueType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// 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; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Human-readable transaction failure message. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; -} + /// + public override string ToString() => Value; -/// Per-statement results, or a classified transaction error. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteTransactionResult -{ - /// Classified transaction failure, when execution did not succeed. - [JsonPropertyName("error")] - public SessionFsSqliteTransactionError? Error { get; set; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpPlanEnumValueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Per-statement query results in input order. - [JsonPropertyName("results")] - public IList Results { get => field ??= []; set; } + /// + public override void Write(Utf8JsonWriter writer, McpPlanEnumValueType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanEnumValueType)); + } + } } -/// One statement in an atomic SQLite transaction. + +/// Transport exposed by a locally launched package. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteTransactionStatement +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanPackageTransport : IEquatable { - /// Optional named bind parameters. - [JsonPropertyName("params")] - public IDictionary? Params { get; set; } + private readonly string? _value; - /// SQL statement to execute. - [JsonPropertyName("query")] - public string Query { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanPackageTransport(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// How to execute the statement. - [JsonPropertyName("queryType")] - public SessionFsSqliteQueryType QueryType { get; set; } -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// 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; + /// A locally launched process spoken to over standard input and output. + public static McpPlanPackageTransport Stdio { get; } = new("stdio"); - /// Ordered SQL statements to execute in one transaction. - [JsonPropertyName("statements")] - public IList Statements { get => field ??= []; set; } -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanPackageTransport left, McpPlanPackageTransport right) => left.Equals(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; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanPackageTransport left, McpPlanPackageTransport right) => !(left == right); -/// Identifies the target session. -public sealed class SessionFsSqliteExistsRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override bool Equals(object? obj) => obj is McpPlanPackageTransport other && Equals(other); -/// 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 bool Equals(McpPlanPackageTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Provider-supplied title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// URL for web-rendered canvases. - [JsonPropertyName("url")] - public string? Url { get; set; } -} + /// + public override string ToString() => Value; -/// Host capabilities. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasHostContextCapabilities -{ - /// Whether canvas rendering is supported. - [JsonPropertyName("canvases")] - public bool? Canvases { get; set; } -} + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpPlanPackageTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } -/// 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; } + /// + public override void Write(Utf8JsonWriter writer, McpPlanPackageTransport value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPackageTransport)); + } + } } -/// Session context supplied by the runtime. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasSessionContext -{ - /// Active session working directory, when known. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } -} -/// Canvas open parameters sent to the provider. +/// Transport exposed by a remote endpoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasProviderOpenRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanRemoteTransport : IEquatable { - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; + private readonly string? _value; - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanRemoteTransport(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Canvas open input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// An HTTP endpoint. + public static McpPlanRemoteTransport Http { get; } = new("http"); - /// Stable caller-supplied canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// A streamable HTTP endpoint. + public static McpPlanRemoteTransport StreamableHttp { get; } = new("streamable-http"); - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// A server-sent events endpoint. + public static McpPlanRemoteTransport Sse { get; } = new("sse"); - /// 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 ==(McpPlanRemoteTransport left, McpPlanRemoteTransport 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 !=(McpPlanRemoteTransport left, McpPlanRemoteTransport right) => !(left == right); - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// + public override bool Equals(object? obj) => obj is McpPlanRemoteTransport other && Equals(other); - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// + public bool Equals(McpPlanRemoteTransport 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 McpPlanRemoteTransport 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) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanRemoteTransport)); + } + } } -/// Canvas action invocation parameters sent to the provider. + +/// Why capability and protocol-version negotiation refused a caller. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasProviderInvokeActionRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CatalogNegotiationRefusedReason : 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 CatalogNegotiationRefusedReason(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; } + /// The caller's protocol version is below the lowest this runtime serves. + public static CatalogNegotiationRefusedReason UnsupportedProtocolVersion { get; } = new("unsupported-protocol-version"); - /// Action input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// The caller requires at least one capability this runtime cannot honour. + public static CatalogNegotiationRefusedReason UnsupportedCapability { get; } = new("unsupported-capability"); - /// Canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => left.Equals(right); - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => !(left == right); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override bool Equals(object? obj) => obj is CatalogNegotiationRefusedReason other && Equals(other); -/// 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 bool Equals(CatalogNegotiationRefusedReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// 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; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// 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 string ToString() => Value; -/// 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; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CatalogNegotiationRefusedReason 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) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNegotiationRefusedReason)); + } + } } -/// A discovered extension entrypoint that the registered integrator may classify and resolve to an opaque launch profile. + +/// Which kind of opaque handle was presented. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExtensionLaunchProviderResolveRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CatalogHandleType : IEquatable { - /// 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; + private readonly string? _value; - /// Human-readable extension name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CatalogHandleType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Discovery source for the extension entrypoint. - [JsonPropertyName("source")] - public ExtensionSource Source { get; set; } -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// 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 -{ -} + /// A search candidate handle. + public static CatalogHandleType Candidate { get; } = new("candidate"); -/// 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; } + /// An install plan handle. + public static CatalogHandleType Plan { get; } = new("plan"); - /// 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; } + /// A model-safe reference to one retained search candidate. + public static CatalogHandleType Selection { get; } = new("selection"); - /// HTTP request headers, preserving multiple values per name. - [JsonPropertyName("headers")] - public IDictionary> Headers { get => field ??= new Dictionary>(); set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogHandleType left, CatalogHandleType right) => left.Equals(right); - /// 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; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogHandleType left, CatalogHandleType right) => !(left == right); - /// HTTP method, e.g. GET, POST. - [JsonPropertyName("method")] - public string Method { get; set; } = string.Empty; + /// + public override bool Equals(object? obj) => obj is CatalogHandleType other && Equals(other); - /// 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; } + /// + public bool Equals(CatalogHandleType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// 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; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// 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; } + /// + public override string ToString() => Value; - /// 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; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CatalogHandleType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Absolute request URL. - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, CatalogHandleType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleType)); + } + } } -/// Acknowledgement. The SDK is free to ignore the ack and treat chunk delivery as fire-and-forget. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpRequestChunkResult -{ -} -/// A request body chunk or cancellation signal. +/// Why a presented handle was rejected. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpRequestChunkRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CatalogHandleRejectionReason : IEquatable { - /// 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; } + private readonly string? _value; - /// Optional human-readable reason for the cancellation, propagated for logging. - [JsonPropertyName("cancelReason")] - public string? CancelReason { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CatalogHandleRejectionReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// 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; + /// Gets the value associated with this . + public string Value => _value ?? 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; } + /// The handle is unparseable or unknown. + public static CatalogHandleRejectionReason Invalid { get; } = new("invalid"); - /// Matches the requestId from the originating httpRequestStart frame. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; -} + /// The handle's time to live has elapsed. + public static CatalogHandleRejectionReason Stale { get; } = new("stale"); -/// Client environment metadata describing the process that produced a telemetry event. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryClientInfo -{ - /// Copilot CLI version string. - [JsonPropertyName("cli_version")] - public string CliVersion { get; set; } = string.Empty; + /// The handle has already been used, and handles are single-use. + public static CatalogHandleRejectionReason Replayed { get; } = new("replayed"); - /// Name of the client application. - [JsonPropertyName("client_name")] - public string? ClientName { get; set; } + /// The handle was issued by a different runtime instance or session. + public static CatalogHandleRejectionReason Foreign { get; } = new("foreign"); - /// Type of client. - [JsonPropertyName("client_type")] - public string? ClientType { get; set; } + /// The handle was issued for another catalog operation. + public static CatalogHandleRejectionReason WrongKind { get; } = new("wrong-kind"); - /// Copilot subscription plan, when known. - [JsonPropertyName("copilot_plan")] - public string? CopilotPlan { get; set; } + /// The supplied search identifier does not match the retained candidate. + public static CatalogHandleRejectionReason SearchMismatch { get; } = new("search-mismatch"); - /// Number of logical CPU cores on the host. - [JsonPropertyName("cpu_count")] - public long? CpuCount { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogHandleRejectionReason left, CatalogHandleRejectionReason right) => left.Equals(right); - /// 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 not equivalent. + public static bool operator !=(CatalogHandleRejectionReason left, CatalogHandleRejectionReason right) => !(left == right); - /// Stable machine identifier for the device. - [JsonPropertyName("dev_device_id")] - public string? DevDeviceId { get; set; } + /// + public override bool Equals(object? obj) => obj is CatalogHandleRejectionReason other && Equals(other); - /// Whether the user is a GitHub/Microsoft staff member. - [JsonPropertyName("is_staff")] - public bool? IsStaff { get; set; } + /// + public bool Equals(CatalogHandleRejectionReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Node.js runtime version string. - [JsonPropertyName("node_version")] - public string NodeVersion { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Operating system architecture (e.g. arm64, x64). - [JsonPropertyName("os_arch")] - public string OsArch { get; set; } = string.Empty; + /// + public override string ToString() => Value; - /// Operating system platform (e.g. darwin, linux, win32). - [JsonPropertyName("os_platform")] - public string OsPlatform { get; set; } = string.Empty; + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CatalogHandleRejectionReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Operating system version string. - [JsonPropertyName("os_version")] - public string OsVersion { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, CatalogHandleRejectionReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleRejectionReason)); + } + } } -/// 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. + +/// Which request field was rejected locally or by the catalog authority. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryEvent +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CatalogInvalidRequestField : IEquatable { - /// Client environment metadata. - [JsonPropertyName("client")] - public GitHubTelemetryClientInfo? Client { get; set; } - - /// Copilot tracking ID for user-level attribution. - [JsonPropertyName("copilot_tracking_id")] - public string? CopilotTrackingId { get; set; } - - /// Timestamp when the event was created (ISO 8601 format). - [JsonPropertyName("created_at")] - public string? CreatedAt { get; set; } + private readonly string? _value; - /// Experiment assignment context. - [JsonPropertyName("exp_assignment_context")] - public string? ExpAssignmentContext { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CatalogInvalidRequestField(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Feature flags enabled for this session, as a map from flag to value. - [JsonPropertyName("features")] - public IDictionary? Features { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). - [JsonPropertyName("kind")] - public string Kind { get; set; } = string.Empty; + /// The selected existing attached session was missing, malformed or unavailable. + public static CatalogInvalidRequestField PolicySessionId { get; } = new("policySessionId"); - /// Numeric metrics as a map from key to value. - [JsonPropertyName("metrics")] - public IDictionary Metrics { get => field ??= new Dictionary(); set; } + /// The search query was empty or longer than permitted. + public static CatalogInvalidRequestField Query { get; } = new("query"); - /// Reference to the model call that produced this event. - [JsonPropertyName("model_call_id")] - public string? ModelCallId { get; set; } + /// The requested result count fell outside its permitted range. + public static CatalogInvalidRequestField Limit { get; } = new("limit"); - /// String-valued properties as a map from key to value. - [JsonPropertyName("properties")] - public IDictionary Properties { get => field ??= new Dictionary(); set; } + /// The requested candidate kinds were empty or contained a duplicate. + public static CatalogInvalidRequestField Kinds { get; } = new("kinds"); - /// Session identifier the event belongs to. - [JsonPropertyName("session_id")] - public string? SessionId { get; set; } -} + /// The negotiation block was missing or malformed. + public static CatalogInvalidRequestField Contract { get; } = new("contract"); -/// Payload for a `gitHubTelemetry.event` notification: a single GitHub telemetry event the runtime forwards to a host connection that opted into telemetry forwarding during the `server.connect` handshake. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryNotification -{ - /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. - [JsonPropertyName("event")] - public GitHubTelemetryEvent Event { get => field ??= new(); set; } + /// The plan source was missing or malformed. + public static CatalogInvalidRequestField Source { get; } = new("source"); - /// 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; } + /// The supplied card was missing its media type, URL, or data. + public static CatalogInvalidRequestField Card { get; } = new("card"); - /// 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; } -} + /// The requested configuration scope is not one this runtime writes. + public static CatalogInvalidRequestField Scope { get; } = new("scope"); -/// SDK host response to a GitHub credential request. -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(GitHubTokenAcquireResultToken), "token")] -[JsonDerivedType(typeof(GitHubTokenAcquireResultCancelled), "cancelled")] -public partial class GitHubTokenAcquireResult -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// The 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 token variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class GitHubTokenAcquireResultToken : GitHubTokenAcquireResult -{ - /// - [JsonIgnore] - public override string Kind => "token"; + /// The opaque selection reference was missing or malformed. + public static CatalogInvalidRequestField SelectionRef { get; } = new("selectionRef"); - /// GitHub access token acquired by the SDK host. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } + /// The terminal selection outcome was missing or unsupported. + public static CatalogInvalidRequestField Outcome { get; } = new("outcome"); - /// 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 equivalent. + public static bool operator ==(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => left.Equals(right); - /// OAuth token type. Defaults to bearer when omitted. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("tokenType")] - public string? TokenType { get; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => !(left == right); -/// The cancelled variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class GitHubTokenAcquireResultCancelled : GitHubTokenAcquireResult -{ /// - [JsonIgnore] - public override string Kind => "cancelled"; -} + public override bool Equals(object? obj) => obj is CatalogInvalidRequestField other && Equals(other); -/// 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; + /// + public bool Equals(CatalogInvalidRequestField other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Why the runtime is requesting a GitHub credential. - [JsonPropertyName("reason")] - public GitHubTokenAcquireReason Reason { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Opaque identifier generated by the SDK for this callback registration. - [JsonPropertyName("registrationId")] - public string RegistrationId { get; set; } = string.Empty; + /// + public override string ToString() => Value; - /// Session receiving the token. Absent only before a cloud session has been assigned its id. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CatalogInvalidRequestField 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) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogInvalidRequestField)); + } + } } -/// Closed set of public task kinds a connection can negotiate. + +/// 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 TaskKind : 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 TaskKind(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; - /// Runtime-owned background agent task. - public static TaskKind Agent { get; } = new("agent"); + /// No credential was presented, so there is nothing to refresh and the caller must sign in. + public static CatalogAuthenticationRequiredReason NoCredential { get; } = new("no-credential"); - /// Runtime-owned shell task. - public static TaskKind Shell { get; } = new("shell"); + /// 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"); - /// Client-owned externally executed task. - public static TaskKind Client { get; } = new("client"); + /// 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 ==(TaskKind left, TaskKind 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 !=(TaskKind left, TaskKind 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 TaskKind other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAuthenticationRequiredReason other && Equals(other); /// - public bool Equals(TaskKind 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); @@ -23350,107 +29433,164 @@ 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 CatalogAuthenticationRequiredReason 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, CatalogAuthenticationRequiredReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAuthenticationRequiredReason)); } } } -/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +/// 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 HookType : 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 HookType(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; - /// Runs before a tool is invoked. - public static HookType PreToolUse { get; } = new("preToolUse"); + /// No network is available, so nothing was attempted. + public static CatalogNetworkFailureReason Offline { get; } = new("offline"); - /// Runs before an MCP tool is invoked. - public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); + /// The authority's name could not be resolved. + public static CatalogNetworkFailureReason Dns { get; } = new("dns"); - /// Runs after a tool completes successfully. - public static HookType PostToolUse { get; } = new("postToolUse"); + /// The request exceeded its time budget. + public static CatalogNetworkFailureReason Timeout { get; } = new("timeout"); - /// Runs after a tool fails. - public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + /// The TLS handshake or certificate validation failed. + public static CatalogNetworkFailureReason Tls { get; } = new("tls"); - /// Runs after the user submits a prompt. - public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + /// The connection was refused or reset. + public static CatalogNetworkFailureReason ConnectionRefused { get; } = new("connection-refused"); - /// 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"); + /// The configured proxy returned 407 and requires authentication. + public static CatalogNetworkFailureReason ProxyAuthenticationRequired { get; } = new("proxy-authentication-required"); - /// Runs when a session starts. - public static HookType SessionStart { get; } = new("sessionStart"); + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + public static CatalogNetworkFailureReason RateLimited { get; } = new("rate-limited"); - /// Runs when a session ends. - public static HookType SessionEnd { get; } = new("sessionEnd"); + /// The authority returned a transient 5xx response. + public static CatalogNetworkFailureReason ServiceUnavailable { get; } = new("service-unavailable"); - /// Runs after an agent result is produced. - public static HookType PostResult { get; } = new("postResult"); + /// The authority returned another status the runtime treats as a failure. + public static CatalogNetworkFailureReason HttpStatus { get; } = new("http-status"); - /// Runs before a pull request description is generated. - public static HookType PrePRDescription { get; } = new("prePRDescription"); + /// The response exceeded the permitted size. + public static CatalogNetworkFailureReason ResponseTooLarge { get; } = new("response-too-large"); - /// Runs when the agent encounters an error. - public static HookType ErrorOccurred { get; } = new("errorOccurred"); + /// A redirect was refused by the runtime's redirect policy. + public static CatalogNetworkFailureReason RedirectRejected { get; } = new("redirect-rejected"); - /// Runs when the agent stops. - public static HookType AgentStop { get; } = new("agentStop"); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogNetworkFailureReason left, CatalogNetworkFailureReason right) => left.Equals(right); - /// Runs when a subagent starts. - public static HookType SubagentStart { get; } = new("subagentStart"); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogNetworkFailureReason left, CatalogNetworkFailureReason right) => !(left == right); - /// Runs when a subagent stops. - public static HookType SubagentStop { get; } = new("subagentStop"); + /// + public override bool Equals(object? obj) => obj is CatalogNetworkFailureReason other && Equals(other); - /// Runs before conversation context is compacted. - public static HookType PreCompact { get; } = new("preCompact"); + /// + public bool Equals(CatalogNetworkFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Runs when the agent requests permission. - public static HookType PermissionRequest { get; } = new("permissionRequest"); + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Runs when the agent emits a notification. - public static HookType Notification { get; } = new("notification"); + /// + public override string ToString() => Value; - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(HookType left, HookType right) => left.Equals(right); + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CatalogNetworkFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(HookType left, HookType right) => !(left == right); + /// + public override void Write(Utf8JsonWriter writer, CatalogNetworkFailureReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNetworkFailureReason)); + } + } +} + + +/// Which hardened-fetch control refused a retrieval. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CatalogUnsafeRetrievalReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CatalogUnsafeRetrievalReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The URL used a scheme the runtime refuses to fetch. + public static CatalogUnsafeRetrievalReason BlockedScheme { get; } = new("blocked-scheme"); + + /// The URL embedded credentials. + public static CatalogUnsafeRetrievalReason CredentialsInUrl { get; } = new("credentials-in-url"); + + /// The URL resolved to a loopback, private, link-local, or cloud metadata address. + public static CatalogUnsafeRetrievalReason BlockedAddress { get; } = new("blocked-address"); + + /// A redirect target resolved to a blocked address. + public static CatalogUnsafeRetrievalReason RedirectToBlockedAddress { get; } = new("redirect-to-blocked-address"); + + /// The configured proxy policy refused the request. + public static CatalogUnsafeRetrievalReason ProxyRejected { get; } = new("proxy-rejected"); + + /// The authority is not permitted for card retrieval. + public static CatalogUnsafeRetrievalReason HostNotPermitted { get; } = new("host-not-permitted"); + + /// 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 HookType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnsafeRetrievalReason other && Equals(other); /// - public bool Equals(HookType 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); @@ -23458,68 +29598,68 @@ 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 CatalogUnsafeRetrievalReason 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, CatalogUnsafeRetrievalReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnsafeRetrievalReason)); } } } -/// Configuration tier that contributed a discovered hook action. +/// Media type a catalog card is interpreted as. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HookOrigin : 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 HookOrigin(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; - /// Hook loaded from user settings or the user's hook directory. - public static HookOrigin User { get; } = new("user"); + /// The current MCP server card media type. + public static CatalogMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); - /// Hook loaded from repository settings or the repository hook directory. - public static HookOrigin Repository { get; } = new("repository"); + /// The legacy MCP server card media type, accepted for compatibility. + public static CatalogMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); - /// 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"); + /// An AI skill card. Representable and searchable, but typed non-installable. + public static CatalogMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); - /// Hook enforced by centrally managed policy. - public static HookOrigin Policy { get; } = new("policy"); + /// 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 ==(HookOrigin left, HookOrigin right) => left.Equals(right); + /// 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 !=(HookOrigin left, HookOrigin right) => !(left == 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 HookOrigin other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMediaType other && Equals(other); /// - public bool Equals(HookOrigin 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); @@ -23527,68 +29667,71 @@ 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 CatalogMediaType 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, CatalogMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMediaType)); } } } -/// Resolved Anthropic adaptive-thinking capability for a model. +/// How a card failed validation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AdaptiveThinkingSupport : 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 AdaptiveThinkingSupport(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; - /// The model does not accept thinking.type='adaptive'. - public static AdaptiveThinkingSupport Unsupported { get; } = new("unsupported"); + /// The document is not well-formed JSON. + public static CatalogMalformedCardReason InvalidJson { get; } = new("invalid-json"); - /// The model accepts adaptive thinking but also accepts thinking.type='enabled'. - public static AdaptiveThinkingSupport Optional { get; } = new("optional"); + /// The document does not satisfy its media type's schema. + public static CatalogMalformedCardReason SchemaViolation { get; } = new("schema-violation"); - /// The model defaults to adaptive thinking and rejects thinking.type='enabled' with HTTP 400, but still accepts thinking.type='disabled' (e.g. opus-4.7/4.8/5, sonnet-5). - public static AdaptiveThinkingSupport Required { get; } = new("required"); + /// The declared media type is not one this runtime understands. + public static CatalogMalformedCardReason UnsupportedMediaType { get; } = new("unsupported-media-type"); - /// 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 field the media type requires is absent. + public static CatalogMalformedCardReason MissingRequiredField { get; } = new("missing-required-field"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => left.Equals(right); + /// The document exceeded the permitted size. + public static CatalogMalformedCardReason SizeLimitExceeded { get; } = new("size-limit-exceeded"); - /// 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 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 AdaptiveThinkingSupport other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMalformedCardReason other && Equals(other); /// - public bool Equals(AdaptiveThinkingSupport 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); @@ -23596,65 +29739,68 @@ public AdaptiveThinkingSupport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AdaptiveThinkingSupport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogMalformedCardReason 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, CatalogMalformedCardReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AdaptiveThinkingSupport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMalformedCardReason)); } } } -/// Model capability category for grouping in the model picker. +/// Which wire-contract rule an upstream response broke. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPickerCategory : 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 ModelPickerCategory(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; - /// Lightweight model category optimized for faster, lower-cost interactions. - public static ModelPickerCategory Lightweight { get; } = new("lightweight"); + /// A result carried both a URL and embedded data, when exactly one is permitted. + public static CatalogContractViolationReason BothUrlAndData { get; } = new("both-url-and-data"); - /// Versatile model category suitable for a broad range of tasks. - public static ModelPickerCategory Versatile { get; } = new("versatile"); + /// A result carried neither a URL nor embedded data, when exactly one is required. + public static CatalogContractViolationReason NeitherUrlNorData { get; } = new("neither-url-nor-data"); - /// Powerful model category optimized for complex tasks. - public static ModelPickerCategory Powerful { get; } = new("powerful"); + /// Two results claimed the same collision key: normalised identity for existing kinds, or normalised identity and declared version for Agent Plugins. + public static CatalogContractViolationReason DuplicateIdentity { get; } = new("duplicate-identity"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelPickerCategory left, ModelPickerCategory right) => left.Equals(right); + /// A result declared no media type, or one this contract does not model. + public static CatalogContractViolationReason UnknownMediaType { get; } = new("unknown-media-type"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelPickerCategory left, ModelPickerCategory right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogContractViolationReason left, CatalogContractViolationReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogContractViolationReason left, CatalogContractViolationReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelPickerCategory other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogContractViolationReason other && Equals(other); /// - public bool Equals(ModelPickerCategory 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); @@ -23662,68 +29808,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 CatalogContractViolationReason 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, CatalogContractViolationReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogContractViolationReason)); } } } -/// Relative cost tier for token-based billing users. +/// Why no usable transport could be offered. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPickerPriceCategory : 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 ModelPickerPriceCategory(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; - /// Lowest relative token cost tier. - public static ModelPickerPriceCategory Low { get; } = new("low"); - - /// Medium relative token cost tier. - public static ModelPickerPriceCategory Medium { get; } = new("medium"); + /// The card advertises no transport this runtime can use. + public static CatalogUnavailableTransportReason NoEligibleTransport { get; } = new("no-eligible-transport"); - /// High relative token cost tier. - public static ModelPickerPriceCategory High { get; } = new("high"); + /// Every advertised transport is of a kind this runtime does not implement. + public static CatalogUnavailableTransportReason TransportNotSupported { get; } = new("transport-not-supported"); - /// Highest relative token cost tier. - public static ModelPickerPriceCategory VeryHigh { get; } = new("very_high"); + /// 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 ==(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelPickerPriceCategory other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnavailableTransportReason other && Equals(other); /// - public bool Equals(ModelPickerPriceCategory 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); @@ -23731,65 +29874,65 @@ 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 CatalogUnavailableTransportReason 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, CatalogUnavailableTransportReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerPriceCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableTransportReason)); } } } -/// Current policy state for this model. +/// Why a discoverable candidate cannot be installed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPolicyState : 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 ModelPolicyState(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 model is enabled by policy. - public static ModelPolicyState Enabled { get; } = new("enabled"); + /// This kind of resource is not installable through this surface. + public static CatalogNotInstallableReason KindNotInstallable { get; } = new("kind-not-installable"); - /// The model is disabled by policy. - public static ModelPolicyState Disabled { get; } = new("disabled"); + /// AI skills are discoverable but have no typed importer in this phase. + public static CatalogNotInstallableReason AiSkillNotInstallable { get; } = new("ai-skill-not-installable"); - /// No explicit policy is configured for the model. - public static ModelPolicyState Unconfigured { get; } = new("unconfigured"); + /// Policy forbids installing this candidate. + public static CatalogNotInstallableReason PolicyForbids { get; } = new("policy-forbids"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelPolicyState left, ModelPolicyState right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelPolicyState left, ModelPolicyState right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelPolicyState other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogNotInstallableReason other && Equals(other); /// - public bool Equals(ModelPolicyState 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); @@ -23797,68 +29940,71 @@ public ModelPolicyState(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ModelPolicyState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogNotInstallableReason 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, CatalogNotInstallableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPolicyState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNotInstallableReason)); } } } -/// Server transport type: stdio, http, sse (deprecated), or memory. +/// 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 DiscoveredMcpServerType : 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 DiscoveredMcpServerType(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; - /// Server communicates over stdio with a local child process. - public static DiscoveredMcpServerType Stdio { get; } = new("stdio"); + /// Bounded search is not wired up on this runtime build. + public static CatalogUnavailableReason SearchUnavailable { get; } = new("search-unavailable"); - /// Server communicates over streamable HTTP. - public static DiscoveredMcpServerType Http { get; } = new("http"); + /// Install planning is not wired up on this runtime build. + public static CatalogUnavailableReason PlanningUnavailable { get; } = new("planning-unavailable"); - /// Server communicates over Server-Sent Events (deprecated). - public static DiscoveredMcpServerType Sse { get; } = new("sse"); + /// Exact candidate selection is not available in this session or runtime. + public static CatalogUnavailableReason SelectionUnavailable { get; } = new("selection-unavailable"); - /// Server is backed by an in-memory runtime implementation. - public static DiscoveredMcpServerType Memory { get; } = new("memory"); + /// No catalog authority is configured for this runtime. + public static CatalogUnavailableReason AuthorityNotConfigured { get; } = new("authority-not-configured"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => left.Equals(right); + /// 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 ==(CatalogUnavailableReason left, CatalogUnavailableReason 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 !=(CatalogUnavailableReason left, CatalogUnavailableReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiscoveredMcpServerType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnavailableReason other && Equals(other); /// - public bool Equals(DiscoveredMcpServerType 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); @@ -23866,86 +30012,68 @@ 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 CatalogUnavailableReason 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, CatalogUnavailableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredMcpServerType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableReason)); } } } -/// 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. +/// 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 CatalogCapability : 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 CatalogCapability(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; - /// Understands the current `application/mcp-server-card+json` media type. - public static CatalogCapability McpServerCard { get; } = new("mcp-server-card"); - - /// Understands the legacy `application/mcp-server+json` media type. - public static CatalogCapability LegacyMcpServerCard { get; } = new("legacy-mcp-server-card"); - - /// Understands `application/ai-skill` candidates as discovery-only and typed non-installable. - public static CatalogCapability AiSkillDiscovery { get; } = new("ai-skill-discovery"); - - /// Understands opt-in `application/vnd.github.copilot-plugin` candidates and their typed identity, version, source, and compatibility fields. - public static CatalogCapability AgentPluginDiscovery { get; } = new("agent-plugin-discovery"); - - /// Understands side-effect-free MCP install-plan requests, results, and plan handles; `planning-unavailable` separately reports that planning is not enabled. - public static CatalogCapability McpInstallPlanning { get; } = new("mcp-install-planning"); - - /// Understands plans that enumerate every eligible transport rather than a single preferred one. - public static CatalogCapability MultipleTransportChoice { get; } = new("multiple-transport-choice"); - - /// Understands explicit numbered navigation and authority-reported pagination metadata with opaque tokens. Advertised and granted only when requested. - public static CatalogCapability CatalogSearchPagination { get; } = new("catalog-search-pagination"); + /// Eligible for default catalogue exposure. + public static CatalogTrustEligibility Default { get; } = new("default"); - /// 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"); + /// Eligible only when expanded or community results are requested. + public static CatalogTrustEligibility Expanded { get; } = new("expanded"); - /// Understands exact candidate selection through model-safe opaque references and host-only candidate-handle hand-off. - public static CatalogCapability CatalogSelection { get; } = new("catalog-selection"); + /// Not eligible for normal catalogue exposure. + public static CatalogTrustEligibility Hidden { get; } = new("hidden"); - /// 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 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 ==(CatalogCapability left, CatalogCapability 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 !=(CatalogCapability left, CatalogCapability 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 CatalogCapability other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustEligibility other && Equals(other); /// - public bool Equals(CatalogCapability 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); @@ -23953,62 +30081,59 @@ public CatalogCapability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogTrustEligibility 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, CatalogTrustEligibility value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCapability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustEligibility)); } } } -/// Whether a planned configuration change would create or modify an entry. +/// Bounded authority that supplied a catalogue trust observation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanConfigurationOperation : 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 McpPlanConfigurationOperation(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; - /// Creates a configuration entry that does not exist yet. - public static McpPlanConfigurationOperation Add { get; } = new("add"); - - /// Modifies a configuration entry that already exists. - public static McpPlanConfigurationOperation Update { get; } = new("update"); + /// 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 ==(McpPlanConfigurationOperation left, McpPlanConfigurationOperation 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 !=(McpPlanConfigurationOperation left, McpPlanConfigurationOperation 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 McpPlanConfigurationOperation other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustSource other && Equals(other); /// - public bool Equals(McpPlanConfigurationOperation 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); @@ -24016,59 +30141,59 @@ 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 CatalogTrustSource 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, CatalogTrustSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanConfigurationOperation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSource)); } } } -/// Configuration scope an MCP install plan targets. +/// Schema version of the catalogue trust snapshot envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanScope : 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 McpPlanScope(string value) + public CatalogTrustSnapshotSchemaVersion(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The user's own MCP configuration. - public static McpPlanScope User { get; } = new("user"); + /// 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 ==(McpPlanScope left, McpPlanScope 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 !=(McpPlanScope left, McpPlanScope 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 McpPlanScope other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustSnapshotSchemaVersion other && Equals(other); /// - public bool Equals(McpPlanScope 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); @@ -24076,65 +30201,62 @@ public McpPlanScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogTrustSnapshotSchemaVersion 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, CatalogTrustSnapshotSchemaVersion value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSnapshotSchemaVersion)); } } } -/// What policy decided for a planned server. +/// 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 McpPlanPolicyDecision : 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 McpPlanPolicyDecision(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; - /// 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"); + /// Tier one as assigned by the catalogue authority. + public static CatalogTrustTier T1 { get; } = new("T1"); - /// Policy permits the server only after an explicit approval. - public static McpPlanPolicyDecision RequiresApproval { get; } = new("requires-approval"); + /// 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 ==(McpPlanPolicyDecision left, McpPlanPolicyDecision 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 !=(McpPlanPolicyDecision left, McpPlanPolicyDecision 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 McpPlanPolicyDecision other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustTier other && Equals(other); /// - public bool Equals(McpPlanPolicyDecision 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); @@ -24142,68 +30264,74 @@ 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 CatalogTrustTier 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, CatalogTrustTier value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicyDecision)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustTier)); } } } -/// Which authority produced a policy decision. +/// Configuration ownership and setup observations, distinct from tool permissions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPolicySource : 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 McpPlanPolicySource(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; - /// No policy applied, so the server is permitted by default. - public static McpPlanPolicySource None { get; } = new("none"); + /// Owned configuration exists; inventory alone does not grant activation. + public static McpInstallationState NeedsSetup { get; } = new("needs-setup"); - /// An enterprise allowlist evaluated the server. - public static McpPlanPolicySource EnterpriseAllowlist { get; } = new("enterprise-allowlist"); + /// The selected authorised session reports an active installation. + public static McpInstallationState Active { get; } = new("active"); - /// The registry the card came from evaluated the server. - public static McpPlanPolicySource RegistryPolicy { get; } = new("registry-policy"); + /// The selected server requires explicit sign-in. + public static McpInstallationState AuthenticationRequired { get; } = new("authentication-required"); - /// Local trust settings evaluated the server. - public static McpPlanPolicySource LocalTrust { get; } = new("local-trust"); + /// The selected server could not be activated. + public static McpInstallationState ActivationFailed { get; } = new("activation-failed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanPolicySource left, McpPlanPolicySource right) => left.Equals(right); + /// Owned configuration no longer matches its receipt. + public static McpInstallationState ConfigurationModified { get; } = new("configuration-modified"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanPolicySource left, McpPlanPolicySource right) => !(left == right); + /// Confirmed durable work or unsafe evidence requires recovery. + public static McpInstallationState RecoveryRequired { get; } = new("recovery-required"); + + /// Returns a value indicating whether two instances are 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 McpPlanPolicySource other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationState other && Equals(other); /// - public bool Equals(McpPlanPolicySource 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); @@ -24211,59 +30339,125 @@ 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 McpInstallationState 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, McpInstallationState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicySource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationState)); } } } -/// Canonical digest algorithm for a validated MCP card. +/// Bounded refusal categories, without echoing handles, credentials or configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CardDigestAlgorithm : 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 CardDigestAlgorithm(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; - /// SHA-256 over RFC 8785 canonical JSON encoded as UTF-8. - public static CardDigestAlgorithm Sha256Rfc8785 { get; } = new("sha256-rfc8785"); + /// The selected choice or request is unsupported or malformed. + public static McpInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CardDigestAlgorithm left, CardDigestAlgorithm right) => left.Equals(right); + /// The bounded original-connection operation limit was reached. + public static McpInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CardDigestAlgorithm left, CardDigestAlgorithm right) => !(left == right); + /// 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 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 CardDigestAlgorithm other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationFailureReason other && Equals(other); /// - public bool Equals(CardDigestAlgorithm 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); @@ -24271,62 +30465,62 @@ 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 McpInstallationFailureReason 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, McpInstallationFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CardDigestAlgorithm)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationFailureReason)); } } } -/// JSON MCP card media type accepted for install planning. +/// 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 McpServerCardMediaType : 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 McpServerCardMediaType(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 current MCP server card media type. - public static McpServerCardMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); + /// The selected operating-system keychain, without fallback to file storage. + public static McpInstallationSecretStorage Keychain { get; } = new("keychain"); - /// The legacy MCP server card media type, accepted for compatibility. - public static McpServerCardMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); + /// 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 ==(McpServerCardMediaType left, McpServerCardMediaType 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 !=(McpServerCardMediaType left, McpServerCardMediaType 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 McpServerCardMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationSecretStorage other && Equals(other); /// - public bool Equals(McpServerCardMediaType 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); @@ -24334,71 +30528,62 @@ 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 McpInstallationSecretStorage 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, McpInstallationSecretStorage value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpServerCardMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationSecretStorage)); } } } -/// Where a required value is applied when the planned server is launched. +/// Persisted extension discovery source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanValueCategory : 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 McpPlanValueCategory(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; - /// Set as an environment variable on the launched process. - public static McpPlanValueCategory EnvironmentVariable { get; } = new("environment-variable"); - - /// Passed to the runtime that launches the package. - public static McpPlanValueCategory RuntimeArgument { get; } = new("runtime-argument"); - - /// Passed to the packaged server itself. - public static McpPlanValueCategory PackageArgument { get; } = new("package-argument"); - - /// Sent as a request header to a remote endpoint. - public static McpPlanValueCategory Header { get; } = new("header"); + /// Extension discovered from the user's extensions directory. + public static DiscoveredExtensionSource User { get; } = new("user"); - /// Substituted into the remote endpoint URL. - public static McpPlanValueCategory UrlVariable { get; } = new("url-variable"); + /// 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 ==(McpPlanValueCategory left, McpPlanValueCategory 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 !=(McpPlanValueCategory left, McpPlanValueCategory 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 McpPlanValueCategory other && Equals(other); + public override bool Equals(object? obj) => obj is DiscoveredExtensionSource other && Equals(other); /// - public bool Equals(McpPlanValueCategory 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); @@ -24406,68 +30591,65 @@ 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 DiscoveredExtensionSource 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, DiscoveredExtensionSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanValueCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionSource)); } } } -/// Scalar type a required value must conform to. +/// Effective extension loading and agent-management mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanScalarValueType : 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 McpPlanScalarValueType(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; - /// Free text. - public static McpPlanScalarValueType String { get; } = new("string"); - - /// A number. - public static McpPlanScalarValueType Number { get; } = new("number"); + /// Extensions are not loaded. + public static DiscoveredExtensionMode Disabled { get; } = new("disabled"); - /// A boolean. - public static McpPlanScalarValueType Boolean { get; } = new("boolean"); + /// Extensions are loaded, but the agent cannot create, reload, or manage them. + public static DiscoveredExtensionMode LoadOnly { get; } = new("load_only"); - /// A filesystem path. - public static McpPlanScalarValueType Path { get; } = new("path"); + /// 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 ==(McpPlanScalarValueType left, McpPlanScalarValueType 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 !=(McpPlanScalarValueType left, McpPlanScalarValueType 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 McpPlanScalarValueType other && Equals(other); + public override bool Equals(object? obj) => obj is DiscoveredExtensionMode other && Equals(other); /// - public bool Equals(McpPlanScalarValueType 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); @@ -24475,59 +30657,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 DiscoveredExtensionMode 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, DiscoveredExtensionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScalarValueType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionMode)); } } } -/// Discriminator for an enumerated required value. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanEnumValueType : 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 McpPlanEnumValueType(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; - /// One of a fixed, non-empty set of permitted values. - public static McpPlanEnumValueType Enum { get; } = new("enum"); + /// 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 ==(McpPlanEnumValueType left, McpPlanEnumValueType 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 !=(McpPlanEnumValueType left, McpPlanEnumValueType 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 McpPlanEnumValueType other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationScope other && Equals(other); /// - public bool Equals(McpPlanEnumValueType 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); @@ -24535,59 +30717,65 @@ 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 SkillInstallationScope 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, SkillInstallationScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanEnumValueType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationScope)); } } } -/// Transport exposed by a locally launched package. +/// Owned Skill state observed from files and receipts. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPackageTransport : 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 McpPlanPackageTransport(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; - /// A locally launched process spoken to over standard input and output. - public static McpPlanPackageTransport Stdio { get; } = new("stdio"); + /// Owned files and receipt evidence match. + public static SkillInstallationOwnershipState Intact { get; } = new("intact"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanPackageTransport left, McpPlanPackageTransport right) => left.Equals(right); + /// Owned files no longer match the receipt. + public static SkillInstallationOwnershipState Modified { get; } = new("modified"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanPackageTransport left, McpPlanPackageTransport right) => !(left == right); + /// Ownership evidence requires recovery before mutation. + public static SkillInstallationOwnershipState RecoveryRequired { get; } = new("recovery-required"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillInstallationOwnershipState left, SkillInstallationOwnershipState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInstallationOwnershipState left, SkillInstallationOwnershipState right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanPackageTransport other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationOwnershipState other && Equals(other); /// - public bool Equals(McpPlanPackageTransport 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); @@ -24595,65 +30783,68 @@ 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 SkillInstallationOwnershipState 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, SkillInstallationOwnershipState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPackageTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationOwnershipState)); } } } -/// Transport exposed by a remote endpoint. +/// Bound-session observation after reconciling persisted enablement. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanRemoteTransport : 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 McpPlanRemoteTransport(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; - /// An HTTP endpoint. - public static McpPlanRemoteTransport Http { get; } = new("http"); + /// The selected session has loaded this Skill and it is enabled. + public static SkillInstallationSessionState LoadedEnabled { get; } = new("loaded-enabled"); - /// A streamable HTTP endpoint. - public static McpPlanRemoteTransport StreamableHttp { get; } = new("streamable-http"); + /// The selected session has loaded this Skill or settings and it is disabled. + public static SkillInstallationSessionState LoadedDisabled { get; } = new("loaded-disabled"); - /// A server-sent events endpoint. - public static McpPlanRemoteTransport Sse { get; } = new("sse"); + /// The selected session has not loaded Skills after the latest change. + public static SkillInstallationSessionState NotLoaded { get; } = new("not-loaded"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanRemoteTransport left, McpPlanRemoteTransport right) => left.Equals(right); + /// The selected session could not be inspected. + public static SkillInstallationSessionState Unknown { get; } = new("unknown"); - /// 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 equivalent. + public static bool operator ==(SkillInstallationSessionState left, SkillInstallationSessionState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInstallationSessionState left, SkillInstallationSessionState right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanRemoteTransport other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationSessionState other && Equals(other); /// - public bool Equals(McpPlanRemoteTransport 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); @@ -24661,62 +30852,146 @@ 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 SkillInstallationSessionState 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, SkillInstallationSessionState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanRemoteTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationSessionState)); } } } -/// Why capability and protocol-version negotiation refused a caller. +/// Bounded refusal categories for verified Skill installation management. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNegotiationRefusedReason : 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 CatalogNegotiationRefusedReason(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 caller's protocol version is below the lowest this runtime serves. - public static CatalogNegotiationRefusedReason UnsupportedProtocolVersion { get; } = new("unsupported-protocol-version"); + /// 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 caller requires at least one capability this runtime cannot honour. - public static CatalogNegotiationRefusedReason UnsupportedCapability { get; } = new("unsupported-capability"); + /// The request is unsupported or malformed. + public static SkillInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => left.Equals(right); + /// The bounded operation limit was reached. + public static SkillInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => !(left == right); + /// The original operation was cancelled. + public static SkillInstallationFailureReason Cancelled { get; } = new("cancelled"); + + /// The original host cannot receive human confirmation. + public static SkillInstallationFailureReason ConfirmationUnavailable { get; } = new("confirmation-unavailable"); + + /// The confirmation response is malformed or mismatched. + public static SkillInstallationFailureReason ConfirmationInvalid { get; } = new("confirmation-invalid"); + + /// Existing authenticated session and host authority is unavailable. + public static SkillInstallationFailureReason PolicyContextUnavailable { get; } = new("policy-context-unavailable"); + + /// The original authority or policy changed. + public static SkillInstallationFailureReason PolicyChanged { get; } = new("policy-changed"); + + /// No matching owned resource or original operation exists. + public static SkillInstallationFailureReason ResourceNotFound { get; } = new("resource-not-found"); + + /// The original plan deadline elapsed. + public static SkillInstallationFailureReason PlanExpired { get; } = new("plan-expired"); + + /// The one-use plan was already consumed. + public static SkillInstallationFailureReason PlanReplayed { get; } = new("plan-replayed"); + + /// The handle belongs to a different runtime, session or connection. + public static SkillInstallationFailureReason ForeignRuntime { get; } = new("foreign-runtime"); + + /// The handle is not the expected Skill handle kind. + public static SkillInstallationFailureReason WrongKind { get; } = new("wrong-kind"); + + /// The handle was minted for a different search result or authority. + public static SkillInstallationFailureReason SearchMismatch { get; } = new("search-mismatch"); + + /// The handle or operation expired. + public static SkillInstallationFailureReason Expired { get; } = new("expired"); + + /// The candidate handle is not a verified installable Skill candidate. + public static SkillInstallationFailureReason InvalidCandidate { get; } = new("invalid-candidate"); + + /// The verified Skill descriptor could not be retrieved safely. + public static SkillInstallationFailureReason DescriptorUnavailable { get; } = new("descriptor-unavailable"); + + /// The verified Skill descriptor failed validation. + public static SkillInstallationFailureReason DescriptorInvalid { get; } = new("descriptor-invalid"); + + /// The Skill entrypoint could not be retrieved or verified. + public static SkillInstallationFailureReason EntrypointUnavailable { get; } = new("entrypoint-unavailable"); + + /// The Skill entrypoint is not a valid Skill. + public static SkillInstallationFailureReason InvalidSkill { get; } = new("invalid-skill"); + + /// The reviewed Skill payload could not be acquired. + public static SkillInstallationFailureReason PayloadUnavailable { get; } = new("payload-unavailable"); + + /// The acquired payload no longer matches the reviewed descriptor. + public static SkillInstallationFailureReason PayloadMismatch { get; } = new("payload-mismatch"); + + /// The retained Skill source changed after planning. + public static SkillInstallationFailureReason SourceChanged { get; } = new("source-changed"); + + /// The selected runtime cannot inspect or control the requested lifecycle operation. + public static SkillInstallationFailureReason LifecycleUnavailable { get; } = new("lifecycle-unavailable"); + + /// Durable Skill installation evidence requires recovery before mutation. + public static SkillInstallationFailureReason RecoveryRequired { get; } = new("recovery-required"); + + /// The Skill entrypoint exceeds the bounded complete review size. + public static SkillInstallationFailureReason ReviewTooLarge { get; } = new("review-too-large"); + + /// Skill installation storage or admission is busy. + public static SkillInstallationFailureReason Busy { get; } = new("busy"); + + /// An owned Skill with the same identity or target already exists. + public static SkillInstallationFailureReason AlreadyInstalled { get; } = new("already-installed"); + + /// Installed Skill files no longer match ownership evidence. + public static SkillInstallationFailureReason ConfigurationModified { get; } = new("configuration-modified"); + + /// A storage operation failed; inspect durable state before retrying. + public static SkillInstallationFailureReason WriteFailed { get; } = new("write-failed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillInstallationFailureReason left, SkillInstallationFailureReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInstallationFailureReason left, SkillInstallationFailureReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogNegotiationRefusedReason other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationFailureReason other && Equals(other); /// - public bool Equals(CatalogNegotiationRefusedReason 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); @@ -24724,65 +30999,68 @@ 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 SkillInstallationFailureReason 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, SkillInstallationFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNegotiationRefusedReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationFailureReason)); } } } -/// Which kind of opaque handle was presented. +/// Which tier this directory belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogHandleType : 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 CatalogHandleType(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 search candidate handle. - public static CatalogHandleType Candidate { get; } = new("candidate"); + /// A project's repository skill directory. + public static SkillDiscoveryScope Project { get; } = new("project"); - /// An install plan handle. - public static CatalogHandleType Plan { get; } = new("plan"); + /// The user's personal Copilot skill directory. + public static SkillDiscoveryScope PersonalCopilot { get; } = new("personal-copilot"); - /// A model-safe reference to one retained search candidate. - public static CatalogHandleType Selection { get; } = new("selection"); + /// The user's personal agents skill directory. + public static SkillDiscoveryScope PersonalAgents { get; } = new("personal-agents"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogHandleType left, CatalogHandleType right) => left.Equals(right); + /// A configured custom skill directory. + public static SkillDiscoveryScope Custom { get; } = new("custom"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogHandleType left, CatalogHandleType right) => !(left == right); + /// Returns a value indicating whether two instances are 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 !=(SkillDiscoveryScope left, SkillDiscoveryScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogHandleType other && Equals(other); + public override bool Equals(object? obj) => obj is SkillDiscoveryScope other && Equals(other); /// - public bool Equals(CatalogHandleType 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); @@ -24790,74 +31068,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 SkillDiscoveryScope 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, SkillDiscoveryScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillDiscoveryScope)); } } } -/// Why a presented handle was rejected. +/// 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 CatalogHandleRejectionReason : 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 CatalogHandleRejectionReason(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 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"); + /// An install plan can be computed for this MCP server candidate. + public static CatalogMcpServerInstallability Installable { get; } = new("installable"); - /// The supplied search identifier does not match the retained candidate. - public static CatalogHandleRejectionReason SearchMismatch { get; } = new("search-mismatch"); + /// 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 ==(CatalogHandleRejectionReason left, CatalogHandleRejectionReason 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 !=(CatalogHandleRejectionReason left, CatalogHandleRejectionReason 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 CatalogHandleRejectionReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMcpServerInstallability other && Equals(other); /// - public bool Equals(CatalogHandleRejectionReason 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); @@ -24865,89 +31131,71 @@ public CatalogHandleRejectionReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogHandleRejectionReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogMcpServerInstallability 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, CatalogMcpServerInstallability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleRejectionReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMcpServerInstallability)); } } } -/// Which request field was rejected locally or by the catalog authority. +/// Typed installability state for an AI skill candidate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogInvalidRequestField : 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 CatalogInvalidRequestField(string value) + public CatalogAiSkillInstallability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The 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"); + /// This AI skill candidate carries verified materialisation metadata and the selected session may plan installation. + public static CatalogAiSkillInstallability Installable { get; } = new("installable"); - /// 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"); + /// Skill installation is understood but disabled for the selected session. + public static CatalogAiSkillInstallability FeatureDisabled { get; } = new("feature-disabled"); - /// The locally owned session identifier was missing or malformed. - public static CatalogInvalidRequestField SessionId { get; } = new("sessionId"); + /// The candidate lacks verified materialisation metadata required for installation. + public static CatalogAiSkillInstallability MaterialisationUnavailable { get; } = new("materialisation-unavailable"); - /// The opaque selection reference was missing or malformed. - public static CatalogInvalidRequestField SelectionRef { get; } = new("selectionRef"); + /// Policy refuses Skill installation for the selected session or authority. + public static CatalogAiSkillInstallability PolicyForbids { get; } = new("policy-forbids"); - /// The terminal selection outcome was missing or unsupported. - public static CatalogInvalidRequestField Outcome { get; } = new("outcome"); + /// 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 ==(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => left.Equals(right); + /// 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 !=(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => !(left == 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 CatalogInvalidRequestField other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAiSkillInstallability other && Equals(other); /// - public bool Equals(CatalogInvalidRequestField 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); @@ -24955,65 +31203,59 @@ 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 CatalogAiSkillInstallability 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, CatalogAiSkillInstallability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogInvalidRequestField)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillInstallability)); } } } -/// Why the catalog authority did not accept the caller's identity. +/// Canonical AI skill media type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAuthenticationRequiredReason : 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 CatalogAuthenticationRequiredReason(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; - /// No credential was presented, so there is nothing to refresh and the caller must sign in. - public static CatalogAuthenticationRequiredReason NoCredential { get; } = new("no-credential"); - - /// 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"); - - /// 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"); + /// 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 ==(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason 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 !=(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason 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 CatalogAuthenticationRequiredReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAiSkillMediaType other && Equals(other); /// - public bool Equals(CatalogAuthenticationRequiredReason 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); @@ -25021,89 +31263,65 @@ 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 CatalogAiSkillMediaType 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, CatalogAiSkillMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAuthenticationRequiredReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillMediaType)); } } } -/// Categorised network failure, low cardinality so it can be aggregated without carrying a URL. +/// 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 CatalogNetworkFailureReason : 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 CatalogNetworkFailureReason(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; - /// 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 plugin contributes at least one GitHub Copilot Canvas. + public static CatalogAgentPluginCompatibilityTag Canvas { get; } = new("canvas"); - /// The response exceeded the permitted size. - public static CatalogNetworkFailureReason ResponseTooLarge { get; } = new("response-too-large"); + /// The plugin depends on Canvas for its intended functionality. + public static CatalogAgentPluginCompatibilityTag CanvasOnly { get; } = new("canvas-only"); - /// A redirect was refused by the runtime's redirect policy. - public static CatalogNetworkFailureReason RedirectRejected { get; } = new("redirect-rejected"); + /// 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 ==(CatalogNetworkFailureReason left, CatalogNetworkFailureReason 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 !=(CatalogNetworkFailureReason left, CatalogNetworkFailureReason 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 CatalogNetworkFailureReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAgentPluginCompatibilityTag other && Equals(other); /// - public bool Equals(CatalogNetworkFailureReason 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); @@ -25111,74 +31329,59 @@ public CatalogNetworkFailureReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogNetworkFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogAgentPluginCompatibilityTag 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, CatalogAgentPluginCompatibilityTag value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNetworkFailureReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginCompatibilityTag)); } } } -/// Which hardened-fetch control refused a retrieval. +/// Canonical Agent Plugin media type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnsafeRetrievalReason : 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 CatalogUnsafeRetrievalReason(string value) + public CatalogAgentPluginMediaType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The URL used a scheme the runtime refuses to fetch. - public static CatalogUnsafeRetrievalReason BlockedScheme { get; } = new("blocked-scheme"); - - /// The URL embedded credentials. - public static CatalogUnsafeRetrievalReason CredentialsInUrl { get; } = new("credentials-in-url"); - - /// The URL resolved to a loopback, private, link-local, or cloud metadata address. - public static CatalogUnsafeRetrievalReason BlockedAddress { get; } = new("blocked-address"); - - /// A redirect target resolved to a blocked address. - public static CatalogUnsafeRetrievalReason RedirectToBlockedAddress { get; } = new("redirect-to-blocked-address"); - - /// The configured proxy policy refused the request. - public static CatalogUnsafeRetrievalReason ProxyRejected { get; } = new("proxy-rejected"); - - /// The authority is not permitted for card retrieval. - public static CatalogUnsafeRetrievalReason HostNotPermitted { get; } = new("host-not-permitted"); + /// 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 ==(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason 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 !=(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason 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 CatalogUnsafeRetrievalReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAgentPluginMediaType other && Equals(other); /// - public bool Equals(CatalogUnsafeRetrievalReason 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); @@ -25186,68 +31389,59 @@ 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 CatalogAgentPluginMediaType 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, CatalogAgentPluginMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnsafeRetrievalReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginMediaType)); } } } -/// Media type a catalog card is interpreted as. +/// 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 CatalogMediaType : 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 CatalogMediaType(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; - /// The current MCP server card media type. - public static CatalogMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); - - /// The legacy MCP server card media type, accepted for compatibility. - public static CatalogMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); - - /// An AI skill card. Representable and searchable, but typed non-installable. - public static CatalogMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); - - /// An inert Agent Plugin descriptor. - public static CatalogMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); + /// 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 ==(CatalogMediaType left, CatalogMediaType 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 !=(CatalogMediaType left, CatalogMediaType 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 CatalogMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogSearchTotalCountRelation other && Equals(other); /// - public bool Equals(CatalogMediaType 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); @@ -25255,71 +31449,65 @@ 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 CatalogSearchTotalCountRelation 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, CatalogSearchTotalCountRelation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSearchTotalCountRelation)); } } } -/// How a card failed validation. +/// What kind of resource a catalog candidate describes. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogMalformedCardReason : 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 CatalogMalformedCardReason(string value) + public CatalogCandidateKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The document is not well-formed JSON. - public static CatalogMalformedCardReason InvalidJson { get; } = new("invalid-json"); - - /// The document does not satisfy its media type's schema. - public static CatalogMalformedCardReason SchemaViolation { get; } = new("schema-violation"); - - /// The declared media type is not one this runtime understands. - public static CatalogMalformedCardReason UnsupportedMediaType { get; } = new("unsupported-media-type"); + /// An MCP server, which can be planned for installation. + public static CatalogCandidateKind McpServer { get; } = new("mcp-server"); - /// A field the media type requires is absent. - public static CatalogMalformedCardReason MissingRequiredField { get; } = new("missing-required-field"); + /// An AI skill, which is discoverable but not installable through this surface. + public static CatalogCandidateKind AiSkill { get; } = new("ai-skill"); - /// The document exceeded the permitted size. - public static CatalogMalformedCardReason SizeLimitExceeded { get; } = new("size-limit-exceeded"); + /// An inert Agent Plugin candidate, available only when explicitly requested. + public static CatalogCandidateKind Plugin { get; } = new("plugin"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogCandidateKind left, CatalogCandidateKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogCandidateKind left, CatalogCandidateKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogMalformedCardReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogCandidateKind other && Equals(other); /// - public bool Equals(CatalogMalformedCardReason 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); @@ -25327,68 +31515,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 CatalogCandidateKind 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, CatalogCandidateKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMalformedCardReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCandidateKind)); } } } -/// Which wire-contract rule an upstream response broke. +/// Terminal outcome declared for a retained catalog selection group. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogContractViolationReason : 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 CatalogContractViolationReason(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; - /// A result carried both a URL and embedded data, when exactly one is permitted. - public static CatalogContractViolationReason BothUrlAndData { get; } = new("both-url-and-data"); + /// Choose the candidate named by selectionRef. + public static CatalogSelectionDecision Selected { get; } = new("selected"); - /// A result carried neither a URL nor embedded data, when exactly one is required. - public static CatalogContractViolationReason NeitherUrlNorData { get; } = new("neither-url-nor-data"); + /// Explicitly decline every candidate in the search. + public static CatalogSelectionDecision Declined { get; } = new("declined"); - /// 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"); + /// Cancel the selection interaction without choosing a candidate. + public static CatalogSelectionDecision Cancelled { get; } = new("cancelled"); - /// A result declared no media type, or one this contract does not model. - public static CatalogContractViolationReason UnknownMediaType { get; } = new("unknown-media-type"); + /// 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 ==(CatalogContractViolationReason left, CatalogContractViolationReason 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 !=(CatalogContractViolationReason left, CatalogContractViolationReason 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 CatalogContractViolationReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogSelectionDecision other && Equals(other); /// - public bool Equals(CatalogContractViolationReason 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); @@ -25396,65 +31584,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 CatalogSelectionDecision 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, CatalogSelectionDecision value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogContractViolationReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSelectionDecision)); } } } -/// Why no usable transport could be offered. +/// Where completed plugin content was staged before atomic promotion. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnavailableTransportReason : 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 CatalogUnavailableTransportReason(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 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"); + /// A sibling of the installed-plugins root, outside the recursively watched tree. + public static PluginInstallStagingMode External { get; } = new("external"); - /// Eligible remotes could not be enumerated, so no explicit choice can be offered. - public static CatalogUnavailableTransportReason RemoteEnumerationUnavailable { get; } = new("remote-enumeration-unavailable"); + /// 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 ==(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason 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 !=(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason 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 CatalogUnavailableTransportReason other && Equals(other); + public override bool Equals(object? obj) => obj is PluginInstallStagingMode other && Equals(other); /// - public bool Equals(CatalogUnavailableTransportReason 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); @@ -25462,65 +31647,74 @@ 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 PluginInstallStagingMode 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, PluginInstallStagingMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableTransportReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); } } } -/// Why a discoverable candidate cannot be installed. +/// Where the agent definition was loaded from. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNotInstallableReason : 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 CatalogNotInstallableReason(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; - /// This kind of resource is not installable through this surface. - public static CatalogNotInstallableReason KindNotInstallable { get; } = new("kind-not-installable"); + /// Agent loaded from the user's personal agent configuration. + public static AgentInfoSource User { get; } = new("user"); - /// AI skills are discoverable but have no typed importer in this phase. - public static CatalogNotInstallableReason AiSkillNotInstallable { get; } = new("ai-skill-not-installable"); + /// Agent loaded from the current project's repository configuration. + public static AgentInfoSource Project { get; } = new("project"); - /// Policy forbids installing this candidate. - public static CatalogNotInstallableReason PolicyForbids { get; } = new("policy-forbids"); + /// Agent inherited from a parent project or workspace. + public static AgentInfoSource Inherited { get; } = new("inherited"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => left.Equals(right); + /// Agent provided by a remote runtime or service. + public static AgentInfoSource Remote { get; } = new("remote"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => !(left == right); + /// 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 CatalogNotInstallableReason other && Equals(other); + public override bool Equals(object? obj) => obj is AgentInfoSource other && Equals(other); /// - public bool Equals(CatalogNotInstallableReason 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); @@ -25528,71 +31722,62 @@ 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 AgentInfoSource 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, AgentInfoSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNotInstallableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentInfoSource)); } } } -/// Why a catalog operation is not available on this runtime. +/// Which tier this directory belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnavailableReason : 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 CatalogUnavailableReason(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; - /// 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 user's personal agent configuration directory. + public static AgentDiscoveryPathScope User { get; } = new("user"); - /// The surface is disabled by policy on this runtime. - public static CatalogUnavailableReason DisabledByPolicy { get; } = new("disabled-by-policy"); + /// A project's repository agent directory. + public static AgentDiscoveryPathScope Project { get; } = new("project"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogUnavailableReason left, CatalogUnavailableReason 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 !=(CatalogUnavailableReason left, CatalogUnavailableReason 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 CatalogUnavailableReason other && Equals(other); + public override bool Equals(object? obj) => obj is AgentDiscoveryPathScope other && Equals(other); /// - public bool Equals(CatalogUnavailableReason 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); @@ -25600,62 +31785,68 @@ 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 AgentDiscoveryPathScope 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, AgentDiscoveryPathScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentDiscoveryPathScope)); } } } -/// Persisted extension discovery source. +/// Where this source lives — used for UI grouping. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiscoveredExtensionSource : 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 DiscoveredExtensionSource(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; - /// Extension discovered from the user's extensions directory. - public static DiscoveredExtensionSource User { get; } = new("user"); + /// Instructions live in user-level configuration. + public static InstructionSourceLocation User { get; } = new("user"); - /// Extension contributed by an installed plugin. - public static DiscoveredExtensionSource Plugin { get; } = new("plugin"); + /// Instructions live in repository-level configuration. + public static InstructionSourceLocation Repository { get; } = new("repository"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiscoveredExtensionSource left, DiscoveredExtensionSource right) => left.Equals(right); + /// Instructions live under the current working directory. + public static InstructionSourceLocation WorkingDirectory { get; } = new("working-directory"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiscoveredExtensionSource left, DiscoveredExtensionSource right) => !(left == right); + /// Instructions live in plugin-provided configuration. + public static InstructionSourceLocation Plugin { get; } = new("plugin"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(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 DiscoveredExtensionSource other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionSourceLocation other && Equals(other); /// - public bool Equals(DiscoveredExtensionSource 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); @@ -25663,65 +31854,77 @@ 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 InstructionSourceLocation 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, InstructionSourceLocation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceLocation)); } } } -/// Effective extension loading and agent-management mode. +/// Category of instruction source — used for merge logic. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiscoveredExtensionMode : 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 DiscoveredExtensionMode(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; - /// Extensions are not loaded. - public static DiscoveredExtensionMode Disabled { get; } = new("disabled"); + /// Instructions loaded from the user's home configuration. + public static InstructionSourceType Home { get; } = new("home"); - /// Extensions are loaded, but the agent cannot create, reload, or manage them. - public static DiscoveredExtensionMode LoadOnly { get; } = new("load_only"); + /// Instructions loaded from repository-scoped files. + public static InstructionSourceType Repo { get; } = new("repo"); - /// Extensions are loaded and the agent can create, reload, and manage them. - public static DiscoveredExtensionMode LoadAndAugment { get; } = new("load_and_augment"); + /// Instructions loaded from model-specific files. + public static InstructionSourceType Model { get; } = new("model"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiscoveredExtensionMode left, DiscoveredExtensionMode right) => left.Equals(right); + /// Instructions loaded from VS Code instruction files. + public static InstructionSourceType Vscode { get; } = new("vscode"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiscoveredExtensionMode left, DiscoveredExtensionMode right) => !(left == right); + /// 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 DiscoveredExtensionMode other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionSourceType other && Equals(other); /// - public bool Equals(DiscoveredExtensionMode 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); @@ -25729,62 +31932,62 @@ 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 InstructionSourceType 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, InstructionSourceType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceType)); } } } -/// Whether an MCP server candidate can be planned for installation. +/// 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 CatalogMcpServerInstallability : 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 CatalogMcpServerInstallability(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; - /// An install plan can be computed for this MCP server candidate. - public static CatalogMcpServerInstallability Installable { get; } = new("installable"); + /// The target is a single instruction file. + public static InstructionDiscoveryPathKind File { get; } = new("file"); - /// Policy forbids installing this MCP server candidate. - public static CatalogMcpServerInstallability NotInstallablePolicy { get; } = new("not-installable-policy"); + /// 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 ==(CatalogMcpServerInstallability left, CatalogMcpServerInstallability 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 !=(CatalogMcpServerInstallability left, CatalogMcpServerInstallability 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 CatalogMcpServerInstallability other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionDiscoveryPathKind other && Equals(other); /// - public bool Equals(CatalogMcpServerInstallability 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); @@ -25792,68 +31995,68 @@ public CatalogMcpServerInstallability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogMcpServerInstallability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override InstructionDiscoveryPathKind 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, InstructionDiscoveryPathKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMcpServerInstallability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathKind)); } } } -/// Authority-computed exposure eligibility, kept separate from tier. The current tier-only Agent Finder response maps to `unknown`, never to a locally inferred eligibility. +/// Which tier this target belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustEligibility : 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 CatalogTrustEligibility(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; - /// Eligible for default catalogue exposure. - public static CatalogTrustEligibility Default { get; } = new("default"); + /// Instructions live in user-level configuration. + public static InstructionDiscoveryPathLocation User { get; } = new("user"); - /// Eligible only when expanded or community results are requested. - public static CatalogTrustEligibility Expanded { get; } = new("expanded"); + /// Instructions live in repository-level configuration. + public static InstructionDiscoveryPathLocation Repository { get; } = new("repository"); - /// Not eligible for normal catalogue exposure. - public static CatalogTrustEligibility Hidden { get; } = new("hidden"); + /// Instructions live under the current working directory. + public static InstructionDiscoveryPathLocation WorkingDirectory { get; } = new("working-directory"); - /// The authority did not supply an eligibility decision. - public static CatalogTrustEligibility 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 ==(CatalogTrustEligibility left, CatalogTrustEligibility 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 !=(CatalogTrustEligibility left, CatalogTrustEligibility 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 CatalogTrustEligibility other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionDiscoveryPathLocation other && Equals(other); /// - public bool Equals(CatalogTrustEligibility 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); @@ -25861,59 +32064,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 InstructionDiscoveryPathLocation 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, InstructionDiscoveryPathLocation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustEligibility)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathLocation)); } } } -/// Bounded authority that supplied a catalogue trust observation. +/// 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 CatalogTrustSource : 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 CatalogTrustSource(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; - /// GitHub Agent Finder supplied the trust field on its search result. - public static CatalogTrustSource AgentFinder { get; } = new("agent-finder"); + /// 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 ==(CatalogTrustSource left, CatalogTrustSource 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 !=(CatalogTrustSource left, CatalogTrustSource 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 CatalogTrustSource other && Equals(other); + public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); /// - public bool Equals(CatalogTrustSource 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); @@ -25921,59 +32124,65 @@ 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 SlashCommandInputCompletion 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, SlashCommandInputCompletion value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); } } } -/// Schema version of the catalogue trust snapshot envelope. +/// 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 CatalogTrustSnapshotSchemaVersion : 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 CatalogTrustSnapshotSchemaVersion(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; - /// Initial envelope carrying one bounded service tier or one explicit unavailable state. - public static CatalogTrustSnapshotSchemaVersion V1 { get; } = new("v1"); + /// Command implemented by the runtime. + public static SlashCommandKind Builtin { get; } = new("builtin"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion right) => left.Equals(right); + /// Command backed by a skill. + public static SlashCommandKind Skill { get; } = new("skill"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion right) => !(left == right); + /// 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 ==(SlashCommandKind left, SlashCommandKind right) => left.Equals(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 CatalogTrustSnapshotSchemaVersion other && Equals(other); + public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); /// - public bool Equals(CatalogTrustSnapshotSchemaVersion 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); @@ -25981,62 +32190,62 @@ 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 SlashCommandKind 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, SlashCommandKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSnapshotSchemaVersion)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); } } } -/// Service-computed trust tier currently emitted by Agent Finder. It is independent of search score, popularity, and client-side ranking. +/// Hosting platform type of the repository. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustTier : IEquatable +public readonly struct SessionWorkingDirectoryContextHostType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogTrustTier(string value) + public SessionWorkingDirectoryContextHostType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Tier one as assigned by the catalogue authority. - public static CatalogTrustTier T1 { get; } = new("T1"); + /// The working directory repository is hosted on GitHub. + public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github"); - /// Tier two as assigned by the catalogue authority. - public static CatalogTrustTier T2 { get; } = new("T2"); + /// The working directory repository is hosted on Azure DevOps. + public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogTrustTier left, CatalogTrustTier right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogTrustTier left, CatalogTrustTier right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogTrustTier other && Equals(other); + public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other); /// - public bool Equals(CatalogTrustTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionWorkingDirectoryContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26044,59 +32253,62 @@ 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 SessionWorkingDirectoryContextHostType 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, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustTier)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType)); } } } -/// Typed non-installable state for an AI skill candidate. +/// Severity of a managed-settings validation finding. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAiSkillInstallability : 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 CatalogAiSkillInstallability(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; - /// AI skills are discovery-only on this surface. - public static CatalogAiSkillInstallability NotInstallableKind { get; } = new("not-installable-kind"); + /// The runtime rejects the document. + public static ManagedSettingsDiagnosticSeverity Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAiSkillInstallability left, CatalogAiSkillInstallability right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(CatalogAiSkillInstallability left, CatalogAiSkillInstallability right) => !(left == 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 !=(ManagedSettingsDiagnosticSeverity left, ManagedSettingsDiagnosticSeverity right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAiSkillInstallability other && Equals(other); + public override bool Equals(object? obj) => obj is ManagedSettingsDiagnosticSeverity other && Equals(other); /// - public bool Equals(CatalogAiSkillInstallability 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); @@ -26104,59 +32316,65 @@ 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 ManagedSettingsDiagnosticSeverity 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, ManagedSettingsDiagnosticSeverity value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillInstallability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsDiagnosticSeverity)); } } } -/// Canonical AI skill media type. +/// A channel accepted by managedSettings.compose. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAiSkillMediaType : 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 CatalogAiSkillMediaType(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; - /// An AI skill card. - public static CatalogAiSkillMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); + /// Device policy, the strongest channel. + public static ManagedSettingsChannel Device { get; } = new("device"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => left.Equals(right); + /// Account or organization policy. + public static ManagedSettingsChannel Server { get; } = new("server"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => !(left == right); + /// 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 ==(ManagedSettingsChannel left, ManagedSettingsChannel right) => left.Equals(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 CatalogAiSkillMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is ManagedSettingsChannel other && Equals(other); /// - public bool Equals(CatalogAiSkillMediaType 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); @@ -26164,65 +32382,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 ManagedSettingsChannel 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, ManagedSettingsChannel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsChannel)); } } } -/// Explicit Agent Plugin compatibility declared by exact catalog tags. Clients must not infer these values from display text or other metadata. +/// Path conventions used by this filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAgentPluginCompatibilityTag : 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 CatalogAgentPluginCompatibilityTag(string value) + public SessionFsSetProviderConventions(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The plugin contributes at least one GitHub Copilot Canvas. - public static CatalogAgentPluginCompatibilityTag Canvas { get; } = new("canvas"); - - /// The plugin depends on Canvas for its intended functionality. - public static CatalogAgentPluginCompatibilityTag CanvasOnly { get; } = new("canvas-only"); - - /// The plugin targets GitHub Copilot. - public static CatalogAgentPluginCompatibilityTag GitHubCopilot { get; } = new("github-copilot"); + /// 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 ==(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag 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 !=(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag 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 CatalogAgentPluginCompatibilityTag other && Equals(other); + public override bool Equals(object? obj) => obj is SessionFsSetProviderConventions other && Equals(other); /// - public bool Equals(CatalogAgentPluginCompatibilityTag 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); @@ -26230,59 +32445,62 @@ 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 SessionFsSetProviderConventions 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, SessionFsSetProviderConventions value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginCompatibilityTag)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionFsSetProviderConventions)); } } } -/// Canonical Agent Plugin media type. +/// Repository host type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAgentPluginMediaType : 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 CatalogAgentPluginMediaType(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; - /// A GitHub Copilot Agent Plugin descriptor. - public static CatalogAgentPluginMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); + /// Session repository is hosted on GitHub. + public static SessionContextHostType GitHub { get; } = new("github"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => left.Equals(right); + /// Session repository is hosted on Azure DevOps. + public static SessionContextHostType Ado { get; } = new("ado"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => !(left == 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 !=(SessionContextHostType left, SessionContextHostType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAgentPluginMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is SessionContextHostType other && Equals(other); /// - public bool Equals(CatalogAgentPluginMediaType 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); @@ -26290,59 +32508,68 @@ 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 SessionContextHostType 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, SessionContextHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionContextHostType)); } } } -/// Relationship of the backend-reported count to the complete query result set. +/// 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 CatalogSearchTotalCountRelation : 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 CatalogSearchTotalCountRelation(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; - /// No exact/full-query or lower-bound guarantee is available. - public static CatalogSearchTotalCountRelation Unknown { get; } = new("unknown"); + /// No turn is running. + public static RemoteSessionHostStatus Idle { get; } = new("idle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogSearchTotalCountRelation left, CatalogSearchTotalCountRelation 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 !=(CatalogSearchTotalCountRelation left, CatalogSearchTotalCountRelation 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 CatalogSearchTotalCountRelation other && Equals(other); + public override bool Equals(object? obj) => obj is RemoteSessionHostStatus other && Equals(other); /// - public bool Equals(CatalogSearchTotalCountRelation 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); @@ -26350,65 +32577,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 RemoteSessionHostStatus 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, RemoteSessionHostStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSearchTotalCountRelation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionHostStatus)); } } } -/// What kind of resource a catalog candidate describes. +/// 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 CatalogCandidateKind : 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 CatalogCandidateKind(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; - /// 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"); + /// GitHub Copilot coding agent task. + public static RemoteSessionMetadataTaskType Cca { get; } = new("cca"); - /// An inert Agent Plugin candidate, available only when explicitly requested. - public static CatalogCandidateKind Plugin { get; } = new("plugin"); + /// CLI remote task. + public static RemoteSessionMetadataTaskType Cli { get; } = new("cli"); - /// 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 ==(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType 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 !=(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogCandidateKind other && Equals(other); + public override bool Equals(object? obj) => obj is RemoteSessionMetadataTaskType other && Equals(other); /// - public bool Equals(CatalogCandidateKind 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); @@ -26416,68 +32640,62 @@ public CatalogCandidateKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogCandidateKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override RemoteSessionMetadataTaskType 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, RemoteSessionMetadataTaskType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCandidateKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionMetadataTaskType)); } } } -/// Terminal outcome declared for a retained catalog selection group. +/// Step status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogSelectionDecision : 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 CatalogSelectionDecision(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; - /// Choose the candidate named by selectionRef. - public static CatalogSelectionDecision Selected { get; } = new("selected"); - - /// Explicitly decline every candidate in the search. - public static CatalogSelectionDecision Declined { get; } = new("declined"); - - /// Cancel the selection interaction without choosing a candidate. - public static CatalogSelectionDecision Cancelled { get; } = new("cancelled"); + /// The step has started and has not yet finished. + public static SessionsOpenProgressStatus InProgress { get; } = new("in-progress"); - /// Declare that the host's live interaction deadline elapsed while the reference remained valid. - public static CatalogSelectionDecision TimedOut { get; } = new("timed-out"); + /// 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 ==(CatalogSelectionDecision left, CatalogSelectionDecision 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 !=(CatalogSelectionDecision left, CatalogSelectionDecision 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 CatalogSelectionDecision other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenProgressStatus other && Equals(other); /// - public bool Equals(CatalogSelectionDecision 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); @@ -26485,62 +32703,74 @@ 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 SessionsOpenProgressStatus 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, SessionsOpenProgressStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSelectionDecision)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStatus)); } } } -/// Where completed plugin content was staged before atomic promotion. +/// Handoff step. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PluginInstallStagingMode : 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 PluginInstallStagingMode(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; - /// A sibling of the installed-plugins root, outside the recursively watched tree. - public static PluginInstallStagingMode External { get; } = new("external"); + /// Loading the source session's events from the remote service. + public static SessionsOpenProgressStep LoadSession { get; } = new("load-session"); - /// A sibling of the destination plugin directory, used when external staging is unavailable. - public static PluginInstallStagingMode DestinationSibling { get; } = new("destination_sibling"); + /// Validating that the local repository matches the remote session's repository. + public static SessionsOpenProgressStep ValidateRepo { get; } = new("validate-repo"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PluginInstallStagingMode left, PluginInstallStagingMode right) => left.Equals(right); + /// Checking the local working tree for uncommitted changes that would block the handoff. + public static SessionsOpenProgressStep CheckChanges { get; } = new("check-changes"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PluginInstallStagingMode left, PluginInstallStagingMode right) => !(left == right); + /// Checking out the branch associated with the remote session in the local working tree. + public static SessionsOpenProgressStep CheckoutBranch { get; } = new("checkout-branch"); + + /// Creating the new local session and seeding it with the source session's events. + public static SessionsOpenProgressStep CreateSession { get; } = new("create-session"); + + /// Persisting the newly-created local session to disk. + public static SessionsOpenProgressStep SaveSession { get; } = new("save-session"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PluginInstallStagingMode other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenProgressStep other && Equals(other); /// - public bool Equals(PluginInstallStagingMode 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); @@ -26548,68 +32778,71 @@ 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 SessionsOpenProgressStep 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, SessionsOpenProgressStep value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStep)); } } } -/// Which tier this directory belongs to. +/// Outcome of the open request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillDiscoveryScope : 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 SkillDiscoveryScope(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 project's repository skill directory. - public static SkillDiscoveryScope Project { get; } = new("project"); + /// A new session was created. + public static SessionsOpenStatus Created { get; } = new("created"); - /// The user's personal Copilot skill directory. - public static SkillDiscoveryScope PersonalCopilot { get; } = new("personal-copilot"); + /// An existing session was loaded or reattached. + public static SessionsOpenStatus Resumed { get; } = new("resumed"); - /// The user's personal agents skill directory. - public static SkillDiscoveryScope PersonalAgents { get; } = new("personal-agents"); + /// No matching persisted session was found. + public static SessionsOpenStatus NotFound { get; } = new("not_found"); - /// A configured custom skill directory. - public static SkillDiscoveryScope Custom { get; } = new("custom"); + /// Connected to an existing remote session. + public static SessionsOpenStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SkillDiscoveryScope left, SkillDiscoveryScope right) => left.Equals(right); + /// Remote session was handed off to a new local session. + public static SessionsOpenStatus HandedOff { get; } = new("handed_off"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SkillDiscoveryScope left, SkillDiscoveryScope right) => !(left == right); + /// 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 SkillDiscoveryScope other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenStatus other && Equals(other); /// - public bool Equals(SkillDiscoveryScope 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); @@ -26617,74 +32850,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 SessionsOpenStatus 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, SessionsOpenStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillDiscoveryScope)); + 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); @@ -26692,62 +32913,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); @@ -26755,68 +32979,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); @@ -26824,77 +33042,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); @@ -26902,62 +33105,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); @@ -26965,68 +33177,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); @@ -27034,59 +33240,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); @@ -27094,65 +33303,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); @@ -27160,62 +33372,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); @@ -27223,62 +33438,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); @@ -27286,68 +33510,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); @@ -27355,62 +33585,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); @@ -27418,62 +33648,65 @@ 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. +/// Availability. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionsOpenProgressStatus : IEquatable +public readonly struct ConnectorDiscoveryAvailability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionsOpenProgressStatus(string value) + public ConnectorDiscoveryAvailability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The step has started and has not yet finished. - public static SessionsOpenProgressStatus InProgress { get; } = new("in-progress"); + /// Enabled. + public static ConnectorDiscoveryAvailability Enabled { get; } = new("enabled"); - /// The step has completed successfully. - public static SessionsOpenProgressStatus Complete { get; } = new("complete"); + /// Disabled. + public static ConnectorDiscoveryAvailability Disabled { get; } = new("disabled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => left.Equals(right); + /// Unavailable. + public static ConnectorDiscoveryAvailability Unavailable { get; } = new("unavailable"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionsOpenProgressStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorDiscoveryAvailability other && Equals(other); /// - public bool Equals(SessionsOpenProgressStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorDiscoveryAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27481,74 +33714,83 @@ 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 ConnectorDiscoveryAvailability 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, ConnectorDiscoveryAvailability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorDiscoveryAvailability)); } } } -/// Handoff step. +/// Authentication type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionsOpenProgressStep : IEquatable +public readonly struct AuthInfoType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionsOpenProgressStep(string value) + public AuthInfoType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Loading the source session's events from the remote service. - public static SessionsOpenProgressStep LoadSession { get; } = new("load-session"); + /// Authentication provided by a GitHub App HMAC credential. + public static AuthInfoType Hmac { get; } = new("hmac"); - /// Validating that the local repository matches the remote session's repository. - public static SessionsOpenProgressStep ValidateRepo { get; } = new("validate-repo"); + /// Authentication resolved from environment-provided credentials. + public static AuthInfoType Env { get; } = new("env"); - /// Checking the local working tree for uncommitted changes that would block the handoff. - public static SessionsOpenProgressStep CheckChanges { get; } = new("check-changes"); + /// Authentication from an interactive user sign-in. + public static AuthInfoType User { get; } = new("user"); - /// Checking out the branch associated with the remote session in the local working tree. - public static SessionsOpenProgressStep CheckoutBranch { get; } = new("checkout-branch"); + /// Authentication from a selected provider-owned account, without a GitHub credential. + public static AuthInfoType Account { get; } = new("account"); - /// Creating the new local session and seeding it with the source session's events. - public static SessionsOpenProgressStep CreateSession { get; } = new("create-session"); + /// Authentication delegated to the GitHub CLI. + public static AuthInfoType GhCli { get; } = new("gh-cli"); - /// Persisting the newly-created local session to disk. - public static SessionsOpenProgressStep SaveSession { get; } = new("save-session"); + /// Authentication from an API key credential. + public static AuthInfoType ApiKey { get; } = new("api-key"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => left.Equals(right); + /// Authentication from a GitHub token. + public static AuthInfoType Token { get; } = new("token"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => !(left == right); + /// Authentication from an SDK GitHub token callback. + public static AuthInfoType TokenProvider { get; } = new("token-provider"); + + /// Authentication from a Copilot API token. + public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionsOpenProgressStep other && Equals(other); + public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other); /// - public bool Equals(SessionsOpenProgressStep other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AuthInfoType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27556,71 +33798,71 @@ 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 AuthInfoType 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, AuthInfoType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStep)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); } } } -/// Outcome of the open request. +/// Authoritative service connection state for one Connector. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionsOpenStatus : 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 SessionsOpenStatus(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; - /// A new session was created. - public static SessionsOpenStatus Created { get; } = new("created"); + /// The Connector is available but not connected. + public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - /// An existing session was loaded or reattached. - public static SessionsOpenStatus Resumed { get; } = new("resumed"); + /// The Connector service is still completing connection or consent. + public static ConnectorCatalogStatus Pending { get; } = new("pending"); - /// No matching persisted session was found. - public static SessionsOpenStatus NotFound { get; } = new("not_found"); + /// The Connector is connected and may contribute MCP servers. + public static ConnectorCatalogStatus Connected { get; } = new("connected"); - /// Connected to an existing remote session. - public static SessionsOpenStatus Connected { get; } = new("connected"); + /// The Connector service reports an unusable connection. + public static ConnectorCatalogStatus Error { get; } = new("error"); - /// Remote session was handed off to a new local session. - public static SessionsOpenStatus HandedOff { get; } = new("handed_off"); + /// 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 ==(SessionsOpenStatus left, SessionsOpenStatus right) => left.Equals(right); + /// 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 !=(SessionsOpenStatus left, SessionsOpenStatus right) => !(left == 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 SessionsOpenStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); /// - public bool Equals(SessionsOpenStatus 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); @@ -27628,62 +33870,68 @@ 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 ConnectorCatalogStatus 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, ConnectorCatalogStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); } } } -/// Neutral SDK discriminator for the connected remote session kind. +/// 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 ConnectedRemoteSessionMetadataKind : 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 ConnectedRemoteSessionMetadataKind(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; - /// Remote CLI session. - public static ConnectedRemoteSessionMetadataKind RemoteSession { get; } = new("remote-session"); + /// The agent is responding interactively to the user. + public static SendAgentMode Interactive { get; } = new("interactive"); - /// GitHub Copilot coding agent session. - public static ConnectedRemoteSessionMetadataKind CodingAgent { get; } = new("coding-agent"); + /// 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 ==(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind 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 !=(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind 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 ConnectedRemoteSessionMetadataKind other && Equals(other); + public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); /// - public bool Equals(ConnectedRemoteSessionMetadataKind 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); @@ -27691,65 +33939,62 @@ 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 SendAgentMode 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, SendAgentMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectedRemoteSessionMetadataKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); } } } -/// Which session sources to include. Defaults to `local` for backward compatibility. +/// 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 SessionSource : 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 SessionSource(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; - /// Return only local sessions. - public static SessionSource Local { get; } = new("local"); - - /// Return only remote sessions. - public static SessionSource Remote { get; } = new("remote"); + /// Append the message to the normal session queue. + public static SendMode Enqueue { get; } = new("enqueue"); - /// Return both local and remote sessions. - public static SessionSource All { get; } = new("all"); + /// 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 ==(SessionSource left, SessionSource 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 !=(SessionSource left, SessionSource 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 SessionSource other && Equals(other); + public override bool Equals(object? obj) => obj is SendMode other && Equals(other); /// - public bool Equals(SessionSource 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); @@ -27757,62 +34002,65 @@ 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 SendMode 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, SendMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); } } } -/// 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. +/// 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 EventsCursorStatus : 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 EventsCursorStatus(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; - /// The read succeeded against the requested history. - public static EventsCursorStatus Ok { get; } = new("ok"); + /// Informational message. + public static SessionLogLevel Info { get; } = new("info"); - /// The requested continuation is unavailable; see the endpoint's recovery semantics. - public static EventsCursorStatus Expired { get; } = new("expired"); + /// Warning message that may require attention. + public static SessionLogLevel Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); + /// Error message describing a failure. + public static SessionLogLevel Error { get; } = new("error"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); + /// Returns a value indicating whether two instances are 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 !=(SessionLogLevel left, SessionLogLevel right) => !(left == right); /// - public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); + public override bool Equals(object? obj) => obj is SessionLogLevel other && Equals(other); /// - public bool Equals(EventsCursorStatus 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); @@ -27820,62 +34068,65 @@ public EventsCursorStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionLogLevel 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, SessionLogLevel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); } } } -/// 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. +/// 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 EventsReadDirection : 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 EventsReadDirection(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; - /// Page from the cursor toward newer events (default). - public static EventsReadDirection Forward { get; } = new("forward"); + /// The adapter declares that automatic discovery is safe when the other policy fields are satisfied. + public static ModelProviderAutomaticDiscoveryMode Automatic { get; } = new("automatic"); - /// Tail-first: return the newest events and page toward older events. - public static EventsReadDirection Backward { get; } = new("backward"); + /// 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 ==(EventsReadDirection left, EventsReadDirection 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 !=(EventsReadDirection left, EventsReadDirection 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 EventsReadDirection other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderAutomaticDiscoveryMode other && Equals(other); /// - public bool Equals(EventsReadDirection 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); @@ -27883,71 +34134,71 @@ 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 ModelProviderAutomaticDiscoveryMode 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, ModelProviderAutomaticDiscoveryMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderAutomaticDiscoveryMode)); } } } -/// Kind of attention required when status === "attention". Meaningful only when status === "attention". +/// Network reach an adapter may use during discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryAttentionKind : 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 AgentRegistryLiveTargetEntryAttentionKind(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; - /// Session is blocked on an unrecoverable error. - public static AgentRegistryLiveTargetEntryAttentionKind Error { get; } = new("error"); + /// Discovery does not contact a network service. + public static ModelProviderDiscoveryNetworkScope None { get; } = new("none"); - /// Session is waiting for a tool-permission decision. - public static AgentRegistryLiveTargetEntryAttentionKind Permission { get; } = new("permission"); + /// Discovery is limited to loopback addresses on the local machine. + public static ModelProviderDiscoveryNetworkScope LoopbackOnly { get; } = new("loopbackOnly"); - /// Session is waiting for the user to approve or reject a plan. - public static AgentRegistryLiveTargetEntryAttentionKind ExitPlan { get; } = new("exit_plan"); + /// Discovery contacts only endpoints the user already configured. + public static ModelProviderDiscoveryNetworkScope ConfiguredEndpointOnly { get; } = new("configuredEndpointOnly"); - /// Session is waiting on an elicitation prompt. - public static AgentRegistryLiveTargetEntryAttentionKind Elicitation { get; } = new("elicitation"); + /// Discovery may scan or contact the local network. + public static ModelProviderDiscoveryNetworkScope LocalNetwork { get; } = new("localNetwork"); - /// Session is waiting for free-form user input. - public static AgentRegistryLiveTargetEntryAttentionKind UserInput { get; } = new("user_input"); + /// 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 ==(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderDiscoveryNetworkScope left, ModelProviderDiscoveryNetworkScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderDiscoveryNetworkScope left, ModelProviderDiscoveryNetworkScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryAttentionKind other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderDiscoveryNetworkScope other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryAttentionKind 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); @@ -27955,62 +34206,68 @@ 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 ModelProviderDiscoveryNetworkScope 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, ModelProviderDiscoveryNetworkScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryAttentionKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderDiscoveryNetworkScope)); } } } -/// Process kind tag for the registry entry. +/// 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 AgentRegistryLiveTargetEntryKind : 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 AgentRegistryLiveTargetEntryKind(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; - /// Interactive Copilot CLI exposing a UI server (legacy/normal CLI process). - public static AgentRegistryLiveTargetEntryKind UiServer { get; } = new("ui-server"); + /// Built into the runtime. + public static ModelProviderProvenanceSource BuiltIn { get; } = new("builtIn"); - /// Headless `--server --managed-server` child spawned by a controller. - public static AgentRegistryLiveTargetEntryKind ManagedServer { get; } = new("managed-server"); + /// 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 ==(AgentRegistryLiveTargetEntryKind left, AgentRegistryLiveTargetEntryKind 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 !=(AgentRegistryLiveTargetEntryKind left, AgentRegistryLiveTargetEntryKind 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 AgentRegistryLiveTargetEntryKind other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderProvenanceSource other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryKind 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); @@ -28018,62 +34275,62 @@ 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 ModelProviderProvenanceSource 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, ModelProviderProvenanceSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderProvenanceSource)); } } } -/// How the most recent turn ended (clean vs aborted). Lets the renderer distinguish done from done_cancelled. +/// Transport to be used for provider requests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryLastTerminalEvent : 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 AgentRegistryLiveTargetEntryLastTerminalEvent(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; - /// Last turn ended cleanly (model returned a final assistant message). - public static AgentRegistryLiveTargetEntryLastTerminalEvent TurnEnd { get; } = new("turn_end"); + /// HTTP request/streaming transport. + public static ProviderEndpointTransport Http { get; } = new("http"); - /// Last turn was aborted (e.g. user interrupted). - public static AgentRegistryLiveTargetEntryLastTerminalEvent Abort { get; } = new("abort"); + /// WebSocket transport. + public static ProviderEndpointTransport Websockets { get; } = new("websockets"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderEndpointTransport left, ProviderEndpointTransport right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderEndpointTransport left, ProviderEndpointTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryLastTerminalEvent other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointTransport other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryLastTerminalEvent 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); @@ -28081,68 +34338,65 @@ 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 ProviderEndpointTransport 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, ProviderEndpointTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryLastTerminalEvent)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); } } } -/// Coarse lifecycle status of the foreground session. +/// 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 AgentRegistryLiveTargetEntryStatus : 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 AgentRegistryLiveTargetEntryStatus(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 actively processing a turn. - public static AgentRegistryLiveTargetEntryStatus Working { get; } = new("working"); - - /// Session is idle, waiting for input. - public static AgentRegistryLiveTargetEntryStatus Waiting { get; } = new("waiting"); + /// OpenAI-compatible endpoint (use the OpenAI client library). + public static ProviderEndpointType Openai { get; } = new("openai"); - /// Last turn completed successfully. - public static AgentRegistryLiveTargetEntryStatus Done { get; } = new("done"); + /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). + public static ProviderEndpointType Azure { get; } = new("azure"); - /// Session needs user attention (see attentionKind for the specific reason). - public static AgentRegistryLiveTargetEntryStatus Attention { get; } = new("attention"); + /// 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 ==(AgentRegistryLiveTargetEntryStatus left, AgentRegistryLiveTargetEntryStatus 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 !=(AgentRegistryLiveTargetEntryStatus left, AgentRegistryLiveTargetEntryStatus 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 AgentRegistryLiveTargetEntryStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointType other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryStatus 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); @@ -28150,65 +34404,62 @@ 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 ProviderEndpointType 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, ProviderEndpointType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); } } } -/// Categorized reason no canonical process log could be opened. +/// 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 AgentRegistryLogCaptureOpenErrorReason : 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 AgentRegistryLogCaptureOpenErrorReason(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; - /// 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"); + /// Classic chat-completions request shape. + public static ProviderEndpointWireApi Completions { get; } = new("completions"); - /// Other / uncategorized open failure. - public static AgentRegistryLogCaptureOpenErrorReason Other { get; } = new("other"); + /// Newer responses request shape. + public static ProviderEndpointWireApi Responses { get; } = new("responses"); - /// 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 ==(ProviderEndpointWireApi left, ProviderEndpointWireApi 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 !=(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLogCaptureOpenErrorReason other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); /// - public bool Equals(AgentRegistryLogCaptureOpenErrorReason 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); @@ -28216,71 +34467,68 @@ 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 ProviderEndpointWireApi 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, ProviderEndpointWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLogCaptureOpenErrorReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); } } } -/// Which parameter field was invalid. Omitted when the rejection is not field-specific. +/// Typed outcome for a provider operation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnValidationErrorField : 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 AgentRegistrySpawnValidationErrorField(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; - /// The cwd parameter. - public static AgentRegistrySpawnValidationErrorField Cwd { get; } = new("cwd"); - - /// The session name parameter. - public static AgentRegistrySpawnValidationErrorField Name { get; } = new("name"); + /// The operation completed successfully; an empty inventory is valid. + public static ModelProviderOperationOutcomeCode Success { get; } = new("success"); - /// The agentName parameter. - public static AgentRegistrySpawnValidationErrorField AgentName { get; } = new("agentName"); + /// 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"); - /// The model parameter. - public static AgentRegistrySpawnValidationErrorField Model { get; } = new("model"); + /// The provider is configured or expected but could not be reached. + public static ModelProviderOperationOutcomeCode Unreachable { get; } = new("unreachable"); - /// The permissionMode parameter. - public static AgentRegistrySpawnValidationErrorField PermissionMode { get; } = new("permissionMode"); + /// 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 ==(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderOperationOutcomeCode left, ModelProviderOperationOutcomeCode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => !(left == 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 AgentRegistrySpawnValidationErrorField other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderOperationOutcomeCode other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorField 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); @@ -28288,74 +34536,59 @@ 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 ModelProviderOperationOutcomeCode 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, ModelProviderOperationOutcomeCode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorField)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderOperationOutcomeCode)); } } } -/// 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 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 AgentRegistrySpawnValidationErrorReason(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; - /// 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"); - - /// Requested agent name was not found in builtin or custom agents. - public static AgentRegistrySpawnValidationErrorReason UnknownAgent { get; } = new("unknown-agent"); - - /// Requested model is not available to this session. - public static AgentRegistrySpawnValidationErrorReason UnknownModel { get; } = new("unknown-model"); - - /// Caller asked for permissionMode='yolo' but the controller is not currently in allow-all mode. - public static AgentRegistrySpawnValidationErrorReason YoloNotAllowed { get; } = new("yolo-not-allowed"); + /// Gets the append value. + public static ProtocolAppendMode Append { get; } = new("append"); - /// 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 ==(ProtocolAppendMode left, ProtocolAppendMode 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 !=(ProtocolAppendMode left, ProtocolAppendMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorReason other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolAppendMode other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorReason 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); @@ -28363,62 +34596,59 @@ 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 ProtocolAppendMode 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, ProtocolAppendMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); } } } -/// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnPermissionMode : 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 AgentRegistrySpawnPermissionMode(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; - /// Standard permission posture (prompts for each request). - public static AgentRegistrySpawnPermissionMode Default { get; } = new("default"); - - /// Full allow-all (requires the controller-local session to currently be in allow-all mode). - public static AgentRegistrySpawnPermissionMode Yolo { get; } = new("yolo"); + /// Gets the replace value. + public static ProtocolReplaceMode Replace { get; } = new("replace"); - /// 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 ==(ProtocolReplaceMode left, ProtocolReplaceMode 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 !=(ProtocolReplaceMode left, ProtocolReplaceMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnPermissionMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolReplaceMode other && Equals(other); /// - public bool Equals(AgentRegistrySpawnPermissionMode 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); @@ -28426,68 +34656,59 @@ 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 ProtocolReplaceMode 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, ProtocolReplaceMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnPermissionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); } } } -/// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SendAgentMode : 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 SendAgentMode(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 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"); - - /// The agent is in shell-focused UI mode. - public static SendAgentMode Shell { get; } = new("shell"); + /// Gets the customize value. + public static ProtocolCustomizeMode Customize { get; } = new("customize"); - /// 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 ==(ProtocolCustomizeMode left, ProtocolCustomizeMode 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 !=(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolCustomizeMode other && Equals(other); /// - public bool Equals(SendAgentMode 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); @@ -28495,62 +34716,68 @@ 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 ProtocolCustomizeMode 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, ProtocolCustomizeMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); } } } -/// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SendMode : 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 SendMode(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; - /// Append the message to the normal session queue. - public static SendMode Enqueue { get; } = new("enqueue"); + /// Replace the section content. + public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - /// Interject the message during the in-progress turn. - public static SendMode Immediate { get; } = new("immediate"); + /// Remove the section content. + public static ProtocolStaticSectionAction Remove { get; } = new("remove"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SendMode left, SendMode right) => left.Equals(right); + /// Append content to the section. + public static ProtocolStaticSectionAction Append { get; } = new("append"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SendMode left, SendMode right) => !(left == right); + /// 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 ==(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => left.Equals(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 SendMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); /// - public bool Equals(SendMode 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); @@ -28558,65 +34785,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 ProtocolStaticSectionAction 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, ProtocolStaticSectionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); } } } -/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". +/// 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 SessionLogLevel : 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 SessionLogLevel(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; - /// Informational message. - public static SessionLogLevel Info { get; } = new("info"); - - /// Warning message that may require attention. - public static SessionLogLevel Warning { get; } = new("warning"); + /// No matching entry is registered; the caller should add the entry. + public static ModelProviderConfigurationDisposition Create { get; } = new("create"); - /// Error message describing a failure. - public static SessionLogLevel Error { get; } = new("error"); + /// 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 ==(SessionLogLevel left, SessionLogLevel 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 !=(SessionLogLevel left, SessionLogLevel 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 SessionLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderConfigurationDisposition other && Equals(other); /// - public bool Equals(SessionLogLevel 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); @@ -28624,65 +34848,77 @@ 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 ModelProviderConfigurationDisposition 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, ModelProviderConfigurationDisposition value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderConfigurationDisposition)); } } } -/// Disposition of a permission request as observed by the responding client. +/// 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 PermissionDecisionOutcome : 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 PermissionDecisionOutcome(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; - /// The request was approved automatically without a new human decision. - public static PermissionDecisionOutcome AutoApproved { get; } = new("auto_approved"); + /// OpenAI API. + public static ProviderConfigModelProvider Openai { get; } = new("openai"); - /// The request was denied without an interactive user decision; source records why. - public static PermissionDecisionOutcome AutopilotDenied { get; } = new("autopilot_denied"); + /// Anthropic API. + public static ProviderConfigModelProvider Anthropic { get; } = new("anthropic"); - /// The response came from an interactive user prompt. - public static PermissionDecisionOutcome PromptedUser { get; } = new("prompted_user"); + /// Azure OpenAI Service. + public static ProviderConfigModelProvider AzureOpenai { get; } = new("azure_openai"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => left.Equals(right); + /// Ollama. + public static ProviderConfigModelProvider Ollama { get; } = new("ollama"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => !(left == right); + /// LM Studio. + public static ProviderConfigModelProvider LmStudio { get; } = new("lm_studio"); + + /// Foundry Local. + public static ProviderConfigModelProvider FoundryLocal { get; } = new("foundry_local"); + + /// llama.cpp server. + public static ProviderConfigModelProvider LlamaCpp { get; } = new("llama_cpp"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionDecisionOutcome other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigModelProvider other && Equals(other); /// - public bool Equals(PermissionDecisionOutcome 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); @@ -28690,65 +34926,62 @@ public PermissionDecisionOutcome(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionDecisionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigModelProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionDecisionOutcome value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigModelProvider value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionOutcome)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigModelProvider)); } } } -/// Response capability available to the client when it settled a permission request. +/// Provider transport. Defaults to "http". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionResponseCapability : 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 PermissionResponseCapability(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 client could ask a user for this decision. - public static PermissionResponseCapability Interactive { get; } = new("interactive"); - - /// The client could return an automated response but could not ask a user. - public static PermissionResponseCapability Headless { get; } = new("headless"); + /// HTTP request/streaming transport. + public static ProviderConfigTransport Http { get; } = new("http"); - /// The client had no response path available. - public static PermissionResponseCapability None { get; } = new("none"); + /// WebSocket transport. + public static ProviderConfigTransport Websockets { get; } = new("websockets"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionResponseCapability left, PermissionResponseCapability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigTransport left, ProviderConfigTransport right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionResponseCapability left, PermissionResponseCapability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigTransport left, ProviderConfigTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionResponseCapability other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigTransport other && Equals(other); /// - public bool Equals(PermissionResponseCapability 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); @@ -28756,71 +34989,65 @@ public PermissionResponseCapability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionResponseCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionResponseCapability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionResponseCapability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); } } } -/// Client surface that submitted a permission response. +/// 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 PermissionDecisionSurface : 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 PermissionDecisionSurface(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; - /// The interactive Copilot CLI terminal UI. - public static PermissionDecisionSurface Tui { get; } = new("tui"); - - /// The non-interactive Copilot CLI prompt mode. - public static PermissionDecisionSurface PromptMode { get; } = new("prompt_mode"); - - /// The Copilot App client. - public static PermissionDecisionSurface CopilotApp { get; } = new("copilot_app"); + /// Generic OpenAI-compatible API. + public static ProviderConfigType Openai { get; } = new("openai"); - /// An Agent Client Protocol host. - public static PermissionDecisionSurface Acp { get; } = new("acp"); + /// Azure OpenAI Service endpoint. + public static ProviderConfigType Azure { get; } = new("azure"); - /// A generic Copilot SDK client. - public static PermissionDecisionSurface Sdk { get; } = new("sdk"); + /// Anthropic API endpoint. + public static ProviderConfigType Anthropic { get; } = new("anthropic"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionDecisionSurface left, PermissionDecisionSurface right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigType left, ProviderConfigType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionDecisionSurface left, PermissionDecisionSurface right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionDecisionSurface other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigType other && Equals(other); /// - public bool Equals(PermissionDecisionSurface 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); @@ -28828,80 +35055,62 @@ public PermissionDecisionSurface(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionDecisionSurface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionSurface)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); } } } -/// Authentication type. +/// Wire API format (openai/azure only). Defaults to "completions". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AuthInfoType : 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 AuthInfoType(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; - /// Authentication provided by a GitHub App HMAC credential. - public static AuthInfoType Hmac { get; } = new("hmac"); - - /// Authentication resolved from environment-provided credentials. - public static AuthInfoType Env { get; } = new("env"); - - /// Authentication from an interactive user sign-in. - public static AuthInfoType User { get; } = new("user"); - - /// Authentication delegated to the GitHub CLI. - public static AuthInfoType GhCli { get; } = new("gh-cli"); - - /// Authentication from an API key credential. - public static AuthInfoType ApiKey { get; } = new("api-key"); - - /// Authentication from a GitHub token. - public static AuthInfoType Token { get; } = new("token"); - - /// Authentication from an SDK GitHub token callback. - public static AuthInfoType TokenProvider { get; } = new("token-provider"); + /// OpenAI Chat Completions wire format. + public static ProviderConfigWireApi Completions { get; } = new("completions"); - /// Authentication from a Copilot API token. - public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); + /// 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 ==(AuthInfoType left, AuthInfoType 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 !=(AuthInfoType left, AuthInfoType 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 AuthInfoType other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigWireApi other && Equals(other); /// - public bool Equals(AuthInfoType 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); @@ -28909,68 +35118,65 @@ public AuthInfoType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigWireApi)); } } } -/// Source category for a collected debug bundle entry. +/// Disposition of a permission request as observed by the responding client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsSource : IEquatable +public readonly struct PermissionDecisionOutcome : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsSource(string value) + public PermissionDecisionOutcome(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Session event log. - public static DebugCollectLogsSource Events { get; } = new("events"); - - /// Process log for the session. - public static DebugCollectLogsSource ProcessLog { get; } = new("process-log"); + /// The request was approved automatically without a new human decision. + public static PermissionDecisionOutcome AutoApproved { get; } = new("auto_approved"); - /// Interactive shell log for the session. - public static DebugCollectLogsSource ShellLog { get; } = new("shell-log"); + /// The request was denied without an interactive user decision; source records why. + public static PermissionDecisionOutcome AutopilotDenied { get; } = new("autopilot_denied"); - /// Caller-provided diagnostic entry. - public static DebugCollectLogsSource Additional { get; } = new("additional"); + /// The response came from an interactive user prompt. + public static PermissionDecisionOutcome PromptedUser { get; } = new("prompted_user"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsSource left, DebugCollectLogsSource right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsSource left, DebugCollectLogsSource right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsSource other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionDecisionOutcome other && Equals(other); /// - public bool Equals(DebugCollectLogsSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionDecisionOutcome other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28978,62 +35184,65 @@ public DebugCollectLogsSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionDecisionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionDecisionOutcome value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionOutcome)); } } } -/// Destination kind that was written. +/// Response capability available to the client when it settled a permission request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsResultKind : IEquatable +public readonly struct PermissionResponseCapability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsResultKind(string value) + public PermissionResponseCapability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A .tgz archive was written. - public static DebugCollectLogsResultKind Archive { get; } = new("archive"); + /// The client could ask a user for this decision. + public static PermissionResponseCapability Interactive { get; } = new("interactive"); - /// A directory containing the collected files was written. - public static DebugCollectLogsResultKind Directory { get; } = new("directory"); + /// The client could return an automated response but could not ask a user. + public static PermissionResponseCapability Headless { get; } = new("headless"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => left.Equals(right); + /// The client had no response path available. + public static PermissionResponseCapability None { get; } = new("none"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionResponseCapability left, PermissionResponseCapability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionResponseCapability left, PermissionResponseCapability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsResultKind other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionResponseCapability other && Equals(other); /// - public bool Equals(DebugCollectLogsResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionResponseCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29041,62 +35250,71 @@ public DebugCollectLogsResultKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionResponseCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsResultKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionResponseCapability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsResultKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionResponseCapability)); } } } -/// Kind of caller-provided debug log entry. +/// Client surface that submitted a permission response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsEntryKind : IEquatable +public readonly struct PermissionDecisionSurface : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsEntryKind(string value) + public PermissionDecisionSurface(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Include a single server-local file. - public static DebugCollectLogsEntryKind File { get; } = new("file"); + /// The interactive Copilot CLI terminal UI. + public static PermissionDecisionSurface Tui { get; } = new("tui"); - /// Include files from a server-local directory recursively. - public static DebugCollectLogsEntryKind Directory { get; } = new("directory"); + /// The non-interactive Copilot CLI prompt mode. + public static PermissionDecisionSurface PromptMode { get; } = new("prompt_mode"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => left.Equals(right); + /// The Copilot App client. + public static PermissionDecisionSurface CopilotApp { get; } = new("copilot_app"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => !(left == right); + /// An Agent Client Protocol host. + public static PermissionDecisionSurface Acp { get; } = new("acp"); + + /// A generic Copilot SDK client. + public static PermissionDecisionSurface Sdk { get; } = new("sdk"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionDecisionSurface left, PermissionDecisionSurface right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionDecisionSurface left, PermissionDecisionSurface right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsEntryKind other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionDecisionSurface other && Equals(other); /// - public bool Equals(DebugCollectLogsEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionDecisionSurface other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29104,65 +35322,71 @@ public DebugCollectLogsEntryKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionDecisionSurface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsEntryKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsEntryKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionSurface)); } } } -/// How a collected debug entry should be redacted before being staged. +/// The provider kind stamped on a signed-in account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsRedaction : IEquatable +public readonly struct AccountKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsRedaction(string value) + public AccountKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Redact the file as plain UTF-8 log text. - public static DebugCollectLogsRedaction PlainText { get; } = new("plain-text"); + /// An OAuth github.com account. + public static AccountKind GitHubDotCom { get; } = new("githubDotCom"); - /// Redact each non-empty line as a session event JSON object, falling back to plain-text redaction for malformed lines. - public static DebugCollectLogsRedaction EventsJsonl { get; } = new("events-jsonl"); + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com. + public static AccountKind Proxima { get; } = new("proxima"); - /// No redaction is applied. The caller must ensure any necessary redaction is performed before this call. - public static DebugCollectLogsRedaction None { get; } = new("none"); + /// A GitHub (EMU) account derived from a base Entra identity. + public static AccountKind EntraEmu { get; } = new("entraEmu"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => left.Equals(right); + /// A base Microsoft Entra identity. + public static AccountKind Entra { get; } = new("entra"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => !(left == right); + /// A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra identity as an EMU account; its bearer is a Loki-scoped inference token consumed through the model-provider path, not the GitHub switcher. + public static AccountKind Loki { get; } = new("loki"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AccountKind left, AccountKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AccountKind left, AccountKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsRedaction other && Equals(other); + public override bool Equals(object? obj) => obj is AccountKind other && Equals(other); /// - public bool Equals(DebugCollectLogsRedaction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AccountKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29170,65 +35394,65 @@ public DebugCollectLogsRedaction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsRedaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AccountKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsRedaction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AccountKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsRedaction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AccountKind)); } } } -/// Cumulative resource ceiling that stopped a factory run. +/// A provider a consumer may interactively sign in with. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FactoryRunFailureKind : IEquatable +public readonly struct LoginProviderKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryRunFailureKind(string value) + public LoginProviderKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The run admitted the approved maximum total number of subagents. - public static FactoryRunFailureKind MaxTotalSubagents { get; } = new("maxTotalSubagents"); + /// OAuth github.com sign-in via the browser (web loopback + PKCE). + public static LoginProviderKind GitHubDotCom { get; } = new("githubDotCom"); - /// The run reached the approved accumulated active-execution time in seconds. - public static FactoryRunFailureKind TimeoutSeconds { get; } = new("timeoutSeconds"); + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com; the host is supplied interactively through the neutral input-required step. + public static LoginProviderKind Proxima { get; } = new("proxima"); - /// The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. - public static FactoryRunFailureKind MaxAiCredits { get; } = new("maxAiCredits"); + /// Microsoft Entra sign-in that derives a GitHub (EMU) credential. + public static LoginProviderKind Entra { get; } = new("entra"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryRunFailureKind left, FactoryRunFailureKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(LoginProviderKind left, LoginProviderKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryRunFailureKind left, FactoryRunFailureKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(LoginProviderKind left, LoginProviderKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryRunFailureKind other && Equals(other); + public override bool Equals(object? obj) => obj is LoginProviderKind other && Equals(other); /// - public bool Equals(FactoryRunFailureKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(LoginProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29236,89 +35460,68 @@ public FactoryRunFailureKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryRunFailureKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override LoginProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryRunFailureKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryRunFailureKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(LoginProviderKind)); } } } -/// Execution-critical factory storage operation. +/// Disposition of a login attempt, including pending user decisions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FactoryDurableOperation : IEquatable +public readonly struct AuthLoginResultStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryDurableOperation(string value) + public AuthLoginResultStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Creating the durable run and declared phases. - public static FactoryDurableOperation CreateRun { get; } = new("createRun"); - - /// Persisting the transition to running. - public static FactoryDurableOperation MarkRunStarted { get; } = new("markRunStarted"); - - /// Persisting the terminal run envelope. - public static FactoryDurableOperation FinishRun { get; } = new("finishRun"); - - /// Persisting subagent admission accounting. - public static FactoryDurableOperation ReserveAgent { get; } = new("reserveAgent"); - - /// Rolling back an uncommitted subagent admission. - public static FactoryDurableOperation ReleaseAgent { get; } = new("releaseAgent"); - - /// Persisting an idempotent model-usage charge. - public static FactoryDurableOperation ChargeCredit { get; } = new("chargeCredit"); - - /// Persisting active execution time. - public static FactoryDurableOperation AddElapsed { get; } = new("addElapsed"); - - /// Reading the authoritative AI-credit total. - public static FactoryDurableOperation ReconcileCreditTotal { get; } = new("reconcileCreditTotal"); + /// The credential was persisted and the selected account is signed in. + public static AuthLoginResultStatus Completed { get; } = new("completed"); - /// Reading a journal entry without treating storage failure as a cache miss. - public static FactoryDurableOperation JournalGet { get; } = new("journalGet"); + /// Persistence needs explicit consent to store the token in plaintext. + public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); - /// Persisting a journal entry before reporting success. - public static FactoryDurableOperation JournalPut { get; } = new("journalPut"); + /// 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"); - /// Renewing the durable owner lease that proves this process still owns the run. - public static FactoryDurableOperation RefreshLease { get; } = new("refreshLease"); + /// The user declined plaintext persistence. + public static AuthLoginResultStatus Declined { get; } = new("declined"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryDurableOperation left, FactoryDurableOperation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AuthLoginResultStatus left, AuthLoginResultStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryDurableOperation left, FactoryDurableOperation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AuthLoginResultStatus left, AuthLoginResultStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryDurableOperation other && Equals(other); + public override bool Equals(object? obj) => obj is AuthLoginResultStatus other && Equals(other); /// - public bool Equals(FactoryDurableOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AuthLoginResultStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29326,77 +35529,68 @@ public FactoryDurableOperation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryDurableOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AuthLoginResultStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryDurableOperation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AuthLoginResultStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryDurableOperation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthLoginResultStatus)); } } } -/// Current or terminal state of a factory run. +/// Source category for a collected debug bundle entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FactoryRunStatus : IEquatable +public readonly struct DebugCollectLogsSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryRunStatus(string value) + public DebugCollectLogsSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The run was minted and is awaiting approval. - public static FactoryRunStatus Pending { get; } = new("pending"); - - /// The run is executing. - public static FactoryRunStatus Running { get; } = new("running"); - - /// The run completed successfully. - public static FactoryRunStatus Completed { get; } = new("completed"); - - /// The run was interrupted while resource budget remained. - public static FactoryRunStatus Halted { get; } = new("halted"); + /// Session event log. + public static DebugCollectLogsSource Events { get; } = new("events"); - /// The current attempt stopped intentionally and the run may be resumed. - public static FactoryRunStatus Paused { get; } = new("paused"); + /// Process log for the session. + public static DebugCollectLogsSource ProcessLog { get; } = new("process-log"); - /// The run was cancelled before completion. - public static FactoryRunStatus Cancelled { get; } = new("cancelled"); + /// Interactive shell log for the session. + public static DebugCollectLogsSource ShellLog { get; } = new("shell-log"); - /// The factory body failed or reached a cumulative resource ceiling. - public static FactoryRunStatus Error { get; } = new("error"); + /// Caller-provided diagnostic entry. + public static DebugCollectLogsSource Additional { get; } = new("additional"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryRunStatus left, FactoryRunStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsSource left, DebugCollectLogsSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryRunStatus left, FactoryRunStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsSource left, DebugCollectLogsSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryRunStatus other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsSource other && Equals(other); /// - public bool Equals(FactoryRunStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29404,68 +35598,62 @@ public FactoryRunStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryRunStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryRunStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryRunStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsSource)); } } } -/// Derived lifecycle state of a factory phase. +/// Destination kind that was written. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FactoryPhaseStatus : IEquatable +public readonly struct DebugCollectLogsResultKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryPhaseStatus(string value) + public DebugCollectLogsResultKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The phase has not been entered yet. - public static FactoryPhaseStatus Pending { get; } = new("pending"); - - /// The phase is currently entered and accumulating active time. - public static FactoryPhaseStatus Active { get; } = new("active"); - - /// The phase was entered and has since been closed. - public static FactoryPhaseStatus Completed { get; } = new("completed"); + /// A .tgz archive was written. + public static DebugCollectLogsResultKind Archive { get; } = new("archive"); - /// The phase was never entered because a later phase was entered or the run reached a terminal state. - public static FactoryPhaseStatus Skipped { get; } = new("skipped"); + /// A directory containing the collected files was written. + public static DebugCollectLogsResultKind Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryPhaseStatus left, FactoryPhaseStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryPhaseStatus left, FactoryPhaseStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryPhaseStatus other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsResultKind other && Equals(other); /// - public bool Equals(FactoryPhaseStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29473,62 +35661,62 @@ public FactoryPhaseStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryPhaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryPhaseStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsResultKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryPhaseStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsResultKind)); } } } -/// Kind of factory progress line. +/// Kind of caller-provided debug log entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FactoryLogLineKind : IEquatable +public readonly struct DebugCollectLogsEntryKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryLogLineKind(string value) + public DebugCollectLogsEntryKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A narrator log line. - public static FactoryLogLineKind Log { get; } = new("log"); + /// Include a single server-local file. + public static DebugCollectLogsEntryKind File { get; } = new("file"); - /// A named factory phase marker. - public static FactoryLogLineKind Phase { get; } = new("phase"); + /// Include files from a server-local directory recursively. + public static DebugCollectLogsEntryKind Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryLogLineKind left, FactoryLogLineKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryLogLineKind left, FactoryLogLineKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryLogLineKind other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsEntryKind other && Equals(other); /// - public bool Equals(FactoryLogLineKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29536,62 +35724,65 @@ public FactoryLogLineKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryLogLineKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryLogLineKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsEntryKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryLogLineKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsEntryKind)); } } } -/// Action the runtime selected for a durable factory pause checkpoint. +/// How a collected debug entry should be redacted before being staged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FactoryPauseCheckpointAction : IEquatable +public readonly struct DebugCollectLogsRedaction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryPauseCheckpointAction(string value) + public DebugCollectLogsRedaction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The checkpoint was committed by a prior paused attempt, so execution may continue. - public static FactoryPauseCheckpointAction Continue { get; } = new("continue"); + /// Redact the file as plain UTF-8 log text. + public static DebugCollectLogsRedaction PlainText { get; } = new("plain-text"); - /// This attempt claimed the checkpoint and must cooperatively stop. - public static FactoryPauseCheckpointAction Pause { get; } = new("pause"); + /// Redact each non-empty line as a session event JSON object, falling back to plain-text redaction for malformed lines. + public static DebugCollectLogsRedaction EventsJsonl { get; } = new("events-jsonl"); + + /// No redaction is applied. The caller must ensure any necessary redaction is performed before this call. + public static DebugCollectLogsRedaction None { get; } = new("none"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryPauseCheckpointAction left, FactoryPauseCheckpointAction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryPauseCheckpointAction left, FactoryPauseCheckpointAction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryPauseCheckpointAction other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsRedaction other && Equals(other); /// - public bool Equals(FactoryPauseCheckpointAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsRedaction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29599,20 +35790,20 @@ public FactoryPauseCheckpointAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryPauseCheckpointAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsRedaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryPauseCheckpointAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsRedaction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryPauseCheckpointAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsRedaction)); } } } @@ -29976,507 +36167,51 @@ public override WorkflowLogLineKind Read(ref Utf8JsonReader reader, Type typeToC } /// - 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffMode)); - } - } -} - - -/// 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 HistoryRewindUnavailableReason : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public HistoryRewindUnavailableReason(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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 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"); - - /// 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 ==(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is HistoryRewindUnavailableReason other && Equals(other); - - /// - public bool Equals(HistoryRewindUnavailableReason 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 HistoryRewindUnavailableReason 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HistoryRewindUnavailableReason)); - } - } -} - - -/// Current normalized autopilot objective lifecycle status. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct AutopilotObjectiveStatus : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public AutopilotObjectiveStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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"); - - /// The objective completed. - public static AutopilotObjectiveStatus Completed { get; } = new("completed"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); - - /// - public bool Equals(AutopilotObjectiveStatus 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 AutopilotObjectiveStatus 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); @@ -30484,71 +36219,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); @@ -30556,59 +36282,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); @@ -30616,62 +36345,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); @@ -30679,62 +36414,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); @@ -30742,74 +36480,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); @@ -30817,62 +36546,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); @@ -30880,59 +36612,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); @@ -30940,62 +36678,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); @@ -31003,62 +36759,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); @@ -31066,65 +36822,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); @@ -31132,62 +36894,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); @@ -31195,62 +36954,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)); } } } -/// OAuth grant type override for this login. +/// Connection class owning a client task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthLoginGrantType : IEquatable +public readonly struct TaskClientOwnerKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOauthLoginGrantType(string value) + public TaskClientOwnerKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. - public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); + /// A discovered extension connection owns the task. + public static TaskClientOwnerKind Extension { get; } = new("extension"); - /// Headless OAuth flow where a confidential client authenticates directly with a client secret. - public static McpOauthLoginGrantType ClientCredentials { get; } = new("client_credentials"); + /// A generic SDK connection owns the task. + public static TaskClientOwnerKind Sdk { get; } = new("sdk"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerKind left, TaskClientOwnerKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOauthLoginGrantType other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); /// - public bool Equals(McpOauthLoginGrantType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientOwnerKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31258,65 +37017,62 @@ public McpOauthLoginGrantType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOauthLoginGrantType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientOwnerKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOauthLoginGrantType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientOwnerKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); } } } -/// Why a passive MCP OAuth probe determined authentication is needed. +/// Presence of the task's bound join. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthProbeNeedsAuthReason : IEquatable +public readonly struct TaskClientOwnerPresence : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOauthProbeNeedsAuthReason(string value) + public TaskClientOwnerPresence(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No token was sent and the server requires authentication. - public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); - - /// A cached token was sent and rejected. - public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); + /// The bound session join is connected. + public static TaskClientOwnerPresence Connected { get; } = new("connected"); - /// The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. - public static McpOauthProbeNeedsAuthReason Upscope { get; } = new("upscope"); + /// The bound session join is disconnected. + public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOauthProbeNeedsAuthReason other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); /// - public bool Equals(McpOauthProbeNeedsAuthReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientOwnerPresence other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31324,65 +37080,74 @@ public McpOauthProbeNeedsAuthReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOauthProbeNeedsAuthReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientOwnerPresence Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOauthProbeNeedsAuthReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientOwnerPresence value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); } } } -/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. +/// Lifecycle status of a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsAvailableDisplayMode : IEquatable +public readonly struct TaskClientStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) + public TaskClientStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); + /// The external owner is actively working. + public static TaskClientStatus Running { get; } = new("running"); - /// Rendered as a fullscreen overlay. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The external owner is connected but waiting. + public static TaskClientStatus Idle { get; } = new("idle"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); + /// The owner reported successful completion. + public static TaskClientStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + /// The owner reported failure. + public static TaskClientStatus Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => !(left == right); + /// The owner reported or confirmed cancellation. + public static TaskClientStatus Cancelled { get; } = new("cancelled"); + + /// The bound owner join disappeared; external executor state is unknown. + public static TaskClientStatus Orphaned { get; } = new("orphaned"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientStatus left, TaskClientStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientStatus left, TaskClientStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31390,65 +37155,62 @@ public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); } } } -/// Current display mode (SEP-1865). +/// Whether the shell runs inside a managed PTY session or as an independent background process. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsDisplayMode : IEquatable +public readonly struct TaskShellInfoAttachmentMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsDisplayMode(string value) + public TaskShellInfoAttachmentMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsSetHostContextDetailsDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsSetHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The shell runs in a managed PTY session. + public static TaskShellInfoAttachmentMode Attached { get; } = new("attached"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// The shell runs as an independent background process. + public static TaskShellInfoAttachmentMode Detached { get; } = new("detached"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskShellInfoAttachmentMode other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskShellInfoAttachmentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31456,65 +37218,59 @@ public McpAppsSetHostContextDetailsDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskShellInfoAttachmentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskShellInfoAttachmentMode)); } } } -/// Platform type for responsive design. +/// Discriminator for a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsPlatform : IEquatable +public readonly struct TaskClientType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsPlatform(string value) + public TaskClientType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Host runs in a web browser. - public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); - - /// Host runs as a desktop application. - public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); - - /// Host runs on a mobile device. - public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// Gets the client value. + public static TaskClientType Client { get; } = new("client"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31522,62 +37278,62 @@ public McpAppsSetHostContextDetailsPlatform(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); } } } -/// UI theme preference per SEP-1865. +/// Active status a client owner may publish with a progress update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsTheme : IEquatable +public readonly struct TaskClientActiveStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsTheme(string value) + public TaskClientActiveStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Light UI theme. - public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); + /// The external owner is actively working. + public static TaskClientActiveStatus Running { get; } = new("running"); - /// Dark UI theme. - public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); + /// The external owner is connected but waiting. + public static TaskClientActiveStatus Idle { get; } = new("idle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientActiveStatus left, TaskClientActiveStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientActiveStatus left, TaskClientActiveStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientActiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31585,65 +37341,62 @@ public McpAppsSetHostContextDetailsTheme(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientActiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientActiveStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); } } } -/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. +/// Consumer allowed to call an MCP tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsAvailableDisplayMode : IEquatable +public readonly struct McpToolUiVisibility : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsAvailableDisplayMode(string value) + public McpToolUiVisibility(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The model may call the tool. + public static McpToolUiVisibility Model { get; } = new("model"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); + /// An MCP App view may call the tool. + public static McpToolUiVisibility App { get; } = new("app"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpToolUiVisibility left, McpToolUiVisibility right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpToolUiVisibility left, McpToolUiVisibility right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpToolUiVisibility other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpToolUiVisibility other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31651,65 +37404,65 @@ public McpAppsHostContextDetailsAvailableDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpToolUiVisibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpToolUiVisibility value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); } } } -/// Current display mode (SEP-1865). +/// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsDisplayMode : IEquatable +public readonly struct McpSamplingExecutionAction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsDisplayMode(string value) + public McpSamplingExecutionAction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); + /// The sampling inference completed and produced a result. + public static McpSamplingExecutionAction Success { get; } = new("success"); - /// Rendered as a fullscreen overlay. - public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The sampling inference failed or was rejected. + public static McpSamplingExecutionAction Failure { get; } = new("failure"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// The sampling inference was cancelled before completion. + public static McpSamplingExecutionAction Cancelled { get; } = new("cancelled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpSamplingExecutionAction other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpSamplingExecutionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31717,65 +37470,62 @@ public McpAppsHostContextDetailsDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpSamplingExecutionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpSamplingExecutionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSamplingExecutionAction)); } } } -/// Platform type for responsive design. +/// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsPlatform : IEquatable +public readonly struct McpSetEnvValueModeDetails : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsPlatform(string value) + public McpSetEnvValueModeDetails(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Host runs in a web browser. - public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); - - /// Host runs as a desktop application. - public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); + /// Treat MCP server environment values as literal strings. + public static McpSetEnvValueModeDetails Direct { get; } = new("direct"); - /// Host runs on a mobile device. - public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// Treat MCP server environment values as host-side references to resolve before launch. + public static McpSetEnvValueModeDetails Indirect { get; } = new("indirect"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is McpSetEnvValueModeDetails other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpSetEnvValueModeDetails other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31783,62 +37533,62 @@ public McpAppsHostContextDetailsPlatform(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpSetEnvValueModeDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpSetEnvValueModeDetails value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSetEnvValueModeDetails)); } } } -/// UI theme preference per SEP-1865. +/// Outcome of starting the original prepared owned login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsTheme : IEquatable +public readonly struct McpOwnedOauthLoginStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsTheme(string value) + public McpOwnedOauthLoginStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Light UI theme. - public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); + /// The original requester may open the returned authorisation URL. + public static McpOwnedOauthLoginStatus AwaitingBrowser { get; } = new("awaiting-browser"); - /// Dark UI theme. - public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); + /// Cached credentials were accepted and the original server finished reconnecting. + public static McpOwnedOauthLoginStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is McpOwnedOauthLoginStatus other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOwnedOauthLoginStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31846,74 +37596,62 @@ public McpAppsHostContextDetailsTheme(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOwnedOauthLoginStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOwnedOauthLoginStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOwnedOauthLoginStatus)); } } } -/// Session-scoped diagnostic threshold. Capture is disabled by default and is never persisted with the session. +/// OAuth grant type override for this login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticLogLevel : IEquatable +public readonly struct McpOauthLoginGrantType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticLogLevel(string value) + public McpOauthLoginGrantType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Disable capture and clear retained diagnostics. - public static DiagnosticLogLevel Off { get; } = new("off"); - - /// Capture failures only. - public static DiagnosticLogLevel Error { get; } = new("error"); - - /// Capture failures, warnings, and stderr. - public static DiagnosticLogLevel Warning { get; } = new("warning"); - - /// Capture lifecycle diagnostics. - public static DiagnosticLogLevel Info { get; } = new("info"); - - /// Capture protocol frames and launch diagnostics. - public static DiagnosticLogLevel Debug { get; } = new("debug"); + /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. + public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); - /// Capture HTTP metadata in addition to debug diagnostics. - public static DiagnosticLogLevel Trace { get; } = new("trace"); + /// Headless OAuth flow where a confidential client authenticates directly with a client secret. + public static McpOauthLoginGrantType ClientCredentials { get; } = new("client_credentials"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticLogLevel left, DiagnosticLogLevel right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticLogLevel left, DiagnosticLogLevel right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthLoginGrantType other && Equals(other); /// - public bool Equals(DiagnosticLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOauthLoginGrantType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31921,62 +37659,65 @@ public DiagnosticLogLevel(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOauthLoginGrantType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticLogLevel value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOauthLoginGrantType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); } } } -/// Whether the supplied diagnostic cursor remained within the retained buffer window. +/// Why a passive MCP OAuth probe determined authentication is needed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticCursorStatus : IEquatable +public readonly struct McpOauthProbeNeedsAuthReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticCursorStatus(string value) + public McpOauthProbeNeedsAuthReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The cursor is valid for the current retained window. - public static DiagnosticCursorStatus Ok { get; } = new("ok"); + /// No token was sent and the server requires authentication. + public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); - /// The cursor no longer addresses retained records; reading resumes at the oldest retained record. - public static DiagnosticCursorStatus Expired { get; } = new("expired"); + /// A cached token was sent and rejected. + public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => left.Equals(right); + /// 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 ==(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticCursorStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthProbeNeedsAuthReason other && Equals(other); /// - public bool Equals(DiagnosticCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOauthProbeNeedsAuthReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31984,62 +37725,65 @@ public DiagnosticCursorStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOauthProbeNeedsAuthReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticCursorStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOauthProbeNeedsAuthReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); } } } -/// Direction of an observed MCP protocol frame. +/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticDirection : IEquatable +public readonly struct McpAppsSetHostContextDetailsAvailableDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpDiagnosticDirection(string value) + public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Frame emitted by the Copilot MCP client. - public static McpDiagnosticDirection ClientToServer { get; } = new("client-to-server"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// Frame received from the MCP server. - public static McpDiagnosticDirection ServerToClient { get; } = new("server-to-client"); + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpDiagnosticDirection left, McpDiagnosticDirection right) => left.Equals(right); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpDiagnosticDirection left, McpDiagnosticDirection right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpDiagnosticDirection other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(McpDiagnosticDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32047,68 +37791,65 @@ public McpDiagnosticDirection(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpDiagnosticDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpDiagnosticDirection value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticDirection)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); } } } -/// Category for an MCP diagnostic record. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticKind : IEquatable +public readonly struct McpAppsSetHostContextDetailsDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpDiagnosticKind(string value) + public McpAppsSetHostContextDetailsDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Connection lifecycle transition or failure. - public static McpDiagnosticKind Lifecycle { get; } = new("lifecycle"); - - /// JSON-RPC protocol frame. - public static McpDiagnosticKind Protocol { get; } = new("protocol"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsDisplayMode Inline { get; } = new("inline"); - /// HTTP request or response metadata. - public static McpDiagnosticKind Http { get; } = new("http"); + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Local MCP server standard-error output. - public static McpDiagnosticKind Stderr { get; } = new("stderr"); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpDiagnosticKind left, McpDiagnosticKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpDiagnosticKind left, McpDiagnosticKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpDiagnosticKind other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(McpDiagnosticKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32116,71 +37857,65 @@ public McpDiagnosticKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpDiagnosticKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpDiagnosticKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); } } } -/// Severity of an emitted diagnostic record. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSeverity : IEquatable +public readonly struct McpAppsSetHostContextDetailsPlatform : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticSeverity(string value) + public McpAppsSetHostContextDetailsPlatform(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Failure that prevented or interrupted communication. - public static DiagnosticSeverity Error { get; } = new("error"); - - /// A recoverable warning or server standard-error output. - public static DiagnosticSeverity Warning { get; } = new("warning"); - - /// Lifecycle transition. - public static DiagnosticSeverity Info { get; } = new("info"); + /// Host runs in a web browser. + public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); - /// Protocol-frame or launch diagnostic. - public static DiagnosticSeverity Debug { get; } = new("debug"); + /// Host runs as a desktop application. + public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// HTTP metadata diagnostic. - public static DiagnosticSeverity Trace { get; } = new("trace"); + /// Host runs on a mobile device. + public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticSeverity left, DiagnosticSeverity right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticSeverity left, DiagnosticSeverity right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(DiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32188,59 +37923,62 @@ public DiagnosticSeverity(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticSeverity value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSeverity)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); } } } -/// A supported diagnostic source. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSource : IEquatable +public readonly struct McpAppsSetHostContextDetailsTheme : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticSource(string value) + public McpAppsSetHostContextDetailsTheme(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the mcp value. - public static DiagnosticSource Mcp { get; } = new("mcp"); + /// Light UI theme. + public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticSource left, DiagnosticSource right) => left.Equals(right); + /// Dark UI theme. + public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticSource left, DiagnosticSource right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticSource other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); /// - public bool Equals(DiagnosticSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32248,65 +37986,65 @@ public DiagnosticSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); } } } -/// Availability of the EXPERIMENTAL session connector API. +/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAvailability : IEquatable +public readonly struct McpAppsHostContextDetailsAvailableDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorAvailability(string value) + public McpAppsHostContextDetailsAvailableDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The resolved Connector feature is enabled and Connector requests are permitted. - public static ConnectorAvailability Enabled { get; } = new("enabled"); + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// The resolved Connector feature is off. No Connector service request is made while disabled. - public static ConnectorAvailability Disabled { get; } = new("disabled"); + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// The session has no eligible host-owned GitHub account or does not support local Connector projection. - public static ConnectorAvailability Unavailable { get; } = new("unavailable"); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorAvailability left, ConnectorAvailability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(ConnectorAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32314,59 +38052,65 @@ public ConnectorAvailability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorAvailability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); } } } -/// Stable OAuth scope whose absence prevents Connector management. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAuthorizationScope : IEquatable +public readonly struct McpAppsHostContextDetailsDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorAuthorizationScope(string value) + public McpAppsHostContextDetailsDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Allows Copilot to manage Connector connections and use their tools. - public static ConnectorAuthorizationScope WritePluginGatewayConnections { get; } = new("write_plugin_gateway_connections"); + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => left.Equals(right); + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => !(left == right); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorAuthorizationScope other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(ConnectorAuthorizationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32374,71 +38118,65 @@ public ConnectorAuthorizationScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorAuthorizationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); } } } -/// Authoritative service connection state for one Connector. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorCatalogStatus : IEquatable +public readonly struct McpAppsHostContextDetailsPlatform : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorCatalogStatus(string value) + public McpAppsHostContextDetailsPlatform(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The Connector is available but not connected. - public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - - /// The Connector service is still completing connection or consent. - public static ConnectorCatalogStatus Pending { get; } = new("pending"); - - /// The Connector is connected and may contribute MCP servers. - public static ConnectorCatalogStatus Connected { get; } = new("connected"); + /// Host runs in a web browser. + public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); - /// The Connector service reports an unusable connection. - public static ConnectorCatalogStatus Error { get; } = new("error"); + /// Host runs as a desktop application. + public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// The service returned a future or unrecognized state. - public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + /// Host runs on a mobile device. + public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32446,77 +38184,62 @@ public ConnectorCatalogStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); } } } -/// Live MCP status of one Connector-owned runtime server. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorMcpStatus : IEquatable +public readonly struct McpAppsHostContextDetailsTheme : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorMcpStatus(string value) + public McpAppsHostContextDetailsTheme(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The server is connected and its tools are available. - public static ConnectorMcpStatus Connected { get; } = new("connected"); - - /// The server connection is still being established. - public static ConnectorMcpStatus Pending { get; } = new("pending"); - - /// The server requires refreshed GitHub authorization. - public static ConnectorMcpStatus NeedsAuth { get; } = new("needs_auth"); - - /// The server failed to connect or initialize. - public static ConnectorMcpStatus Failed { get; } = new("failed"); - - /// The server is intentionally stopped, including when managed policy blocks it. - public static ConnectorMcpStatus Stopped { get; } = new("stopped"); - - /// The server is configured but explicitly disabled. - public static ConnectorMcpStatus Disabled { get; } = new("disabled"); + /// Light UI theme. + public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); - /// The Connector currently has no live server configuration. - public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + /// Dark UI theme. + public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorMcpStatus left, ConnectorMcpStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorMcpStatus left, ConnectorMcpStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorMcpStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); /// - public bool Equals(ConnectorMcpStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32524,62 +38247,62 @@ public ConnectorMcpStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorMcpStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorMcpStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); } } } -/// Transport to be used for provider requests. +/// The sender role of an MCP prompt message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointTransport : IEquatable +public readonly struct McpPromptRole : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointTransport(string value) + public McpPromptRole(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// HTTP request/streaming transport. - public static ProviderEndpointTransport Http { get; } = new("http"); + /// A message from the user. + public static McpPromptRole User { get; } = new("user"); - /// WebSocket transport. - public static ProviderEndpointTransport Websockets { get; } = new("websockets"); + /// A message from the assistant. + public static McpPromptRole Assistant { get; } = new("assistant"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointTransport left, ProviderEndpointTransport right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPromptRole left, McpPromptRole right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointTransport left, ProviderEndpointTransport right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPromptRole left, McpPromptRole right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointTransport other && Equals(other); + public override bool Equals(object? obj) => obj is McpPromptRole other && Equals(other); /// - public bool Equals(ProviderEndpointTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPromptRole other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32587,65 +38310,74 @@ public ProviderEndpointTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPromptRole Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPromptRole value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPromptRole)); } } } -/// Provider family. Matches the `type` field of a BYOK provider config. +/// Session-scoped diagnostic threshold. Capture is disabled by default and is never persisted with the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointType : IEquatable +public readonly struct DiagnosticLogLevel : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointType(string value) + public DiagnosticLogLevel(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OpenAI-compatible endpoint (use the OpenAI client library). - public static ProviderEndpointType Openai { get; } = new("openai"); + /// Disable capture and clear retained diagnostics. + public static DiagnosticLogLevel Off { get; } = new("off"); - /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). - public static ProviderEndpointType Azure { get; } = new("azure"); + /// Capture failures only. + public static DiagnosticLogLevel Error { get; } = new("error"); - /// Anthropic endpoint (use the Anthropic client library). - public static ProviderEndpointType Anthropic { get; } = new("anthropic"); + /// Capture failures, warnings, and stderr. + public static DiagnosticLogLevel Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointType left, ProviderEndpointType right) => left.Equals(right); + /// Capture lifecycle diagnostics. + public static DiagnosticLogLevel Info { get; } = new("info"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointType left, ProviderEndpointType right) => !(left == right); + /// Capture protocol frames and launch diagnostics. + public static DiagnosticLogLevel Debug { get; } = new("debug"); + + /// Capture HTTP metadata in addition to debug diagnostics. + public static DiagnosticLogLevel Trace { get; } = new("trace"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticLogLevel left, DiagnosticLogLevel right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticLogLevel left, DiagnosticLogLevel right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointType other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticLogLevel other && Equals(other); /// - public bool Equals(ProviderEndpointType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32653,62 +38385,62 @@ public ProviderEndpointType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticLogLevel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); } } } -/// Wire API to be used, when required for the provider type. +/// Whether the supplied diagnostic cursor remained within the retained buffer window. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointWireApi : IEquatable +public readonly struct DiagnosticCursorStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointWireApi(string value) + public DiagnosticCursorStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Classic chat-completions request shape. - public static ProviderEndpointWireApi Completions { get; } = new("completions"); + /// The cursor is valid for the current retained window. + public static DiagnosticCursorStatus Ok { get; } = new("ok"); - /// Newer responses request shape. - public static ProviderEndpointWireApi Responses { get; } = new("responses"); + /// The cursor no longer addresses retained records; reading resumes at the oldest retained record. + public static DiagnosticCursorStatus Expired { get; } = new("expired"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticCursorStatus other && Equals(other); /// - public bool Equals(ProviderEndpointWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32716,59 +38448,62 @@ public ProviderEndpointWireApi(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointWireApi value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticCursorStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); } } } -/// Defines the allowed values. +/// Direction of an observed MCP protocol frame. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolAppendMode : IEquatable +public readonly struct McpDiagnosticDirection : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolAppendMode(string value) + public McpDiagnosticDirection(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the append value. - public static ProtocolAppendMode Append { get; } = new("append"); + /// Frame emitted by the Copilot MCP client. + public static McpDiagnosticDirection ClientToServer { get; } = new("client-to-server"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolAppendMode left, ProtocolAppendMode right) => left.Equals(right); + /// Frame received from the MCP server. + public static McpDiagnosticDirection ServerToClient { get; } = new("server-to-client"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolAppendMode left, ProtocolAppendMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpDiagnosticDirection left, McpDiagnosticDirection right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpDiagnosticDirection left, McpDiagnosticDirection right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolAppendMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticDirection other && Equals(other); /// - public bool Equals(ProtocolAppendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpDiagnosticDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32776,59 +38511,68 @@ public ProtocolAppendMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolAppendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpDiagnosticDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolAppendMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpDiagnosticDirection value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticDirection)); } } } -/// Defines the allowed values. +/// Category for an MCP diagnostic record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolReplaceMode : IEquatable +public readonly struct McpDiagnosticKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolReplaceMode(string value) + public McpDiagnosticKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the replace value. - public static ProtocolReplaceMode Replace { get; } = new("replace"); + /// Connection lifecycle transition or failure. + public static McpDiagnosticKind Lifecycle { get; } = new("lifecycle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolReplaceMode left, ProtocolReplaceMode right) => left.Equals(right); + /// JSON-RPC protocol frame. + public static McpDiagnosticKind Protocol { get; } = new("protocol"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolReplaceMode left, ProtocolReplaceMode right) => !(left == right); + /// HTTP request or response metadata. + public static McpDiagnosticKind Http { get; } = new("http"); + + /// Local MCP server standard-error output. + public static McpDiagnosticKind Stderr { get; } = new("stderr"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpDiagnosticKind left, McpDiagnosticKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpDiagnosticKind left, McpDiagnosticKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolReplaceMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticKind other && Equals(other); /// - public bool Equals(ProtocolReplaceMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpDiagnosticKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32836,59 +38580,71 @@ public ProtocolReplaceMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolReplaceMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpDiagnosticKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolReplaceMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpDiagnosticKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticKind)); } } } -/// Defines the allowed values. +/// Severity of an emitted diagnostic record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolCustomizeMode : IEquatable +public readonly struct DiagnosticSeverity : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolCustomizeMode(string value) + public DiagnosticSeverity(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the customize value. - public static ProtocolCustomizeMode Customize { get; } = new("customize"); + /// Failure that prevented or interrupted communication. + public static DiagnosticSeverity Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => left.Equals(right); + /// A recoverable warning or server standard-error output. + public static DiagnosticSeverity Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => !(left == right); + /// Lifecycle transition. + public static DiagnosticSeverity Info { get; } = new("info"); + + /// Protocol-frame or launch diagnostic. + public static DiagnosticSeverity Debug { get; } = new("debug"); + + /// HTTP metadata diagnostic. + public static DiagnosticSeverity Trace { get; } = new("trace"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticSeverity left, DiagnosticSeverity right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticSeverity left, DiagnosticSeverity right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolCustomizeMode other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); /// - public bool Equals(ProtocolCustomizeMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32896,68 +38652,59 @@ public ProtocolCustomizeMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolCustomizeMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolCustomizeMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticSeverity value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSeverity)); } } } -/// Defines the allowed values. +/// A supported diagnostic source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolStaticSectionAction : IEquatable +public readonly struct DiagnosticSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolStaticSectionAction(string value) + public DiagnosticSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Replace the section content. - public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - - /// Remove the section content. - public static ProtocolStaticSectionAction Remove { get; } = new("remove"); - - /// Append content to the section. - public static ProtocolStaticSectionAction Append { get; } = new("append"); - - /// Prepend content to the section. - public static ProtocolStaticSectionAction Prepend { get; } = new("prepend"); + /// Gets the mcp value. + public static DiagnosticSource Mcp { get; } = new("mcp"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticSource left, DiagnosticSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticSource left, DiagnosticSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSource other && Equals(other); /// - public bool Equals(ProtocolStaticSectionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32965,62 +38712,65 @@ public ProtocolStaticSectionAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolStaticSectionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolStaticSectionAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); } } } -/// Provider transport. Defaults to "http". +/// Availability of the EXPERIMENTAL session connector API. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigTransport : IEquatable +public readonly struct ConnectorAvailability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigTransport(string value) + public ConnectorAvailability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// HTTP request/streaming transport. - public static ProviderConfigTransport Http { get; } = new("http"); + /// The resolved Connector feature is enabled and Connector requests are permitted. + public static ConnectorAvailability Enabled { get; } = new("enabled"); - /// WebSocket transport. - public static ProviderConfigTransport Websockets { get; } = new("websockets"); + /// The resolved Connector feature is off. No Connector service request is made while disabled. + public static ConnectorAvailability Disabled { get; } = new("disabled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigTransport left, ProviderConfigTransport right) => left.Equals(right); + /// The session has no eligible host-owned GitHub account or does not support local Connector projection. + public static ConnectorAvailability Unavailable { get; } = new("unavailable"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigTransport left, ProviderConfigTransport right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorAvailability left, ConnectorAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigTransport other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); /// - public bool Equals(ProviderConfigTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33028,65 +38778,59 @@ public ProviderConfigTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorAvailability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); } } } -/// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. +/// Stable OAuth scope whose absence prevents Connector management. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigType : IEquatable +public readonly struct ConnectorAuthorizationScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigType(string value) + public ConnectorAuthorizationScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Generic OpenAI-compatible API. - public static ProviderConfigType Openai { get; } = new("openai"); - - /// Azure OpenAI Service endpoint. - public static ProviderConfigType Azure { get; } = new("azure"); - - /// Anthropic API endpoint. - public static ProviderConfigType Anthropic { get; } = new("anthropic"); + /// Allows Copilot to manage Connector connections and use their tools. + public static ConnectorAuthorizationScope WritePluginGatewayConnections { get; } = new("write_plugin_gateway_connections"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigType left, ProviderConfigType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigType other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAuthorizationScope other && Equals(other); /// - public bool Equals(ProviderConfigType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorAuthorizationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33094,62 +38838,77 @@ public ProviderConfigType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorAuthorizationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); } } } -/// Wire API format (openai/azure only). Defaults to "completions". +/// Live MCP status of one Connector-owned runtime server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigWireApi : IEquatable +public readonly struct ConnectorMcpStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigWireApi(string value) + public ConnectorMcpStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OpenAI Chat Completions wire format. - public static ProviderConfigWireApi Completions { get; } = new("completions"); + /// The server is connected and its tools are available. + public static ConnectorMcpStatus Connected { get; } = new("connected"); - /// OpenAI Responses API wire format. - public static ProviderConfigWireApi Responses { get; } = new("responses"); + /// The server connection is still being established. + public static ConnectorMcpStatus Pending { get; } = new("pending"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigWireApi left, ProviderConfigWireApi right) => left.Equals(right); + /// The server requires refreshed GitHub authorization. + public static ConnectorMcpStatus NeedsAuth { get; } = new("needs_auth"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigWireApi left, ProviderConfigWireApi right) => !(left == right); + /// The server failed to connect or initialize. + public static ConnectorMcpStatus Failed { get; } = new("failed"); + + /// The server is intentionally stopped, including when managed policy blocks it. + public static ConnectorMcpStatus Stopped { get; } = new("stopped"); + + /// The server is configured but explicitly disabled. + public static ConnectorMcpStatus Disabled { get; } = new("disabled"); + + /// The Connector currently has no live server configuration. + public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorMcpStatus left, ConnectorMcpStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorMcpStatus left, ConnectorMcpStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigWireApi other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorMcpStatus other && Equals(other); /// - public bool Equals(ProviderConfigWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorMcpStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33157,20 +38916,20 @@ public ProviderConfigWireApi(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorMcpStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigWireApi value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorMcpStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigWireApi)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); } } } @@ -33431,7 +39190,7 @@ public override void Write(Utf8JsonWriter writer, OptionsUpdateReasoningSummary } -/// Origin of the sandbox choice supplied by an internal client. +/// Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -33460,7 +39219,7 @@ public SandboxConfigSource(string value) /// The user's persisted settings disabled the sandbox. public static SandboxConfigSource UserDisabled { get; } = new("user_disabled"); - /// A command-line flag selected the sandbox state for this session. + /// An explicit session-scoped choice selected the sandbox state, such as a command-line flag. public static SandboxConfigSource SessionFlag { get; } = new("session_flag"); /// The user disabled the sandbox for the current session. @@ -35090,7 +40849,7 @@ public override void Write(Utf8JsonWriter writer, PermissionsSetApproveAllSource } -/// Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. +/// 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. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -35122,7 +40881,7 @@ public PermissionModeSource(string value) /// The mode was set at startup by the `defaultPermissionMode` user setting. public static PermissionModeSource UserSetting { get; } = new("user_setting"); - /// The mode was set at startup by authenticated organization targeting. + /// The mode was set at startup because the authenticated account matched an organization targeting policy. public static PermissionModeSource OrganizationTargeting { get; } = new("organization_targeting"); /// The mode was set through an RPC caller. @@ -35483,69 +41242,6 @@ public override void Write(Utf8JsonWriter writer, WorkspaceSummaryHostType value } -/// Hosting platform type of the repository. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct SessionWorkingDirectoryContextHostType : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public SessionWorkingDirectoryContextHostType(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The working directory repository is hosted on GitHub. - public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github"); - - /// The working directory repository is hosted on Azure DevOps. - public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other); - - /// - public bool Equals(SessionWorkingDirectoryContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override SessionWorkingDirectoryContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType)); - } - } -} - - /// Rust-owned settings predicates exposed across the SDK boundary. Raw feature-flag names are intentionally not part of the contract. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -36901,6 +42597,75 @@ public override void Write(Utf8JsonWriter writer, SessionFsSqliteTransactionErro } +/// Defines the allowed values. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HostExitReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HostExitReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The owner requested disposal. + public static HostExitReason Disposed { get; } = new("disposed"); + + /// The hosting task or its SDK transport exited; this does not mean the runtime process exited. + public static HostExitReason Exited { get; } = new("exited"); + + /// The owning SDK connection disconnected. + public static HostExitReason OwnerDisconnected { get; } = new("ownerDisconnected"); + + /// The runtime is shutting down. + public static HostExitReason RuntimeShutdown { get; } = new("runtimeShutdown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HostExitReason left, HostExitReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HostExitReason left, HostExitReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HostExitReason other && Equals(other); + + /// + public bool Equals(HostExitReason 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 HostExitReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HostExitReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HostExitReason)); + } + } +} + + /// 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. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -36956,51 +42721,117 @@ public override LlmInferenceHttpRequestStartTransport Read(ref Utf8JsonReader re } /// - public override void Write(Utf8JsonWriter writer, LlmInferenceHttpRequestStartTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, LlmInferenceHttpRequestStartTransport value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(LlmInferenceHttpRequestStartTransport)); + } + } +} + + +/// Why the runtime is requesting a GitHub credential. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct GitHubTokenAcquireReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public GitHubTokenAcquireReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The runtime is acquiring the registration's first credential. + public static GitHubTokenAcquireReason Initial { get; } = new("initial"); + + /// The runtime is replacing a credential that is approaching expiry. + public static GitHubTokenAcquireReason Refresh { get; } = new("refresh"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(GitHubTokenAcquireReason left, GitHubTokenAcquireReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(GitHubTokenAcquireReason left, GitHubTokenAcquireReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is GitHubTokenAcquireReason other && Equals(other); + + /// + public bool Equals(GitHubTokenAcquireReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override GitHubTokenAcquireReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, GitHubTokenAcquireReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(LlmInferenceHttpRequestStartTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(GitHubTokenAcquireReason)); } } } -/// Why the runtime is requesting a GitHub credential. +/// Explicit user decisions, never inferred from a permission grant or model response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct GitHubTokenAcquireReason : IEquatable +public readonly struct InstallationDecision : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public GitHubTokenAcquireReason(string value) + public InstallationDecision(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The runtime is acquiring the registration's first credential. - public static GitHubTokenAcquireReason Initial { get; } = new("initial"); + /// The user explicitly approved the exact review on this request. + public static InstallationDecision Confirm { get; } = new("confirm"); - /// The runtime is replacing a credential that is approaching expiry. - public static GitHubTokenAcquireReason Refresh { get; } = new("refresh"); + /// The user declined the reviewed operation. + public static InstallationDecision Decline { get; } = new("decline"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(GitHubTokenAcquireReason left, GitHubTokenAcquireReason right) => left.Equals(right); + /// The user cancelled the pending decision without granting consent. + public static InstallationDecision Cancel { get; } = new("cancel"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(GitHubTokenAcquireReason left, GitHubTokenAcquireReason right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(InstallationDecision left, InstallationDecision right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(InstallationDecision left, InstallationDecision right) => !(left == right); /// - public override bool Equals(object? obj) => obj is GitHubTokenAcquireReason other && Equals(other); + public override bool Equals(object? obj) => obj is InstallationDecision other && Equals(other); /// - public bool Equals(GitHubTokenAcquireReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(InstallationDecision other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -37008,20 +42839,20 @@ public GitHubTokenAcquireReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override GitHubTokenAcquireReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override InstallationDecision Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, GitHubTokenAcquireReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, InstallationDecision value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(GitHubTokenAcquireReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstallationDecision)); } } } @@ -37070,6 +42901,18 @@ 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 ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Hooks APIs. public ServerHooksApi Hooks => field ?? @@ -37118,20 +42961,20 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; - /// Catalog APIs. - public ServerCatalogApi Catalog => + /// Skills APIs. + public ServerSkillsApi Skills => field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; - /// Plugins APIs. - public ServerPluginsApi Plugins => + /// Catalog APIs. + public ServerCatalogApi Catalog => field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; - /// Skills APIs. - public ServerSkillsApi Skills => + /// Plugins APIs. + public ServerPluginsApi Plugins => field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; @@ -37148,6 +42991,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + /// GlobalState APIs. + public ServerGlobalStateApi GlobalState => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Commands APIs. public ServerCommandsApi Commands => field ?? @@ -37160,6 +43009,24 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + /// GitHubRepository APIs. + public ServerGitHubRepositoryApi GitHubRepository => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + + /// GitHubOwners APIs. + public ServerGitHubOwnersApi GitHubOwners => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + + /// Git APIs. + public ServerGitApi Git => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// ManagedSettings APIs. public ServerManagedSettingsApi ManagedSettings => field ?? @@ -37195,6 +43062,168 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + + /// Connectors APIs. + public ServerConnectorsApi Connectors => + 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerHostApi +{ + private readonly JsonRpc _rpc; + + internal ServerHostApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Publishes an attached resident session for this listener's lifetime without copying it. + /// Listener UUID returned by host.start. + /// Canonical runtime session ID attached to the listener's owning connection. + /// The to monitor for cancellation requests. The default is . + /// The existing runtime identity and its resource on the listener. + public async Task PublishSessionAsync(string hostId, string sessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(hostId); + ArgumentNullException.ThrowIfNull(sessionId); + + var request = new HostPublishSessionRequest { HostId = hostId, SessionId = sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "host.publishSession", [request], cancellationToken); + } + + /// Requests app-owned materialization over the owning SDK participant. + /// Unique identity for this participation, independent of the session lifetime. + /// Host-selected SDK creation or resume settings, without executable callbacks or tools. + /// Resume an app-owned durable session instead of creating a new session. + /// The to monitor for cancellation requests. The default is . + /// The resident session the application has materialized on its own connection. + internal async Task CreateSessionAsync(string handoffId, IDictionary config, bool? resume = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(handoffId); + ArgumentNullException.ThrowIfNull(config); + + var request = new HostCreateSessionRequest { HandoffId = handoffId, Config = config, Resume = resume }; + return await CopilotClient.InvokeRpcAsync(_rpc, "host.createSession", [request], cancellationToken); + } + + /// Releases app ownership retention after AHP detaches. + /// Identity of the participation to release without destroying the session. + /// The to monitor for cancellation requests. The default is . + /// Empty acknowledgement for a completed host lifecycle operation. + internal async Task ReleaseSessionAsync(string handoffId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(handoffId); + + var request = new HostReleaseSessionRequest { HandoffId = handoffId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "host.releaseSession", [request], cancellationToken); + } + + /// 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. + /// 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, HostLocalServerOptions? localServer = null, HostGitHubEnvironmentOptions? githubEnvironment = null, bool? sessionFactory = null, bool? resumeFactory = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(hostId); + + var request = new HostStartRequest { HostId = hostId, LocalServer = localServer, GitHubEnvironment = githubEnvironment, SessionFactory = sessionFactory, ResumeFactory = resumeFactory }; + return await CopilotClient.InvokeRpcAsync(_rpc, "host.start", [request], cancellationToken); + } + + /// Stops a listener owned by this SDK connection and joins its cleanup without deleting sessions. + /// Listener UUID. Unknown or successfully stopped IDs are harmless. + /// The to monitor for cancellation requests. The default is . + /// Empty acknowledgement for a completed host lifecycle operation. + public async Task DisposeAsync(string hostId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(hostId); + + var request = new HostDisposeRequest { HostId = hostId }; + 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. + internal async Task GetConfigurationAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "host.getConfiguration", [], cancellationToken); + } + + /// 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 = null, string? token = null, string? environmentId = null, CancellationToken cancellationToken = default) + { + var request = new HostReadyRequest { Address = address, Token = token, EnvironmentId = environmentId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "host.ready", [request], cancellationToken); + } } /// Provides server-scoped Hooks APIs. @@ -37269,6 +43298,72 @@ public async Task GetHostSupportAsync(CancellationToken canc { return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.getHostSupport", [], cancellationToken); } + + /// ProxyCa APIs. + public ServerSandboxProxyCaApi ProxyCa => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; +} + +/// Provides server-scoped SandboxProxyCa APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerSandboxProxyCaApi +{ + private readonly JsonRpc _rpc; + + internal ServerSandboxProxyCaApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task GetStatusAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.getStatus", [request], cancellationToken); + } + + /// Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Result of creating the persistent certificate authority of the sandbox credential proxy. + public async Task CreateAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.create", [request], cancellationToken); + } + + /// Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task RotateAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.rotate", [request], cancellationToken); + } + + /// Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task TrustAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.trust", [request], cancellationToken); + } + + /// Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task RemoveAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.remove", [], cancellationToken); + } } /// Provides server-scoped Tools APIs. @@ -37419,11 +43514,108 @@ public async Task PlanInstallAsync(CatalogClientContract c return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.planInstall", [request], cancellationToken); } + /// 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. + /// A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + /// The to monitor for cancellation requests. The default is . + /// Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. + public async Task PlanInstallAsync(McpPlanInstallRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.Contract); + ArgumentNullException.ThrowIfNull(request.Source); + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.planInstall", [request], cancellationToken); + } + + /// 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. + /// Required bound catalogue and confirmed remote installation capabilities. + /// Original single-use bound plan, never a client-authored configuration. + /// Exact selected alternative from that plan. + /// An existing local session attached to this connection, not permission to attach one. + /// + /// Declared non-secret values. Non-empty only when the caller requires + /// `mcp-configured-remote-installation`; omitted values use the card default. + /// + /// + /// One entry per declared secret placeholder of the selected choice. Non-empty + /// only when the caller requires `mcp-configured-remote-installation`. + /// + /// The exact original source, used transiently only after confirmation. + /// The trusted host presents this choice alongside the exact secret placeholders. + /// The to monitor for cancellation requests. The default is . + /// Management result with contract receipt, or a typed request/negotiation refusal. + public async Task PrepareInstallAsync(CatalogClientContract contract, string planHandle, string choiceId, string policySessionId, IList inputs, IList secrets, McpServerCardReference source, McpInstallationSecretStorage secretStorage, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(planHandle); + ArgumentNullException.ThrowIfNull(choiceId); + ArgumentNullException.ThrowIfNull(policySessionId); + ArgumentNullException.ThrowIfNull(inputs); + ArgumentNullException.ThrowIfNull(secrets); + ArgumentNullException.ThrowIfNull(source); + + var request = new McpPrepareInstallRequest { Contract = contract, PlanHandle = planHandle, ChoiceId = choiceId, PolicySessionId = policySessionId, Inputs = inputs, Secrets = secrets, Source = source, SecretStorage = secretStorage }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.prepareInstall", [request], cancellationToken); + } + + /// 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. + /// Capabilities required by the original prepared operation. + /// Runtime-issued ID already returned by prepareInstall, never reused or rebound. + /// Same existing attached or privately borrowed session as preparation. + /// The to monitor for cancellation requests. The default is . + /// An installation result together with the exact honoured contract, or a negotiation refusal. + public async Task ApplyInstallAsync(CatalogClientContract contract, string operationId, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(operationId); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new McpApplyInstallRequest { Contract = contract, OperationId = operationId, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.applyInstall", [request], cancellationToken); + } + + /// 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. + /// Required authenticated bound installation capabilities. + /// Exact receipt to inspect, not a server-name guess. + /// Existing selected session on the original connection. + /// The to monitor for cancellation requests. The default is . + /// Management result with contract receipt, or a typed request/negotiation refusal. + public async Task PlanUninstallAsync(CatalogClientContract contract, string installationId, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(installationId); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new McpPlanUninstallRequest { Contract = contract, InstallationId = installationId, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.planUninstall", [request], cancellationToken); + } + + /// 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. + /// Required authenticated bound installation capabilities. + /// Opaque original removal plan, consumed once. + /// Same existing selected session as removal preparation. + /// The to monitor for cancellation requests. The default is . + /// An installation result together with the exact honoured contract, or a negotiation refusal. + public async Task ApplyUninstallAsync(CatalogClientContract contract, string planHandle, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(planHandle); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new McpApplyUninstallRequest { Contract = contract, PlanHandle = planHandle, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.applyUninstall", [request], cancellationToken); + } + /// Config APIs. public ServerMcpConfigApi Config => field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + + /// Installations APIs. + public ServerMcpInstallationsApi Installations => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; } /// Provides server-scoped McpConfig APIs. @@ -37513,6 +43705,74 @@ public async Task ReloadAsync(CancellationToken cancellationToken = default) } } +/// Provides server-scoped McpInstallations APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerMcpInstallationsApi +{ + private readonly JsonRpc _rpc; + + internal ServerMcpInstallationsApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// 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. + /// Required authenticated bound installation contract. + /// Existing selected local session on this connection. + /// The to monitor for cancellation requests. The default is . + /// Management result with contract receipt, or a typed request/negotiation refusal. + public async Task ListAsync(CatalogClientContract contract, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new McpInstallationsRequest { Contract = contract, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.installations.list", [request], cancellationToken); + } + + /// 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. + /// Required authenticated bound installation contract. + /// Existing selected local session on this connection. + /// The to monitor for cancellation requests. The default is . + /// Management result with contract receipt, or a typed request/negotiation refusal. + public async Task RecoverAsync(CatalogClientContract contract, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new McpInstallationsRequest { Contract = contract, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.installations.recover", [request], cancellationToken); + } + + /// 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. + /// Original installation wire capability; new-work authentication is not reacquired. + /// Exact runtime-issued operation ID on the original connection. + /// The to monitor for cancellation requests. The default is . + /// Management result with contract receipt, or a typed request/negotiation refusal. + public async Task StatusAsync(CatalogClientContract contract, string operationId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(operationId); + + var request = new McpInstallationOperationRequest { Contract = contract, OperationId = operationId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.installations.status", [request], cancellationToken); + } + + /// 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. + /// Original installation wire capability; new-work authentication is not reacquired. + /// Exact runtime-issued operation ID on the original connection. + /// The to monitor for cancellation requests. The default is . + /// Management result with contract receipt, or a typed request/negotiation refusal. + public async Task CancelAsync(CatalogClientContract contract, string operationId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(operationId); + + var request = new McpInstallationOperationRequest { Contract = contract, OperationId = operationId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "mcp.installations.cancel", [request], cancellationToken); + } +} + /// Provides server-scoped Extensions APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ServerExtensionsApi @@ -37555,6 +43815,239 @@ public async Task DisableAsync(IList ids, CancellationToken cancellation } } +/// Provides server-scoped Skills APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerSkillsApi +{ + private readonly JsonRpc _rpc; + + internal ServerSkillsApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// 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. + /// Required authenticated bound catalogue and Skill installation capabilities. + /// Fresh single-use AI skill candidate handle returned by a bound catalogue search. + /// Existing local session attached to this connection. + /// The to monitor for cancellation requests. The default is . + /// Skill installation management result with the honoured contract, or a typed refusal. + public async Task PlanInstallAsync(CatalogClientContract contract, string candidateHandle, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(candidateHandle); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new SkillPlanInstallRequest { Contract = contract, CandidateHandle = candidateHandle, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.planInstall", [request], cancellationToken); + } + + /// Consumes one verified Skill installation plan, requests explicit human consent through installations.confirm on the original connection, then revalidates and installs the Skill disabled. + /// Required authenticated bound catalogue and Skill installation capabilities. + /// Opaque original plan, consumed once. + /// Same existing selected session as planning. + /// The to monitor for cancellation requests. The default is . + /// Skill installation result with the honoured contract, or a typed request/negotiation refusal. + public async Task ApplyInstallAsync(CatalogClientContract contract, string planHandle, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(planHandle); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new SkillApplyInstallRequest { Contract = contract, PlanHandle = planHandle, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.applyInstall", [request], cancellationToken); + } + + /// 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. + /// Required authenticated bound installation contract. + /// Exact receipt to inspect. + /// Existing selected local session on this connection. + /// The to monitor for cancellation requests. The default is . + /// Skill installation management result with the honoured contract, or a typed refusal. + public async Task PlanUninstallAsync(CatalogClientContract contract, string installationId, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(installationId); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new SkillPlanUninstallRequest { Contract = contract, InstallationId = installationId, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.planUninstall", [request], cancellationToken); + } + + /// Consumes an owned Skill removal plan, requests explicit human consent through installations.confirm, refuses drift, and removes the exact owned files through quarantine. + /// Required authenticated bound installation contract. + /// Opaque original removal plan, consumed once. + /// Same existing selected session as removal preparation. + /// The to monitor for cancellation requests. The default is . + /// Skill installation result with the honoured contract, or a typed request/negotiation refusal. + public async Task ApplyUninstallAsync(CatalogClientContract contract, string planHandle, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(planHandle); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new SkillApplyUninstallRequest { Contract = contract, PlanHandle = planHandle, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.applyUninstall", [request], cancellationToken); + } + + /// Discovers skills across global and project sources. + /// Optional list of project directory paths to scan for project-scoped skills. + /// Optional list of additional skill directory paths to include. + /// Optional skill scan paths to exclude from discovery. + /// When true, omit skills from the host's global sources (personal, custom, plugin, and built-in), returning only project-scoped skills. For multitenant deployments. + /// The to monitor for cancellation requests. The default is . + /// Skills discovered across global and project sources. + public async Task DiscoverAsync(IList? projectPaths = null, IList? skillDirectories = null, IList? ignoredSkillsLocations = null, bool? excludeHostSkills = null, CancellationToken cancellationToken = default) + { + var request = new SkillsDiscoverRequest { ProjectPaths = projectPaths, SkillDirectories = skillDirectories, IgnoredSkillsLocations = ignoredSkillsLocations, ExcludeHostSkills = excludeHostSkills }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.discover", [request], cancellationToken); + } + + /// 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. + /// Optional list of project directory paths. When omitted or empty, only personal and custom directories are returned. + /// Optional skill scan paths to exclude from discovery. + /// When true, omit the host's personal and custom skill directories, leaving only project directories. For multitenant deployments. + /// The to monitor for cancellation requests. The default is . + /// Canonical locations where skills can be created so the runtime will recognize them. + public async Task GetDiscoveryPathsAsync(IList? projectPaths = null, IList? ignoredSkillsLocations = null, bool? excludeHostSkills = null, CancellationToken cancellationToken = default) + { + var request = new SkillsGetDiscoveryPathsRequest { ProjectPaths = projectPaths, IgnoredSkillsLocations = ignoredSkillsLocations, ExcludeHostSkills = excludeHostSkills }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.getDiscoveryPaths", [request], cancellationToken); + } + + /// Installations APIs. + public ServerSkillsInstallationsApi Installations => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + + /// Config APIs. + public ServerSkillsConfigApi Config => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; +} + +/// Provides server-scoped SkillsInstallations APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerSkillsInstallationsApi +{ + private readonly JsonRpc _rpc; + + internal ServerSkillsInstallationsApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Lists owned verified Agent Finder Skill installations for the selected existing session. Listing is never gated by the Skill-install feature flag. + /// Required authenticated bound installation contract. + /// Existing selected local session on this connection. + /// The to monitor for cancellation requests. The default is . + /// Skill installation management result with the honoured contract, or a typed refusal. + public async Task ListAsync(CatalogClientContract contract, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new SkillInstallationsRequest { Contract = contract, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.installations.list", [request], cancellationToken); + } + + /// 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. + /// Required authenticated bound installation contract. + /// Existing selected local session on this connection. + /// The to monitor for cancellation requests. The default is . + /// Skill installation management result with the honoured contract, or a typed refusal. + public async Task RecoverAsync(CatalogClientContract contract, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new SkillInstallationsRequest { Contract = contract, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.installations.recover", [request], cancellationToken); + } + + /// Inspects a known Skill installation operation on its original runtime connection. Status is never gated by the Skill-install feature flag. + /// Required authenticated bound Skill installation capability. + /// Exact runtime-issued operation ID on the original connection. + /// The to monitor for cancellation requests. The default is . + /// Skill installation management result with the honoured contract, or a typed refusal. + public async Task StatusAsync(CatalogClientContract contract, string operationId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(operationId); + + var request = new SkillInstallationOperationRequest { Contract = contract, OperationId = operationId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.installations.status", [request], cancellationToken); + } + + /// Requests cancellation of a known Skill installation operation before commit. Already-started durable work requires recovery instead of silent replay. + /// Required authenticated bound Skill installation capability. + /// Exact runtime-issued operation ID on the original connection. + /// The to monitor for cancellation requests. The default is . + /// Skill installation management result with the honoured contract, or a typed refusal. + public async Task CancelAsync(CatalogClientContract contract, string operationId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(operationId); + + var request = new SkillInstallationOperationRequest { Contract = contract, OperationId = operationId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.installations.cancel", [request], cancellationToken); + } + + /// 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. + /// Required authenticated bound Skill installation capability. + /// Exact receipt identity to update. + /// Persisted enablement value. + /// Existing selected local session to reconcile after persistence. + /// The to monitor for cancellation requests. The default is . + /// Skill installation management result with the honoured contract, or a typed refusal. + public async Task SetEnabledAsync(CatalogClientContract contract, string installationId, bool enabled, string policySessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contract); + ArgumentNullException.ThrowIfNull(installationId); + ArgumentNullException.ThrowIfNull(policySessionId); + + var request = new SkillSetEnabledRequest { Contract = contract, InstallationId = installationId, Enabled = enabled, PolicySessionId = policySessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "skills.installations.setEnabled", [request], cancellationToken); + } +} + +/// Provides server-scoped SkillsConfig APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerSkillsConfigApi +{ + private readonly JsonRpc _rpc; + + internal ServerSkillsConfigApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Replaces the global list of disabled skills. + /// List of skill names to disable. + /// The to monitor for cancellation requests. The default is . + public async Task SetDisabledSkillsAsync(IList disabledSkills, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(disabledSkills); + + var request = new SkillsConfigSetDisabledSkillsRequest { DisabledSkills = disabledSkills }; + await CopilotClient.InvokeRpcAsync(_rpc, "skills.config.setDisabledSkills", [request], cancellationToken); + } + + /// Atomically adds or removes one skill from the disabled list. + /// Name of the skill to add to or remove from the disabled list. + /// True to disable the skill, false to enable it. + /// The to monitor for cancellation requests. The default is . + public async Task SetSkillDisabledAsync(string name, bool disabled, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(name); + + var request = new SkillsConfigSetSkillDisabledRequest { Name = name, Disabled = disabled }; + await CopilotClient.InvokeRpcAsync(_rpc, "skills.config.setSkillDisabled", [request], cancellationToken); + } +} + /// Provides server-scoped Catalog APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ServerCatalogApi @@ -37583,6 +44076,18 @@ public async Task SearchAsync(CatalogClientContract contrac return await CopilotClient.InvokeRpcAsync(_rpc, "catalog.search", [request], cancellationToken); } + /// 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. + /// 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. + /// The to monitor for cancellation requests. The default is . + /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. + public async Task SearchAsync(CatalogSearchRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.Contract); + ArgumentNullException.ThrowIfNull(request.Query); + return await CopilotClient.InvokeRpcAsync(_rpc, "catalog.search", [request], cancellationToken); + } + /// 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. /// Protocol version and capabilities the caller requires. /// Locally owned root session whose retained search state is being resolved. @@ -37793,115 +44298,95 @@ public async Task RefreshAsync(string? name = null, Ca } } -/// Provides server-scoped Skills APIs. +/// Provides server-scoped Agents APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerSkillsApi +public sealed class ServerAgentsApi { private readonly JsonRpc _rpc; - internal ServerSkillsApi(JsonRpc rpc) + internal ServerAgentsApi(JsonRpc rpc) { _rpc = rpc; } - /// Discovers skills across global and project sources. - /// Optional list of project directory paths to scan for project-scoped skills. - /// Optional list of additional skill directory paths to include. - /// Optional skill scan paths to exclude from discovery. - /// When true, omit skills from the host's global sources (personal, custom, plugin, and built-in), returning only project-scoped skills. For multitenant deployments. + /// Discovers custom agents across user, project, plugin, and remote sources. + /// Optional list of project directory paths to scan for project-scoped agents. When omitted or empty, only user/plugin/remote-independent agents are returned (no project scan). + /// When true, omit the host's agents (the user-level agent directory and all plugin agents), leaving only project and remote agents. For multitenant deployments. /// The to monitor for cancellation requests. The default is . - /// Skills discovered across global and project sources. - public async Task DiscoverAsync(IList? projectPaths = null, IList? skillDirectories = null, IList? ignoredSkillsLocations = null, bool? excludeHostSkills = null, CancellationToken cancellationToken = default) + /// Agents discovered across user, project, plugin, and remote sources. + public async Task DiscoverAsync(IList? projectPaths = null, bool? excludeHostAgents = null, CancellationToken cancellationToken = default) { - var request = new SkillsDiscoverRequest { ProjectPaths = projectPaths, SkillDirectories = skillDirectories, IgnoredSkillsLocations = ignoredSkillsLocations, ExcludeHostSkills = excludeHostSkills }; - return await CopilotClient.InvokeRpcAsync(_rpc, "skills.discover", [request], cancellationToken); + var request = new AgentsDiscoverRequest { ProjectPaths = projectPaths, ExcludeHostAgents = excludeHostAgents }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.discover", [request], cancellationToken); } - /// 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. - /// Optional list of project directory paths. When omitted or empty, only personal and custom directories are returned. - /// Optional skill scan paths to exclude from discovery. - /// When true, omit the host's personal and custom skill directories, leaving only project directories. For multitenant deployments. + /// 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. + /// Optional list of project directory paths. When omitted or empty, only the user-level directory is returned. + /// When true, omit the host's user-level agent directory, leaving only project directories. For multitenant deployments (mirrors `discover`'s `excludeHostAgents`). /// The to monitor for cancellation requests. The default is . - /// Canonical locations where skills can be created so the runtime will recognize them. - public async Task GetDiscoveryPathsAsync(IList? projectPaths = null, IList? ignoredSkillsLocations = null, bool? excludeHostSkills = null, CancellationToken cancellationToken = default) + /// Canonical locations where custom agents can be created so the runtime will recognize them. + public async Task GetDiscoveryPathsAsync(IList? projectPaths = null, bool? excludeHostAgents = null, CancellationToken cancellationToken = default) { - var request = new SkillsGetDiscoveryPathsRequest { ProjectPaths = projectPaths, IgnoredSkillsLocations = ignoredSkillsLocations, ExcludeHostSkills = excludeHostSkills }; - return await CopilotClient.InvokeRpcAsync(_rpc, "skills.getDiscoveryPaths", [request], cancellationToken); + var request = new AgentsGetDiscoveryPathsRequest { ProjectPaths = projectPaths, ExcludeHostAgents = excludeHostAgents }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getDiscoveryPaths", [request], cancellationToken); } - /// Config APIs. - public ServerSkillsConfigApi Config => - field ?? - Interlocked.CompareExchange(ref field, new(_rpc), null) ?? - field; -} - -/// Provides server-scoped SkillsConfig APIs. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerSkillsConfigApi -{ - private readonly JsonRpc _rpc; - - internal ServerSkillsConfigApi(JsonRpc rpc) + /// Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + /// The to monitor for cancellation requests. The default is . + /// The agents this runtime ships, named so a consumer can tell them apart from authored ones. + internal async Task GetBuiltinsAsync(CancellationToken cancellationToken = default) { - _rpc = rpc; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltins", [], cancellationToken); } - /// Replaces the global list of disabled skills. - /// List of skill names to disable. + /// Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. /// The to monitor for cancellation requests. The default is . - public async Task SetDisabledSkillsAsync(IList disabledSkills, CancellationToken cancellationToken = default) + /// The shipped agents available under the requested flags. + internal async Task GetAvailableBuiltinsAsync(IDictionary? featureFlags = null, IDictionary? overrides = null, string? context = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(disabledSkills); - - var request = new SkillsConfigSetDisabledSkillsRequest { DisabledSkills = disabledSkills }; - await CopilotClient.InvokeRpcAsync(_rpc, "skills.config.setDisabledSkills", [request], cancellationToken); + var request = new AgentsGetAvailableBuiltinsRequest { FeatureFlags = featureFlags, Overrides = overrides, Context = context }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getAvailableBuiltins", [request], cancellationToken); } - /// Atomically adds or removes one skill from the disabled list. - /// Name of the skill to add to or remove from the disabled list. - /// True to disable the skill, false to enable it. + /// Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. /// The to monitor for cancellation requests. The default is . - public async Task SetSkillDisabledAsync(string name, bool disabled, CancellationToken cancellationToken = default) + /// One shipped agent's definition. + internal async Task GetBuiltinDefinitionAsync(string name, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(name); - var request = new SkillsConfigSetSkillDisabledRequest { Name = name, Disabled = disabled }; - await CopilotClient.InvokeRpcAsync(_rpc, "skills.config.setSkillDisabled", [request], cancellationToken); - } -} - -/// Provides server-scoped Agents APIs. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerAgentsApi -{ - private readonly JsonRpc _rpc; - - internal ServerAgentsApi(JsonRpc rpc) - { - _rpc = rpc; + var request = new AgentsGetBuiltinDefinitionRequest { Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinDefinition", [request], cancellationToken); } - /// Discovers custom agents across user, project, plugin, and remote sources. - /// Optional list of project directory paths to scan for project-scoped agents. When omitted or empty, only user/plugin/remote-independent agents are returned (no project scan). - /// When true, omit the host's agents (the user-level agent directory and all plugin agents), leaving only project and remote agents. For multitenant deployments. + /// Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. /// The to monitor for cancellation requests. The default is . - /// Agents discovered across user, project, plugin, and remote sources. - public async Task DiscoverAsync(IList? projectPaths = null, bool? excludeHostAgents = null, CancellationToken cancellationToken = default) + /// One shipped agent, projected for a listing. + internal async Task GetBuiltinListingDefinitionAsync(string name, CancellationToken cancellationToken = default) { - var request = new AgentsDiscoverRequest { ProjectPaths = projectPaths, ExcludeHostAgents = excludeHostAgents }; - return await CopilotClient.InvokeRpcAsync(_rpc, "agents.discover", [request], cancellationToken); + ArgumentNullException.ThrowIfNull(name); + + var request = new AgentsGetBuiltinListingDefinitionRequest { Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinListingDefinition", [request], cancellationToken); } - /// 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. - /// Optional list of project directory paths. When omitted or empty, only the user-level directory is returned. - /// When true, omit the host's user-level agent directory, leaving only project directories. For multitenant deployments (mirrors `discover`'s `excludeHostAgents`). + /// Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + /// The models available to this session, serialized in the shape the model list carries. /// The to monitor for cancellation requests. The default is . - /// Canonical locations where custom agents can be created so the runtime will recognize them. - public async Task GetDiscoveryPathsAsync(IList? projectPaths = null, bool? excludeHostAgents = null, CancellationToken cancellationToken = default) + /// The model to switch to, and the warning to show when the agent's preference could not be met. + internal async Task CustomAgentInitialModelDecisionAsync(string agentModelsJson, string availableModelsJson, CancellationToken cancellationToken = default) { - var request = new AgentsGetDiscoveryPathsRequest { ProjectPaths = projectPaths, ExcludeHostAgents = excludeHostAgents }; - return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getDiscoveryPaths", [request], cancellationToken); + ArgumentNullException.ThrowIfNull(agentModelsJson); + ArgumentNullException.ThrowIfNull(availableModelsJson); + + var request = new AgentsCustomAgentInitialModelDecisionParams { AgentModelsJson = agentModelsJson, AvailableModelsJson = availableModelsJson }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.customAgentInitialModelDecision", [request], cancellationToken); } } @@ -37939,6 +44424,49 @@ public async Task GetDiscoveryPathsAsync(IListProvides server-scoped GlobalState APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGlobalStateApi +{ + private readonly JsonRpc _rpc; + + internal ServerGlobalStateApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. + /// The to monitor for cancellation requests. The default is . + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + internal async Task LoadAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.load", [], cancellationToken); + } + + /// Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + /// The to monitor for cancellation requests. The default is . + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + internal async Task LoadForConfigDirAsync(string? configDir = null, CancellationToken cancellationToken = default) + { + var request = new GlobalStateLoadForConfigDirRequest { ConfigDir = configDir }; + return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.loadForConfigDir", [request], cancellationToken); + } + + /// Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + /// The to monitor for cancellation requests. The default is . + internal async Task WriteKeyAsync(string key, string? configDir = null, object? value = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(key); + + var request = new GlobalStateWriteKeyRequest { Key = key, ConfigDir = configDir, Value = CopilotClient.ToJsonElementForWire(value) }; + await CopilotClient.InvokeRpcAsync(_rpc, "globalState.writeKey", [request], cancellationToken); + } +} + /// Provides server-scoped Commands APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ServerCommandsApi @@ -37988,31 +44516,138 @@ internal ServerUserSettingsApi(JsonRpc rpc) _rpc = rpc; } - /// Drops this runtime process's in-memory user settings cache so the next settings read observes disk. + /// Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. /// The to monitor for cancellation requests. The default is . - public async Task ReloadAsync(CancellationToken cancellationToken = default) - { - await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.reload", [], cancellationToken); - } - - /// Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. - /// The to monitor for cancellation requests. The default is . - /// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + /// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. public async Task GetAsync(CancellationToken cancellationToken = default) { return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.get", [], cancellationToken); } - /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. + /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. /// Partial user settings to write, as a free-form object keyed by setting name. /// The to monitor for cancellation requests. The default is . - /// Outcome of writing user settings. - public async Task SetAsync(object settings, CancellationToken cancellationToken = default) + public async Task SetAsync(object settings, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(settings); var request = new UserSettingsSetRequest { Settings = CopilotClient.ToJsonElementForWire(settings)!.Value }; - return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken); + await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken); + } +} + +/// Provides server-scoped GitHubRepository APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitHubRepositoryApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitHubRepositoryApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. + /// Absolute path to a directory inside the git working tree to resolve. + /// The to monitor for cancellation requests. The default is . + /// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. + internal async Task AtPathAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(path); + + var request = new GitHubRepositoryAtPathRequest { Path = path }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubRepository.atPath", [request], cancellationToken); + } +} + +/// Provides server-scoped GitHubOwners APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitHubOwnersApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitHubOwnersApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + /// The to monitor for cancellation requests. The default is . + /// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + internal async Task NextRequestIdAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.nextRequestId", [], cancellationToken); + } + + /// Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + /// The to monitor for cancellation requests. The default is . + /// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. + internal async Task ListAsync(long requestId, object authInfo, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(authInfo); + + var request = new GitHubOwnersListRequest { RequestId = requestId, AuthInfo = CopilotClient.ToJsonElementForWire(authInfo)!.Value }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.list", [request], cancellationToken); + } + + /// Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. + /// Request id the listing was started with. + /// The to monitor for cancellation requests. The default is . + /// Whether the id named a running owner listing. + internal async Task CancelAsync(long requestId, CancellationToken cancellationToken = default) + { + var request = new GitHubOwnersCancelRequest { RequestId = requestId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.cancel", [request], cancellationToken); + } +} + +/// Provides server-scoped Git APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reads the remote that the branch checked out in a working tree tracks, as `branch.<name>.remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// Absolute path to a directory inside the git working tree to query. + /// The to monitor for cancellation requests. The default is . + /// The remote the checked-out branch tracks. + internal async Task CurrentBranchRemoteAsync(string cwd, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(cwd); + + var request = new GitCwdRequest { Cwd = cwd }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.currentBranchRemote", [request], cancellationToken); + } + + /// Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. + /// Absolute path to a directory inside the git working tree to query. + /// The to monitor for cancellation requests. The default is . + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + internal async Task WorkingDirectoryContextAsync(string cwd, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(cwd); + + var request = new GitCwdRequest { Cwd = cwd }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.workingDirectoryContext", [request], cancellationToken); + } + + /// Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// Absolute path to the root of the git working tree. + /// The to monitor for cancellation requests. The default is . + /// The GitHub repositories a working tree's remotes point at. + internal async Task ReposFromRemotesAsync(string gitRoot, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(gitRoot); + + var request = new GitReposFromRemotesRequest { GitRoot = gitRoot }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.reposFromRemotes", [request], cancellationToken); } } @@ -38027,7 +44662,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) @@ -38035,12 +44670,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. @@ -38415,6 +45095,54 @@ public async Task EnrichMetadataAsync(IList(_rpc, "sessions.enrichMetadata", [request], cancellationToken); } + /// Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + /// Session ID the workspace record belongs to. + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + /// User-supplied display name for the workspace. + /// The to monitor for cancellation requests. The default is . + /// The workspace record that was written. + internal async Task CreateWorkspaceAsync(string sessionId, string sessionStatePath, string convention, SessionWorkingDirectoryContextWithClient? context = null, string? name = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionId); + ArgumentNullException.ThrowIfNull(sessionStatePath); + ArgumentNullException.ThrowIfNull(convention); + + var request = new SessionsCreateWorkspaceRequest { SessionId = sessionId, SessionStatePath = sessionStatePath, Convention = convention, Context = context, Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.createWorkspace", [request], cancellationToken); + } + + /// Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + /// Root directory every session's state directory sits under. + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + /// The to monitor for cancellation requests. The default is . + /// The workspace record on disk, omitted when the session has none. + internal async Task LoadWorkspaceAsync(string sessionsHome, string sessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionsHome); + ArgumentNullException.ThrowIfNull(sessionId); + + var request = new SessionsLoadWorkspaceRequest { SessionsHome = sessionsHome, SessionId = sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.loadWorkspace", [request], cancellationToken); + } + + /// Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + /// Root directory every session's state directory sits under. + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + /// The to monitor for cancellation requests. The default is . + /// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. + internal async Task UpdateWorkspaceFieldsAsync(string sessionsHome, string sessionId, string fieldsJson, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionsHome); + ArgumentNullException.ThrowIfNull(sessionId); + ArgumentNullException.ThrowIfNull(fieldsJson); + + var request = new SessionsUpdateWorkspaceFieldsRequest { SessionsHome = sessionsHome, SessionId = sessionId, FieldsJson = fieldsJson }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.updateWorkspaceFields", [request], cancellationToken); + } + /// Reloads user, plugin, and (optionally) repo hooks on the active session. /// Active session ID to reload hooks for. /// When true, skip repo-level hooks. Use before folder trust is confirmed; loadDeferredRepoHooks loads them post-trust. @@ -38561,6 +45289,58 @@ public async Task SpawnAsync(string cwd, string? agent } } +/// Provides server-scoped Connectors APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerConnectorsApi +{ + private readonly JsonRpc _rpc; + + internal ServerConnectorsApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Returns feature availability. + /// The to monitor for cancellation requests. The default is . + /// Feature availability. + public async Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getCapabilities", [], cancellationToken); + } + + /// Returns eligible accounts. + /// The to monitor for cancellation requests. The default is . + /// Eligible accounts. + public async Task GetAccountsAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getAccounts", [], cancellationToken); + } + + /// Lists entries for the selected account. + /// Opaque account ID. + /// The to monitor for cancellation requests. The default is . + /// Entries for the selected account. + public async Task ListAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.list", [request], cancellationToken); + } + + /// Refreshes entries for the selected account. + /// Opaque account ID. + /// The to monitor for cancellation requests. The default is . + /// Entries for the selected account. + public async Task RefreshAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.refresh", [request], cancellationToken); + } +} + /// Provides typed session-scoped RPC methods. public sealed class SessionRpc { @@ -38573,6 +45353,12 @@ internal SessionRpc(CopilotSession session) internal CopilotSession Session => _session; + /// Providers APIs. + public ProvidersApi Providers => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Sandbox APIs. public SandboxApi Sandbox => field ?? @@ -38585,20 +45371,20 @@ internal SessionRpc(CopilotSession session) Interlocked.CompareExchange(ref field, new(_session), null) ?? field; - /// Debug APIs. - public DebugApi Debug => + /// Accounts APIs. + public AccountsApi Accounts => field ?? Interlocked.CompareExchange(ref field, new(_session), null) ?? field; - /// Canvas APIs. - public CanvasApi Canvas => + /// Debug APIs. + public DebugApi Debug => field ?? Interlocked.CompareExchange(ref field, new(_session), null) ?? field; - /// Factory APIs. - public FactoryApi Factory => + /// Canvas APIs. + public CanvasApi Canvas => field ?? Interlocked.CompareExchange(ref field, new(_session), null) ?? field; @@ -38990,6 +45776,102 @@ public async Task LogAsync(string message, SessionLogLevel? level = n } } +/// Provides session-scoped Providers APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProvidersApi +{ + private readonly CopilotSession _session; + + internal ProvidersApi(CopilotSession session) + { + _session = session; + } + + /// Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. + /// The to monitor for cancellation requests. The default is . + /// Normalized model-provider adapter definitions available to the session, not discovered instances. + public async Task GetCatalogAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionProvidersGetCatalogRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.getCatalog", [request], cancellationToken); + } + + /// Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + /// Opaque adapter identity returned by `session.providers.getCatalog`. + /// Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + /// The to monitor for cancellation requests. The default is . + /// Provider instances found by a discovery operation. + public async Task DiscoverAsync(string adapterId, object? input = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(adapterId); + _session.ThrowIfDisposed(); + + var request = new ModelProviderDiscoverRequest { SessionId = _session.SessionId, AdapterId = adapterId, Input = CopilotClient.ToJsonElementForWire(input) }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.discover", [request], cancellationToken); + } + + /// Gets current health and version information for a discovered model-provider instance. + /// Provider instance reference returned by discovery. + /// The to monitor for cancellation requests. The default is . + /// Current health information for a provider instance. + public async Task GetStatusAsync(ModelProviderInstanceReference instance, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + _session.ThrowIfDisposed(); + + var request = new ModelProviderGetStatusRequest { SessionId = _session.SessionId, Instance = instance }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.getStatus", [request], cancellationToken); + } + + /// Models APIs. + public ProvidersModelsApi Models => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; +} + +/// Provides session-scoped ProvidersModels APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProvidersModelsApi +{ + private readonly CopilotSession _session; + + internal ProvidersModelsApi(CopilotSession session) + { + _session = session; + } + + /// Lists models installed or otherwise available from a discovered model-provider instance. + /// Provider instance reference returned by discovery. + /// The to monitor for cancellation requests. The default is . + /// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + public async Task ListAsync(ModelProviderInstanceReference instance, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + _session.ThrowIfDisposed(); + + var request = new ModelProviderModelsListRequest { SessionId = _session.SessionId, Instance = instance }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.models.list", [request], cancellationToken); + } + + /// Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. + /// The discovered instance that serves the model. + /// The discovered model to configure. + /// The to monitor for cancellation requests. The default is . + /// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + public async Task PrepareConfigurationAsync(ModelProviderInstance instance, DiscoveredModel model, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(model); + _session.ThrowIfDisposed(); + + var request = new ModelProviderPrepareConfigurationRequest { SessionId = _session.SessionId, Instance = instance, Model = model }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.models.prepareConfiguration", [request], cancellationToken); + } +} + /// Provides session-scoped Sandbox APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SandboxApi @@ -39025,6 +45907,20 @@ public async Task DisableForSessionAsync(string var request = new SandboxDisableForSessionRequest { SessionId = _session.SessionId, RequestId = requestId, DecisionContext = decisionContext }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.sandbox.disableForSession", [request], cancellationToken); } + + /// 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. + /// Identifier of the exact pending sandbox escalation permission request whose sandboxPathGrant to accept. + /// Optional attribution for the permission decision. + /// The to monitor for cancellation requests. The default is . + /// Result of accepting a sandbox path grant. + public async Task GrantPathForRequestAsync(string requestId, PermissionDecisionContext? decisionContext = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + _session.ThrowIfDisposed(); + + var request = new SandboxGrantPathForRequestRequest { SessionId = _session.SessionId, RequestId = requestId, DecisionContext = decisionContext }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.sandbox.grantPathForRequest", [request], cancellationToken); + } } /// Provides session-scoped GitHubAuth APIs. @@ -39161,6 +46057,113 @@ internal async Task> LastAuthErrorsAsync(Cancellation } } +/// Provides session-scoped Accounts APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AccountsApi +{ + private readonly CopilotSession _session; + + internal AccountsApi(CopilotSession session) + { + _session = session; + } + + /// Enumerate a typed accounts collection: the signed-in accounts, or the providers offered for interactive login. + /// Which typed accounts collection to enumerate. + /// The to monitor for cancellation requests. The default is . + /// The enumerated collection, keyed by the same selector as the query. + public async Task EnumerateAsync(AuthEnumerateQuery query, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + _session.ThrowIfDisposed(); + + var request = new AccountsEnumerateRequest { SessionId = _session.SessionId, Query = query }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.accounts.enumerate", [request], cancellationToken); + } + + /// Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors. + /// Which typed accounts datum to read. + /// The to monitor for cancellation requests. The default is . + /// The read result, keyed by the same selector as the query. + public async Task GetAsync(AuthReadQuery query, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + _session.ThrowIfDisposed(); + + var request = new AccountsGetRequest { SessionId = _session.SessionId, Query = query }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.accounts.get", [request], cancellationToken); + } + + /// Apply one non-interactive accounts mutation: switch the active account, log an account out, or set credentials from a token. + /// The non-interactive mutation command to apply. + /// The to monitor for cancellation requests. The default is . + /// Result of a non-interactive accounts mutation. + public async Task SetAsync(AuthWrite command, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(command); + _session.ThrowIfDisposed(); + + var request = new AccountsSetRequest { SessionId = _session.SessionId, Command = command }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.accounts.set", [request], cancellationToken); + } + + /// Login APIs. + public AccountsLoginApi Login => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; +} + +/// Provides session-scoped AccountsLogin APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AccountsLoginApi +{ + private readonly CopilotSession _session; + + internal AccountsLoginApi(CopilotSession session) + { + _session = session; + } + + /// Begin an interactive login flow for a provider kind (dispatch is kind-only) and return its opaque flow id and first step. + /// The provider kind to sign in with. + /// The to monitor for cancellation requests. The default is . + /// A started login flow: its opaque id and first step. + public async Task BeginAsync(LoginProviderKind kind, CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new AuthLoginBeginRequest { SessionId = _session.SessionId, Kind = kind }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.accounts.login.begin", [request], cancellationToken); + } + + /// Advance an in-flight login flow, optionally fulfilling an input-required step, and return the next step. + /// Opaque flow id from begin. + /// Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. + /// The to monitor for cancellation requests. The default is . + /// 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). + public async Task AdvanceAsync(string flowId, string? input = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(flowId); + _session.ThrowIfDisposed(); + + var request = new AuthLoginAdvanceRequest { SessionId = _session.SessionId, FlowId = flowId, Input = input }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.accounts.login.advance", [request], cancellationToken); + } + + /// Cancel an in-flight login flow and release its resources. + /// Opaque flow id from begin. + /// The to monitor for cancellation requests. The default is . + public async Task CancelAsync(string flowId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(flowId); + _session.ThrowIfDisposed(); + + var request = new AuthLoginCancelRequest { SessionId = _session.SessionId, FlowId = flowId }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.accounts.login.cancel", [request], cancellationToken); + } +} + /// Provides session-scoped Debug APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class DebugApi @@ -39331,271 +46334,6 @@ internal async Task UnregisterAsync(string connectionId, CancellationToken cance } } -/// Provides session-scoped Factory APIs. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryApi -{ - private readonly CopilotSession _session; - - internal FactoryApi(CopilotSession session) - { - _session = session; - } - - /// Runs a registered factory by name at the top level. - /// Registered factory name. - /// Factory input value. - /// Factory invocation options. - /// The to monitor for cancellation requests. The default is . - /// Complete current or terminal factory run envelope. - public async Task RunAsync(string name, object args, RunOptions? options = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(name); - ArgumentNullException.ThrowIfNull(args); - _session.ThrowIfDisposed(); - - var request = new FactoryRunRequest { SessionId = _session.SessionId, Name = name, Args = CopilotClient.ToJsonElementForWire(args)!.Value, Options = options }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.run", [request], cancellationToken); - } - - /// Resumes a factory run using its persisted name, arguments, journal, and accounting. - /// Factory run identifier. - /// Optional per-invocation resource ceiling overrides. - /// Whether to notify the originating session when the factory completes. - /// Whether to emit factory phase names to the session transcript. - /// The to monitor for cancellation requests. The default is . - /// Resolved persisted factory identity and resumed run envelope. - public async Task ResumeAsync(string runId, FactoryRunLimits? limits = null, bool? notifyOnComplete = null, bool? logPhaseNames = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - _session.ThrowIfDisposed(); - - var request = new FactoryResumeRequest { SessionId = _session.SessionId, RunId = runId, Limits = limits, NotifyOnComplete = notifyOnComplete, LogPhaseNames = logPhaseNames }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.resume", [request], cancellationToken); - } - - /// Internal tool-originated factory invocation. - /// Registered factory name. - /// Factory input value. - /// Tool-originated factory invocation options. - /// Opaque identifier of the originating tool call. - /// The to monitor for cancellation requests. The default is . - /// Complete current or terminal factory run envelope. - internal async Task RunFromToolAsync(string name, object args, FactoryToolRunOptions? options = null, string? toolCallId = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(name); - ArgumentNullException.ThrowIfNull(args); - _session.ThrowIfDisposed(); - - var request = new FactoryToolRunRequest { SessionId = _session.SessionId, Name = name, Args = CopilotClient.ToJsonElementForWire(args)!.Value, Options = options, ToolCallId = toolCallId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.runFromTool", [request], cancellationToken); - } - - /// Internal tool-originated factory resume. - /// Factory run identifier. - /// Optional per-invocation resource ceiling overrides. - /// Opaque identifier of the originating tool call. - /// The to monitor for cancellation requests. The default is . - /// Resolved persisted factory identity and resumed run envelope. - internal async Task ResumeFromToolAsync(string runId, FactoryRunLimits? limits = null, string? toolCallId = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - _session.ThrowIfDisposed(); - - var request = new FactoryToolResumeRequest { SessionId = _session.SessionId, RunId = runId, Limits = limits, ToolCallId = toolCallId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.resumeFromTool", [request], cancellationToken); - } - - /// Gets the current or settled envelope for a factory run. - /// Factory run identifier. - /// The to monitor for cancellation requests. The default is . - /// Complete current or terminal factory run envelope. - public async Task GetRunAsync(string runId, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - _session.ThrowIfDisposed(); - - var request = new FactoryGetRunRequest { SessionId = _session.SessionId, RunId = runId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.getRun", [request], cancellationToken); - } - - /// Lists durable factory runs for this session in creation order. - /// Exclusive forward cursor. - /// Exclusive backward cursor. - /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. - /// The to monitor for cancellation requests. The default is . - /// A page of factory runs in durable creation order. - public async Task ListRunsAsync(long? afterSeq = null, long? beforeSeq = null, int? limit = null, CancellationToken cancellationToken = default) - { - _session.ThrowIfDisposed(); - - var request = new FactoryListRunsRequest { SessionId = _session.SessionId, AfterSeq = afterSeq, BeforeSeq = beforeSeq, Limit = limit }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.listRuns", [request], cancellationToken); - } - - /// Gets durable and live observability detail for one factory run. - /// Factory run identifier. - /// The to monitor for cancellation requests. The default is . - /// Full factory run observability detail. - public async Task GetRunDetailAsync(string runId, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - _session.ThrowIfDisposed(); - - var request = new FactoryGetRunRequest { SessionId = _session.SessionId, RunId = runId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.getRunDetail", [request], cancellationToken); - } - - /// Pages durable progress for one factory run. - /// Factory run identifier. - /// Optional phase identifier used to scope records and cursors. - /// Exclusive forward cursor. - /// Exclusive backward cursor. - /// Maximum records to return. Defaults to 200 and is capped at 500. - /// The to monitor for cancellation requests. The default is . - /// A bidirectional page of factory progress. - public async Task GetRunProgressAsync(string runId, string? phaseId = null, long? afterSeq = null, long? beforeSeq = null, int? limit = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - _session.ThrowIfDisposed(); - - var request = new FactoryGetRunProgressRequest { SessionId = _session.SessionId, RunId = runId, PhaseId = phaseId, AfterSeq = afterSeq, BeforeSeq = beforeSeq, Limit = limit }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.getRunProgress", [request], cancellationToken); - } - - /// Requests cancellation of a factory run and returns its run envelope. - /// Factory run identifier. - /// The to monitor for cancellation requests. The default is . - /// Complete current or terminal factory run envelope. - public async Task CancelAsync(string runId, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - _session.ThrowIfDisposed(); - - var request = new FactoryCancelRequest { SessionId = _session.SessionId, RunId = runId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.cancel", [request], cancellationToken); - } - - /// Pauses a running factory and returns its settled run envelope. - /// Factory run identifier. - /// The to monitor for cancellation requests. The default is . - /// Complete current or terminal factory run envelope. - public async Task PauseAsync(string runId, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - _session.ThrowIfDisposed(); - - var request = new FactoryPauseRequest { SessionId = _session.SessionId, RunId = runId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.pause", [request], cancellationToken); - } - - /// Atomically pauses an owned factory attempt at a durable checkpoint. - /// Factory run identifier. - /// Opaque token identifying the execution attempt that reached the checkpoint. - /// Stable author-defined checkpoint key. - /// The to monitor for cancellation requests. The default is . - internal async Task PauseAtCheckpointAsync(string runId, string executionToken, string key, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - ArgumentNullException.ThrowIfNull(executionToken); - ArgumentNullException.ThrowIfNull(key); - _session.ThrowIfDisposed(); - - var request = new FactoryPauseCheckpointRequest { SessionId = _session.SessionId, RunId = runId, ExecutionToken = executionToken, Key = key }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.pauseAtCheckpoint", [request], cancellationToken); - } - - /// Records a batch of ordered factory progress lines. - /// Factory run identifier. - /// Opaque token identifying the current factory execution attempt. - /// Ordered progress lines to append. - /// The to monitor for cancellation requests. The default is . - /// Acknowledgement that a factory request was accepted. - public async Task LogAsync(string runId, string executionToken, IList lines, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - ArgumentNullException.ThrowIfNull(executionToken); - ArgumentNullException.ThrowIfNull(lines); - _session.ThrowIfDisposed(); - - var request = new FactoryLogRequest { SessionId = _session.SessionId, RunId = runId, ExecutionToken = executionToken, Lines = lines }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.log", [request], cancellationToken); - } - - /// Runs one factory-scoped subagent and returns its result. - /// Factory run identifier that owns the subagent. - /// Opaque token identifying the current factory execution attempt. - /// Prompt to send to the subagent. - /// Subagent execution options. - /// The to monitor for cancellation requests. The default is . - /// Result of one factory-scoped subagent call. - public async Task AgentAsync(string factoryRunId, string executionToken, string prompt, FactoryAgentOptions opts, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(factoryRunId); - ArgumentNullException.ThrowIfNull(executionToken); - ArgumentNullException.ThrowIfNull(prompt); - ArgumentNullException.ThrowIfNull(opts); - _session.ThrowIfDisposed(); - - var request = new FactoryAgentRequest { SessionId = _session.SessionId, FactoryRunId = factoryRunId, ExecutionToken = executionToken, Prompt = prompt, Opts = opts }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.agent", [request], cancellationToken); - } - - /// Journal APIs. - public FactoryJournalApi Journal => - field ?? - Interlocked.CompareExchange(ref field, new(_session), null) ?? - field; -} - -/// Provides session-scoped FactoryJournal APIs. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FactoryJournalApi -{ - private readonly CopilotSession _session; - - internal FactoryJournalApi(CopilotSession session) - { - _session = session; - } - - /// Reads a memoized factory journal entry. - /// Factory run identifier. - /// Opaque token identifying the current factory execution attempt. - /// Namespaced journal key. - /// The to monitor for cancellation requests. The default is . - /// Result of reading a factory journal entry. - public async Task GetAsync(string runId, string executionToken, string key, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - ArgumentNullException.ThrowIfNull(executionToken); - ArgumentNullException.ThrowIfNull(key); - _session.ThrowIfDisposed(); - - var request = new FactoryJournalGetRequest { SessionId = _session.SessionId, RunId = runId, ExecutionToken = executionToken, Key = key }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.journal.get", [request], cancellationToken); - } - - /// Stores a memoized factory journal entry. - /// Factory run identifier. - /// Opaque token identifying the current factory execution attempt. - /// Namespaced journal key. - /// JSON result to memoize. - /// The to monitor for cancellation requests. The default is . - /// Acknowledgement that a factory request was accepted. - public async Task PutAsync(string runId, string executionToken, string key, object resultJson, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(runId); - ArgumentNullException.ThrowIfNull(executionToken); - ArgumentNullException.ThrowIfNull(key); - ArgumentNullException.ThrowIfNull(resultJson); - _session.ThrowIfDisposed(); - - var request = new FactoryJournalPutRequest { SessionId = _session.SessionId, RunId = runId, ExecutionToken = executionToken, Key = key, ResultJson = CopilotClient.ToJsonElementForWire(resultJson)!.Value }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.journal.put", [request], cancellationToken); - } -} - /// Provides session-scoped Workflow APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class WorkflowApi @@ -39934,13 +46672,15 @@ public async Task SwitchAutoTierAsync(AutoTier? autoT /// 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. /// Model explicitly selected by the CLI, when provided. /// Whether the overlay is being applied while resuming a deferred session. + /// 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. + /// 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. /// The to monitor for cancellation requests. The default is . /// The model identifier active on the session after the switch. - internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? policyHelperModel = null, AutoTier? autoTier = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? repoAutoTier = null, string? cliModel = null, bool? deferredResume = null, CancellationToken cancellationToken = default) + internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? policyHelperModel = null, AutoTier? autoTier = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? repoAutoTier = null, string? cliModel = null, bool? deferredResume = null, string? managedReasoningEffort = null, string? managedContextTier = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, PolicyHelperModel = policyHelperModel, AutoTier = autoTier, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, RepoAutoTier = repoAutoTier, CliModel = cliModel, DeferredResume = deferredResume }; + var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, PolicyHelperModel = policyHelperModel, AutoTier = autoTier, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, RepoAutoTier = repoAutoTier, CliModel = cliModel, DeferredResume = deferredResume, ManagedReasoningEffort = managedReasoningEffort, ManagedContextTier = managedContextTier }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.applyStartupOverlay", [request], cancellationToken); } @@ -40487,14 +47227,15 @@ public async Task GetSourcesAsync(CancellationToke return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.getSources", [request], cancellationToken); } - /// Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk. + /// For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. /// The to monitor for cancellation requests. The default is . - public async Task ReloadAsync(CancellationToken cancellationToken = default) + /// Instruction sources loaded for the session, in merge order. + public async Task ReloadAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); var request = new SessionInstructionsReloadRequest { SessionId = _session.SessionId }; - await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.reload", [request], cancellationToken); + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.reload", [request], cancellationToken); } } @@ -40509,15 +47250,15 @@ internal CustomizationsApi(CopilotSession session) _session = session; } - /// Reloads all repository and user customizations for the active session: instructions, plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns diagnostics from the final skill reload. + /// For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. /// The to monitor for cancellation requests. The default is . - /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. - public async Task ReloadAsync(CancellationToken cancellationToken = default) + /// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. + public async Task ReloadAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); var request = new SessionCustomizationsReloadRequest { SessionId = _session.SessionId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.customizations.reload", [request], cancellationToken); + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.customizations.reload", [request], cancellationToken); } } @@ -40910,7 +47651,18 @@ internal McpApi(CopilotSession session) _session = session; } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + /// The to monitor for cancellation requests. The default is . + internal async Task SetConnectedIdeInfoAsync(SessionConnectedIdeInfo? ide = null, CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpSetConnectedIdeInfoParams { SessionId = _session.SessionId, Ide = ide }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.setConnectedIdeInfo", [request], cancellationToken); + } + + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// The to monitor for cancellation requests. The default is . /// MCP servers configured for the session, with their connection status and host-level state. public async Task ListAsync(CancellationToken cancellationToken = default) @@ -40921,6 +47673,17 @@ public async Task ListAsync(CancellationToken cancellationToken = return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.list", [request], cancellationToken); } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// The to monitor for cancellation requests. The default is . + /// Effective MCP configuration with optional live observations from matching already materialized servers. + public async Task ListConfiguredAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpListConfiguredRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.listConfigured", [request], cancellationToken); + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// Name of the connected MCP server whose tools to list. /// The to monitor for cancellation requests. The default is . @@ -40942,10 +47705,22 @@ public async Task EnableAsync(string serverName, CancellationToken cancellationT ArgumentNullException.ThrowIfNull(serverName); _session.ThrowIfDisposed(); - var request = new McpEnableRequest { SessionId = _session.SessionId, ServerName = serverName }; + var request = new McpEnableRequestWithSession { SessionId = _session.SessionId, ServerName = serverName }; await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.enable", [request], cancellationToken); } + /// Enables an MCP server for the session. + /// Name of the MCP server to enable for the session. + /// The to monitor for cancellation requests. The default is . + public async Task EnableAsync(McpEnableRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.ServerName); + _session.ThrowIfDisposed(); + var wireRequest = new McpEnableRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, ExpectedInstallationId = request.ExpectedInstallationId }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.enable", [wireRequest], cancellationToken); + } + /// Disables an MCP server for the session. /// Name of the MCP server to disable. /// The to monitor for cancellation requests. The default is . @@ -40954,10 +47729,22 @@ public async Task DisableAsync(string serverName, CancellationToken cancellation ArgumentNullException.ThrowIfNull(serverName); _session.ThrowIfDisposed(); - var request = new McpDisableRequest { SessionId = _session.SessionId, ServerName = serverName }; + var request = new McpDisableRequestWithSession { SessionId = _session.SessionId, ServerName = serverName }; await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.disable", [request], cancellationToken); } + /// Disables an MCP server for the session. + /// Name of the MCP server to disable for the session. + /// The to monitor for cancellation requests. The default is . + public async Task DisableAsync(McpDisableRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.ServerName); + _session.ThrowIfDisposed(); + var wireRequest = new McpDisableRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, ExpectedInstallationId = request.ExpectedInstallationId }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.disable", [wireRequest], cancellationToken); + } + /// Reloads MCP server connections for the session. /// The to monitor for cancellation requests. The default is . public async Task ReloadAsync(CancellationToken cancellationToken = default) @@ -41065,10 +47852,22 @@ public async Task StartServerAsync(string serverName, object? config = null, Can ArgumentNullException.ThrowIfNull(serverName); _session.ThrowIfDisposed(); - var request = new McpStartServerRequest { SessionId = _session.SessionId, ServerName = serverName, Config = CopilotClient.ToJsonElementForWire(config) }; + var request = new McpStartServerRequestWithSession { SessionId = _session.SessionId, ServerName = serverName, Config = CopilotClient.ToJsonElementForWire(config) }; await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.startServer", [request], cancellationToken); } + /// 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. + /// 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. + /// The to monitor for cancellation requests. The default is . + public async Task StartServerAsync(McpStartServerRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.ServerName); + _session.ThrowIfDisposed(); + var wireRequest = new McpStartServerRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, Config = request.Config, ExpectedInstallationId = request.ExpectedInstallationId }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.startServer", [wireRequest], cancellationToken); + } + /// 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.*`). /// Name of the MCP server to restart. /// 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). @@ -41078,10 +47877,22 @@ public async Task RestartServerAsync(string serverName, object? config = null, C ArgumentNullException.ThrowIfNull(serverName); _session.ThrowIfDisposed(); - var request = new McpRestartServerRequest { SessionId = _session.SessionId, ServerName = serverName, Config = CopilotClient.ToJsonElementForWire(config) }; + var request = new McpRestartServerRequestWithSession { SessionId = _session.SessionId, ServerName = serverName, Config = CopilotClient.ToJsonElementForWire(config) }; await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.restartServer", [request], cancellationToken); } + /// 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.*`). + /// 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. + /// The to monitor for cancellation requests. The default is . + public async Task RestartServerAsync(McpRestartServerRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.ServerName); + _session.ThrowIfDisposed(); + var wireRequest = new McpRestartServerRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, Config = request.Config, ExpectedInstallationId = request.ExpectedInstallationId }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.restartServer", [wireRequest], cancellationToken); + } + /// Stops an individual MCP server on the session's host. /// Name of the MCP server to stop. /// The to monitor for cancellation requests. The default is . @@ -41090,10 +47901,22 @@ public async Task StopServerAsync(string serverName, CancellationToken cancellat ArgumentNullException.ThrowIfNull(serverName); _session.ThrowIfDisposed(); - var request = new McpStopServerRequest { SessionId = _session.SessionId, ServerName = serverName }; + var request = new McpStopServerRequestWithSession { SessionId = _session.SessionId, ServerName = serverName }; await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.stopServer", [request], cancellationToken); } + /// Stops an individual MCP server on the session's host. + /// Server name for an individual MCP server stop. + /// The to monitor for cancellation requests. The default is . + public async Task StopServerAsync(McpStopServerRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.ServerName); + _session.ThrowIfDisposed(); + var wireRequest = new McpStopServerRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, ExpectedInstallationId = request.ExpectedInstallationId }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.stopServer", [wireRequest], cancellationToken); + } + /// 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. /// Logical server name for the external client. /// The to monitor for cancellation requests. The default is . @@ -41154,6 +47977,12 @@ public async Task IsServerRunningAsync(string serverNa field ?? Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + + /// Prompts APIs. + public McpPromptsApi Prompts => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; } /// Provides session-scoped McpOauth APIs. @@ -41194,7 +48023,25 @@ public async Task AuthenticationStateChangedAsync(string? serverName = null, boo await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.authenticationStateChanged", [request], cancellationToken); } - /// Starts OAuth authentication for a remote MCP server. + /// 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. + /// Name recorded by the authoritative owned installation receipt. + /// Exact installation identity from owned inventory, never a server-name alias. + /// Request a new authorisation rather than accepting a usable cached grant. + /// Display name used by the incumbent OAuth client-registration flow. + /// Text shown on the loopback callback page after successful authorisation. + /// The to monitor for cancellation requests. The default is . + /// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. + public async Task PrepareLoginAsync(string serverName, string expectedInstallationId, bool? forceReauth = null, string? clientName = null, string? callbackSuccessMessage = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + ArgumentNullException.ThrowIfNull(expectedInstallationId); + _session.ThrowIfDisposed(); + + var request = new SessionMcpOauthPrepareLoginRequest { SessionId = _session.SessionId, ServerName = serverName, ExpectedInstallationId = expectedInstallationId, ForceReauth = forceReauth, ClientName = clientName, CallbackSuccessMessage = callbackSuccessMessage }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.prepareLogin", [request], cancellationToken); + } + + /// 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. /// Name of the remote MCP server to authenticate. /// 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. /// 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. @@ -41210,10 +48057,37 @@ public async Task LoginAsync(string serverName, bool? force ArgumentNullException.ThrowIfNull(serverName); _session.ThrowIfDisposed(); - var request = new McpOauthLoginRequest { SessionId = _session.SessionId, ServerName = serverName, ForceReauth = forceReauth, ClientName = clientName, CallbackSuccessMessage = callbackSuccessMessage, ClientId = clientId, ClientSecret = clientSecret, PublicClient = publicClient, GrantType = grantType }; + var request = new McpOauthLoginRequestWithSession { SessionId = _session.SessionId, ServerName = serverName, ForceReauth = forceReauth, ClientName = clientName, CallbackSuccessMessage = callbackSuccessMessage, ClientId = clientId, ClientSecret = clientSecret, PublicClient = publicClient, GrantType = grantType }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.login", [request], cancellationToken); } + /// 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 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) + { + 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, 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 . @@ -41223,10 +48097,38 @@ public async Task ProbeAsync(string serverName, Cancellatio ArgumentNullException.ThrowIfNull(serverName); _session.ThrowIfDisposed(); - var request = new McpOauthProbeRequest { SessionId = _session.SessionId, ServerName = serverName }; + var request = new McpOauthProbeRequestWithSession { SessionId = _session.SessionId, ServerName = serverName }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.probe", [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. + /// Remote MCP server name for a passive OAuth status probe. + /// The to monitor for cancellation requests. The default is . + /// 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. + public async Task ProbeAsync(McpOauthProbeRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.ServerName); + _session.ThrowIfDisposed(); + var wireRequest = new McpOauthProbeRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, ExpectedInstallationId = request.ExpectedInstallationId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.probe", [wireRequest], cancellationToken); + } + + /// Cancels the exact owned OAuth login issued to this original session requester, without clearing shared credentials. + /// The same authoritative installation identity supplied during preparation. + /// Runtime-issued login handle known before the effectful login request begins. + /// The to monitor for cancellation requests. The default is . + /// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. + public async Task CancelLoginAsync(string expectedInstallationId, string loginId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(expectedInstallationId); + ArgumentNullException.ThrowIfNull(loginId); + _session.ThrowIfDisposed(); + + var request = new SessionMcpOauthCancelLoginRequest { SessionId = _session.SessionId, ExpectedInstallationId = expectedInstallationId, LoginId = loginId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.cancelLogin", [request], cancellationToken); + } + /// Responds to a pending MCP OAuth authorization request by its request id. /// OAuth request identifier from the mcp.oauth_required event. /// The to monitor for cancellation requests. The default is . @@ -41419,6 +48321,48 @@ public async Task ListTemplatesAsync(string ser } } +/// Provides session-scoped McpPrompts APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpPromptsApi +{ + private readonly CopilotSession _session; + + internal McpPromptsApi(CopilotSession session) + { + _session = session; + } + + /// Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// Name of the MCP server whose prompts to enumerate. + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + /// The to monitor for cancellation requests. The default is . + /// One page of prompts advertised by the named MCP server. + public async Task ListAsync(string serverName, string? cursor = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + _session.ThrowIfDisposed(); + + var request = new McpPromptsListRequest { SessionId = _session.SessionId, ServerName = serverName, Cursor = cursor }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.prompts.list", [request], cancellationToken); + } + + /// Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch referenced resources. + /// Name of the MCP server hosting the prompt. + /// The programmatic name of the prompt. + /// String-valued arguments to pass to the prompt. + /// The to monitor for cancellation requests. The default is . + /// Prompt messages returned by the MCP server without sending them to the model. + public async Task GetAsync(string serverName, string promptName, IDictionary? arguments = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + ArgumentNullException.ThrowIfNull(promptName); + _session.ThrowIfDisposed(); + + var request = new McpPromptsGetRequest { SessionId = _session.SessionId, ServerName = serverName, PromptName = promptName, Arguments = arguments }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.prompts.get", [request], cancellationToken); + } +} + /// Provides session-scoped Diagnostics APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class DiagnosticsApi @@ -41482,6 +48426,17 @@ public async Task GetCapabilitiesAsync(CancellationToken return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.getCapabilities", [request], cancellationToken); } + /// Returns the session account selection, or null. + /// The to monitor for cancellation requests. The default is . + /// Session account selection, or null. + public async Task GetAccountAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionConnectorsGetAccountRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.getAccount", [request], cancellationToken); + } + /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. /// The to monitor for cancellation requests. The default is . /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. @@ -41590,10 +48545,23 @@ public async Task ReconcileAsync(string accountId, bool? refres ArgumentNullException.ThrowIfNull(accountId); _session.ThrowIfDisposed(); - var request = new ConnectorReconcileRequest { SessionId = _session.SessionId, AccountId = accountId, RefreshCatalog = refreshCatalog }; + var request = new ConnectorReconcileRequestWithSession { SessionId = _session.SessionId, AccountId = accountId, RefreshCatalog = refreshCatalog }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.reconcile", [request], cancellationToken); } + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. + /// Requests authoritative Connector-to-MCP reconciliation for the pinned account. + /// The to monitor for cancellation requests. The default is . + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + public async Task ReconcileAsync(ConnectorReconcileRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.AccountId); + _session.ThrowIfDisposed(); + var wireRequest = new ConnectorReconcileRequestWithSession { SessionId = _session.SessionId, AccountId = request.AccountId, RefreshCatalog = request.RefreshCatalog, ForceConnectorName = request.ForceConnectorName }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.reconcile", [wireRequest], cancellationToken); + } + /// Reconciles the authoritative Connector catalog into the session MCP projection during startup with a bounded deadline and fail-closed cleanup. /// Opaque account selection ID previously returned by an account discovery API. /// The to monitor for cancellation requests. The default is . @@ -41632,7 +48600,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(); @@ -41869,6 +48837,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. @@ -41906,7 +48887,7 @@ internal OptionsApi(CopilotSession session) /// Use shell.initProfile instead. Shell init profile (`None` or `NonInteractive`). /// PowerShell process flags applied to built-in and user-requested shell commands. /// Resolved sandbox configuration. - /// Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. + /// 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. /// Whether interactive shell sessions are logged. /// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. @@ -42350,7 +49331,7 @@ public async Task EphemeralQueryAsync(string question, C /// MCP task metadata. /// The to monitor for cancellation requests. The default is . /// The elicitation response (accept with form values, decline, or cancel). - public async Task ElicitationAsync(string message, UIElicitationSchema requestedSchema, McpElicitationFormMode? mode = null, UIElicitationRequestMeta? _meta = null, McpTaskMetadata? task = null, CancellationToken cancellationToken = default) + public async Task ElicitationAsync(string message, UIElicitationSchema requestedSchema, McpElicitationFormMode? mode = null, IDictionary? _meta = null, McpTaskMetadata? task = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); ArgumentNullException.ThrowIfNull(requestedSchema); @@ -42375,6 +49356,21 @@ public async Task HandlePendingElicitationAsync(string requ return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingElicitation", [request], cancellationToken); } + /// Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. + /// The unique request ID from the elicitation.requested event. + /// The elicitation response (accept with form values, decline, or cancel). + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + internal async Task HandleHumanAskUserAsync(string requestId, UIElicitationResponse result, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(result); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingElicitationRequest { SessionId = _session.SessionId, RequestId = requestId, Result = result }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanAskUser", [request], cancellationToken); + } + /// Resolves a pending `user_input.requested` event with the user's response. /// The unique request ID from the user_input.requested event. /// User response for a pending user-input request, with answer text and whether it was typed freeform. @@ -42390,6 +49386,21 @@ public async Task HandlePendingUserInputAsync(string requ return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingUserInput", [request], cancellationToken); } + /// Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + /// The unique request ID from the user_input.requested event. + /// User response for a pending user-input request, with answer text and whether it was typed freeform. + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the pending UI request was resolved by this call. + internal async Task HandleHumanUserInputAsync(string requestId, UIUserInputResponse response, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(response); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingUserInputRequest { SessionId = _session.SessionId, RequestId = requestId, Response = response }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanUserInput", [request], cancellationToken); + } + /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. /// The unique request ID from the sampling.requested event. /// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. @@ -42448,6 +49459,21 @@ public async Task HandlePendingExitPlanModeAsync(string r return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingExitPlanMode", [request], cancellationToken); } + /// Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + /// The unique request ID from the exit_plan_mode.requested event. + /// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the pending UI request was resolved by this call. + internal async Task HandleHumanExitPlanModeAsync(string requestId, UIExitPlanModeResponse response, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(response); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingExitPlanModeRequest { SessionId = _session.SessionId, RequestId = requestId, Response = response }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanExitPlanMode", [request], cancellationToken); + } + /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. /// The to monitor for cancellation requests. The default is . /// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). @@ -42544,7 +49570,7 @@ public async Task SetApproveAllAsync(bool enable /// 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. /// Permission mode to apply. /// 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. - /// Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. + /// 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. /// The to monitor for cancellation requests. The default is . /// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. public async Task SetModeAsync(PermissionMode mode, string? assistedApprovalModel = null, PermissionModeSource? source = null, CancellationToken cancellationToken = default) @@ -42903,8 +49929,8 @@ public async Task ActivityAsync(CancellationToken cancellationT } /// Returns the token breakdown for the session's current context window for a given model. - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. /// The to monitor for cancellation requests. The default is . /// Token breakdown for the session's current context window, or null if uninitialized. @@ -43051,12 +50077,12 @@ internal ShellApi(CopilotSession session) _session = session; } - /// Starts a shell command and streams output through session notifications. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. + /// Starts a shell command, returning an RPC error if it cannot be spawned. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. /// Shell command to execute. /// Working directory (defaults to session working directory). /// Timeout in milliseconds (default: 30000). /// The to monitor for cancellation requests. The default is . - /// Identifier of the spawned process, used to correlate streamed output and exit notifications. + /// Identifier of the spawned shell process, usable with shell.kill while the process is running. public async Task ExecAsync(string command, string? cwd = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); @@ -43838,22 +50864,6 @@ public interface IProviderTokenHandler Task GetTokenAsync(ProviderTokenAcquireRequest request, CancellationToken cancellationToken = default); } -/// Handles `factory` client session API methods. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public interface IFactoryHandler -{ - /// Asks the owning extension connection to execute a registered factory closure. - /// Parameters sent to the owning extension to execute a factory closure. - /// The to monitor for cancellation requests. The default is . - /// Result returned by an extension factory closure. - Task ExecuteAsync(FactoryExecuteRequest request, CancellationToken cancellationToken = default); - /// Asks the owning extension connection to abort a running factory cooperatively. - /// Parameters for cooperatively aborting a factory body. - /// The to monitor for cancellation requests. The default is . - /// Acknowledgement that a factory request was accepted. - Task AbortAsync(FactoryAbortRequest request, CancellationToken cancellationToken = default); -} - /// Handles `workflow` client session API methods. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public interface IWorkflowHandler @@ -43890,11 +50900,21 @@ public interface ISessionFsHandler /// The to monitor for cancellation requests. The default is . /// File content as a UTF-8 string, or a filesystem error if the read failed. Task ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken = default); + /// Reads binary file content from the client-provided session filesystem. + /// Path of the binary file to read from the client-provided session filesystem. + /// The to monitor for cancellation requests. The default is . + /// File bytes as standard base64, or a filesystem error if the read failed. + Task ReadFileBytesAsync(SessionFsReadFileBytesRequest request, CancellationToken cancellationToken = default); /// Writes a file in the client-provided session filesystem. /// File path, content to write, and optional mode for the client-provided session filesystem. /// The to monitor for cancellation requests. The default is . /// Describes a filesystem error. Task WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken = default); + /// Writes binary file content to the client-provided session filesystem. + /// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + /// The to monitor for cancellation requests. The default is . + /// Describes a filesystem error. + Task WriteFileBytesAsync(SessionFsWriteFileBytesRequest request, CancellationToken cancellationToken = default); /// Appends content to a file in the client-provided session filesystem, creating parent directories as needed. /// File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed. /// The to monitor for cancellation requests. The default is . @@ -43978,9 +50998,6 @@ public sealed class ClientSessionApiHandlers /// Optional handler for ProviderToken client session API methods. public IProviderTokenHandler? ProviderToken { get; set; } - /// Optional handler for Factory client session API methods. - public IFactoryHandler? Factory { get; set; } - /// Optional handler for Workflow client session API methods. public IWorkflowHandler? Workflow { get; set; } @@ -44010,18 +51027,6 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, Func>)(async (request, cancellationToken) => - { - var handler = getHandlers(request.SessionId).Factory; - if (handler is null) throw new InvalidOperationException($"No factory handler registered for session: {request.SessionId}"); - return await handler.ExecuteAsync(request, cancellationToken); - }), singleObjectParam: true); - rpc.SetLocalRpcMethod("factory.abort", (Func>)(async (request, cancellationToken) => - { - var handler = getHandlers(request.SessionId).Factory; - if (handler is null) throw new InvalidOperationException($"No factory handler registered for session: {request.SessionId}"); - return await handler.AbortAsync(request, cancellationToken); - }), singleObjectParam: true); rpc.SetLocalRpcMethod("workflow.execute", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).Workflow; @@ -44046,12 +51051,24 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).SessionFs; + if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); + return await handler.ReadFileBytesAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.writeFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); return await handler.WriteFileAsync(request, cancellationToken); }), singleObjectParam: true); + rpc.SetLocalRpcMethod("sessionFs.writeFileBytes", (Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).SessionFs; + if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); + return await handler.WriteFileBytesAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.appendFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; @@ -44139,6 +51156,16 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, FuncHandles `host` client global API methods. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public interface IHostHandler +{ + /// Reports termination of a connection-owned host listener. + /// Reports a supervised listener's hosting-task termination and cleanup outcome. + /// The to monitor for cancellation requests. The default is . + Task ExitedAsync(HostExitedRequest request, CancellationToken cancellationToken = default); +} + /// Handles `extensionLaunchProvider` client global API methods. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public interface IExtensionLaunchProviderHandler @@ -44187,9 +51214,23 @@ public interface IGitHubTokenHandler Task GetTokenAsync(GitHubTokenAcquireRequest request, CancellationToken cancellationToken = default); } +/// Handles `installations` client global API methods. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public interface IInstallationsHandler +{ + /// Requests a fresh explicit human decision for one sealed installation operation on its original connection. Present the complete typed review, return the original challenge and fingerprint, and never infer approval. The expiresAt deadline, connection closure or standard JSON-RPC $/cancelRequest retires the request; late replies grant no authority. + /// One connection-owned, expiring request for a trusted host's explicit user decision. + /// The to monitor for cancellation requests. The default is . + /// A response is meaningful only on the connection and request that issued its challenge. + Task ConfirmAsync(InstallationsConfirmRequest request, CancellationToken cancellationToken = default); +} + /// Provides all client global API handler groups for a connection. public sealed class ClientGlobalApiHandlers { + /// Optional handler for Host client global API methods. + public IHostHandler? Host { get; set; } + /// Optional handler for ExtensionLaunchProvider client global API methods. public IExtensionLaunchProviderHandler? ExtensionLaunchProvider { get; set; } @@ -44201,6 +51242,9 @@ public sealed class ClientGlobalApiHandlers /// Optional handler for GitHubToken client global API methods. public IGitHubTokenHandler? GitHubToken { get; set; } + + /// Optional handler for Installations client global API methods. + public IInstallationsHandler? Installations { get; set; } } /// Registers client global API handlers on a JSON-RPC connection. @@ -44214,6 +51258,11 @@ internal static class ClientGlobalApiRegistration /// public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiHandlers handlers) { + rpc.SetLocalRpcMethod("host.exited", (Func)(async (request, cancellationToken) => + { + var handler = handlers.Host ?? throw new InvalidOperationException("No host client-global handler registered"); + await handler.ExitedAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("extensionLaunchProvider.resolve", (Func>)(async (request, cancellationToken) => { var handler = handlers.ExtensionLaunchProvider ?? throw new InvalidOperationException("No extensionLaunchProvider client-global handler registered"); @@ -44239,6 +51288,11 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH var handler = handlers.GitHubToken ?? throw new InvalidOperationException("No gitHubToken client-global handler registered"); return await handler.GetTokenAsync(request, cancellationToken); }), singleObjectParam: true); + rpc.SetLocalRpcMethod("installations.confirm", (Func>)(async (request, cancellationToken) => + { + var handler = handlers.Installations ?? throw new InvalidOperationException("No installations client-global handler registered"); + return await handler.ConfirmAsync(request, cancellationToken); + }), singleObjectParam: true); } } @@ -44393,17 +51447,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ExternalToolCompletedEvent), TypeInfoPropertyName = "SessionEventsExternalToolCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.ExternalToolRequestedData), TypeInfoPropertyName = "SessionEventsExternalToolRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.ExternalToolRequestedEvent), TypeInfoPropertyName = "SessionEventsExternalToolRequestedEvent")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryPermissionOperation), TypeInfoPropertyName = "SessionEventsFactoryPermissionOperation")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryPermissionPhase), TypeInfoPropertyName = "SessionEventsFactoryPermissionPhase")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryRunSettledData), TypeInfoPropertyName = "SessionEventsFactoryRunSettledData")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryRunSettledEvent), TypeInfoPropertyName = "SessionEventsFactoryRunSettledEvent")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryRunSettledStatus), TypeInfoPropertyName = "SessionEventsFactoryRunSettledStatus")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryRunStartedData), TypeInfoPropertyName = "SessionEventsFactoryRunStartedData")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryRunStartedEvent), TypeInfoPropertyName = "SessionEventsFactoryRunStartedEvent")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryRunUpdatedData), TypeInfoPropertyName = "SessionEventsFactoryRunUpdatedData")] -[JsonSerializable(typeof(GitHub.Copilot.FactoryRunUpdatedEvent), TypeInfoPropertyName = "SessionEventsFactoryRunUpdatedEvent")] [JsonSerializable(typeof(GitHub.Copilot.FusionAttribution), TypeInfoPropertyName = "SessionEventsFusionAttribution")] [JsonSerializable(typeof(GitHub.Copilot.FusionConversationScope), TypeInfoPropertyName = "SessionEventsFusionConversationScope")] +[JsonSerializable(typeof(GitHub.Copilot.FusionCritic), TypeInfoPropertyName = "SessionEventsFusionCritic")] [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpAction), TypeInfoPropertyName = "SessionEventsFusionFollowUpAction")] [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpRecommendation), TypeInfoPropertyName = "SessionEventsFusionFollowUpRecommendation")] [JsonSerializable(typeof(GitHub.Copilot.FusionPattern), TypeInfoPropertyName = "SessionEventsFusionPattern")] @@ -44427,6 +51473,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.HookProgressEvent), TypeInfoPropertyName = "SessionEventsHookProgressEvent")] [JsonSerializable(typeof(GitHub.Copilot.HookStartData), TypeInfoPropertyName = "SessionEventsHookStartData")] [JsonSerializable(typeof(GitHub.Copilot.HookStartEvent), TypeInfoPropertyName = "SessionEventsHookStartEvent")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseActor), TypeInfoPropertyName = "SessionEventsHumanResponseActor")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedData), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedData")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedEvent), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedResponse), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedResponse")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchDisabledReason), TypeInfoPropertyName = "SessionEventsIndexedSearchDisabledReason")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchErrorType), TypeInfoPropertyName = "SessionEventsIndexedSearchErrorType")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchIncrementalPhase), TypeInfoPropertyName = "SessionEventsIndexedSearchIncrementalPhase")] @@ -44470,6 +51520,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")] @@ -44507,7 +51560,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestExtensionEnvAccess), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestExtensionEnvAccess")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestExtensionManagement), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestExtensionManagement")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestExtensionPermissionAccess), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestExtensionPermissionAccess")] -[JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestFactory), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestFactory")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestHook), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestHook")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestMcp), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestMcp")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestMemory), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestMemory")] @@ -44515,6 +51567,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestPathAccessKind), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestPathAccessKind")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestRead), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestRead")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestUrl), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestUrl")] +[JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestWorkflow), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestWorkflow")] [JsonSerializable(typeof(GitHub.Copilot.PermissionPromptRequestWrite), TypeInfoPropertyName = "SessionEventsPermissionPromptRequestWrite")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRecommendation), TypeInfoPropertyName = "SessionEventsPermissionRecommendation")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRecoveryAttempt), TypeInfoPropertyName = "SessionEventsPermissionRecoveryAttempt")] @@ -44530,7 +51583,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestExtensionEnvAccess), TypeInfoPropertyName = "SessionEventsPermissionRequestExtensionEnvAccess")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestExtensionManagement), TypeInfoPropertyName = "SessionEventsPermissionRequestExtensionManagement")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestExtensionPermissionAccess), TypeInfoPropertyName = "SessionEventsPermissionRequestExtensionPermissionAccess")] -[JsonSerializable(typeof(GitHub.Copilot.PermissionRequestFactory), TypeInfoPropertyName = "SessionEventsPermissionRequestFactory")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestHook), TypeInfoPropertyName = "SessionEventsPermissionRequestHook")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestMcp), TypeInfoPropertyName = "SessionEventsPermissionRequestMcp")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestMemory), TypeInfoPropertyName = "SessionEventsPermissionRequestMemory")] @@ -44543,11 +51595,14 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestShellCommandSegment), TypeInfoPropertyName = "SessionEventsPermissionRequestShellCommandSegment")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestShellPossibleUrl), TypeInfoPropertyName = "SessionEventsPermissionRequestShellPossibleUrl")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestUrl), TypeInfoPropertyName = "SessionEventsPermissionRequestUrl")] +[JsonSerializable(typeof(GitHub.Copilot.PermissionRequestWorkflow), TypeInfoPropertyName = "SessionEventsPermissionRequestWorkflow")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestWrite), TypeInfoPropertyName = "SessionEventsPermissionRequestWrite")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestedData), TypeInfoPropertyName = "SessionEventsPermissionRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRequestedEvent), TypeInfoPropertyName = "SessionEventsPermissionRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.PermissionResult), TypeInfoPropertyName = "SessionEventsPermissionResult")] [JsonSerializable(typeof(GitHub.Copilot.PermissionRule), TypeInfoPropertyName = "SessionEventsPermissionRule")] +[JsonSerializable(typeof(GitHub.Copilot.PermissionSandboxPathGrant), TypeInfoPropertyName = "SessionEventsPermissionSandboxPathGrant")] +[JsonSerializable(typeof(GitHub.Copilot.PermissionSandboxPathGrantAccess), TypeInfoPropertyName = "SessionEventsPermissionSandboxPathGrantAccess")] [JsonSerializable(typeof(GitHub.Copilot.PersistedBinaryImage), TypeInfoPropertyName = "SessionEventsPersistedBinaryImage")] [JsonSerializable(typeof(GitHub.Copilot.PersistedBinaryImageType), TypeInfoPropertyName = "SessionEventsPersistedBinaryImageType")] [JsonSerializable(typeof(GitHub.Copilot.PersistedBinaryResult), TypeInfoPropertyName = "SessionEventsPersistedBinaryResult")] @@ -44574,6 +51629,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.SandboxEnforcementPoint), TypeInfoPropertyName = "SessionEventsSandboxEnforcementPoint")] [JsonSerializable(typeof(GitHub.Copilot.SandboxFilesystemPolicyDetails), TypeInfoPropertyName = "SessionEventsSandboxFilesystemPolicyDetails")] [JsonSerializable(typeof(GitHub.Copilot.SandboxOutcome), TypeInfoPropertyName = "SessionEventsSandboxOutcome")] +[JsonSerializable(typeof(GitHub.Copilot.SandboxPermissiveSource), TypeInfoPropertyName = "SessionEventsSandboxPermissiveSource")] [JsonSerializable(typeof(GitHub.Copilot.SandboxPlatform), TypeInfoPropertyName = "SessionEventsSandboxPlatform")] [JsonSerializable(typeof(GitHub.Copilot.SandboxPolicySource), TypeInfoPropertyName = "SessionEventsSandboxPolicySource")] [JsonSerializable(typeof(GitHub.Copilot.SandboxProxyMode), TypeInfoPropertyName = "SessionEventsSandboxProxyMode")] @@ -44631,14 +51687,14 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationAgentIdle), TypeInfoPropertyName = "SessionEventsSystemNotificationAgentIdle")] [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationData), TypeInfoPropertyName = "SessionEventsSystemNotificationData")] [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationEvent), TypeInfoPropertyName = "SessionEventsSystemNotificationEvent")] -[JsonSerializable(typeof(GitHub.Copilot.SystemNotificationFactoryCompleted), TypeInfoPropertyName = "SessionEventsSystemNotificationFactoryCompleted")] -[JsonSerializable(typeof(GitHub.Copilot.SystemNotificationFactoryCompletedStatus), TypeInfoPropertyName = "SessionEventsSystemNotificationFactoryCompletedStatus")] -[JsonSerializable(typeof(GitHub.Copilot.SystemNotificationFactoryPauseInfo), TypeInfoPropertyName = "SessionEventsSystemNotificationFactoryPauseInfo")] [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationInstructionDiscovered), TypeInfoPropertyName = "SessionEventsSystemNotificationInstructionDiscovered")] [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationNewInboxMessage), TypeInfoPropertyName = "SessionEventsSystemNotificationNewInboxMessage")] [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationShellCompleted), TypeInfoPropertyName = "SessionEventsSystemNotificationShellCompleted")] [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationShellDetachedCompleted), TypeInfoPropertyName = "SessionEventsSystemNotificationShellDetachedCompleted")] [JsonSerializable(typeof(GitHub.Copilot.SystemNotificationUnclassified), TypeInfoPropertyName = "SessionEventsSystemNotificationUnclassified")] +[JsonSerializable(typeof(GitHub.Copilot.SystemNotificationWorkflowCompleted), TypeInfoPropertyName = "SessionEventsSystemNotificationWorkflowCompleted")] +[JsonSerializable(typeof(GitHub.Copilot.SystemNotificationWorkflowCompletedStatus), TypeInfoPropertyName = "SessionEventsSystemNotificationWorkflowCompletedStatus")] +[JsonSerializable(typeof(GitHub.Copilot.SystemNotificationWorkflowPauseInfo), TypeInfoPropertyName = "SessionEventsSystemNotificationWorkflowPauseInfo")] [JsonSerializable(typeof(GitHub.Copilot.TaskBlocker), TypeInfoPropertyName = "SessionEventsTaskBlocker")] [JsonSerializable(typeof(GitHub.Copilot.TaskBlockerKind), TypeInfoPropertyName = "SessionEventsTaskBlockerKind")] [JsonSerializable(typeof(GitHub.Copilot.TaskCompletionOutcome), TypeInfoPropertyName = "SessionEventsTaskCompletionOutcome")] @@ -44656,6 +51712,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteData), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteData")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteError), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteError")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteEvent), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteFileEdit), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteFileEdit")] +[JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteFileEditKind), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteFileEditKind")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteResult), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteResult")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteShellExecution), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteShellExecution")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteToolDescription), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteToolDescription")] @@ -44683,6 +51741,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartToolDescriptionMetaUIVisibility), TypeInfoPropertyName = "SessionEventsToolExecutionStartToolDescriptionMetaUIVisibility")] [JsonSerializable(typeof(GitHub.Copilot.ToolSearchActivatedData), TypeInfoPropertyName = "SessionEventsToolSearchActivatedData")] [JsonSerializable(typeof(GitHub.Copilot.ToolSearchActivatedEvent), TypeInfoPropertyName = "SessionEventsToolSearchActivatedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputData), TypeInfoPropertyName = "SessionEventsToolShellOutputData")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputEvent), TypeInfoPropertyName = "SessionEventsToolShellOutputEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputStream), TypeInfoPropertyName = "SessionEventsToolShellOutputStream")] [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedData), TypeInfoPropertyName = "SessionEventsToolUserRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedEvent), TypeInfoPropertyName = "SessionEventsToolUserRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.UIEphemeralQueryPhase), TypeInfoPropertyName = "SessionEventsUIEphemeralQueryPhase")] @@ -44700,12 +51761,21 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalExtensionEnvAccess), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalExtensionEnvAccess")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalExtensionManagement), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalExtensionManagement")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalExtensionPermissionAccess), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalExtensionPermissionAccess")] -[JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalFactory), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalFactory")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalMcp), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalMcp")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalMemory), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalMemory")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalRead), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalRead")] +[JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalWorkflow), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalWorkflow")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalWrite), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalWrite")] [JsonSerializable(typeof(GitHub.Copilot.Verbosity), TypeInfoPropertyName = "SessionEventsVerbosity")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowPermissionOperation), TypeInfoPropertyName = "SessionEventsWorkflowPermissionOperation")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowPermissionPhase), TypeInfoPropertyName = "SessionEventsWorkflowPermissionPhase")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowRunSettledData), TypeInfoPropertyName = "SessionEventsWorkflowRunSettledData")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowRunSettledEvent), TypeInfoPropertyName = "SessionEventsWorkflowRunSettledEvent")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowRunSettledStatus), TypeInfoPropertyName = "SessionEventsWorkflowRunSettledStatus")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowRunStartedData), TypeInfoPropertyName = "SessionEventsWorkflowRunStartedData")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowRunStartedEvent), TypeInfoPropertyName = "SessionEventsWorkflowRunStartedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowRunUpdatedData), TypeInfoPropertyName = "SessionEventsWorkflowRunUpdatedData")] +[JsonSerializable(typeof(GitHub.Copilot.WorkflowRunUpdatedEvent), TypeInfoPropertyName = "SessionEventsWorkflowRunUpdatedEvent")] [JsonSerializable(typeof(GitHub.Copilot.WorkingDirectoryContext), TypeInfoPropertyName = "SessionEventsWorkingDirectoryContext")] [JsonSerializable(typeof(GitHub.Copilot.WorkingDirectoryContextHostType), TypeInfoPropertyName = "SessionEventsWorkingDirectoryContextHostType")] [JsonSerializable(typeof(GitHub.Copilot.WorkspaceFileChangedOperation), TypeInfoPropertyName = "SessionEventsWorkspaceFileChangedOperation")] @@ -44720,6 +51790,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AccountLogoutRequest))] [JsonSerializable(typeof(AccountLogoutResult))] [JsonSerializable(typeof(AccountQuotaSnapshot))] +[JsonSerializable(typeof(AccountStatus))] +[JsonSerializable(typeof(AccountsEnumerateRequest))] +[JsonSerializable(typeof(AccountsGetRequest))] +[JsonSerializable(typeof(AccountsSetRequest))] [JsonSerializable(typeof(AgentDiscoveryPath))] [JsonSerializable(typeof(AgentDiscoveryPathList))] [JsonSerializable(typeof(AgentGetCurrentResult))] @@ -44733,16 +51807,41 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AgentSelectRequest))] [JsonSerializable(typeof(AgentSelectResult))] [JsonSerializable(typeof(AgentSetPromptRequest))] +[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionParams))] +[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionResult))] [JsonSerializable(typeof(AgentsDiscoverRequest))] +[JsonSerializable(typeof(AgentsGetAvailableBuiltinsRequest))] +[JsonSerializable(typeof(AgentsGetAvailableBuiltinsResult))] +[JsonSerializable(typeof(AgentsGetBuiltinDefinitionRequest))] +[JsonSerializable(typeof(AgentsGetBuiltinDefinitionResult))] +[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionRequest))] +[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionResult))] +[JsonSerializable(typeof(AgentsGetBuiltinsResult))] [JsonSerializable(typeof(AgentsGetDiscoveryPathsRequest))] +[JsonSerializable(typeof(AuthEnumerateQuery))] +[JsonSerializable(typeof(AuthEnumerateValue))] [JsonSerializable(typeof(AuthIdentity))] +[JsonSerializable(typeof(AuthIdentityMetadata))] [JsonSerializable(typeof(AuthInfo))] +[JsonSerializable(typeof(AuthLoginAccount))] +[JsonSerializable(typeof(AuthLoginAdvanceRequest))] +[JsonSerializable(typeof(AuthLoginBeginRequest))] +[JsonSerializable(typeof(AuthLoginBegun))] +[JsonSerializable(typeof(AuthLoginCancelRequest))] +[JsonSerializable(typeof(AuthLoginResultDto))] +[JsonSerializable(typeof(AuthLoginStep))] +[JsonSerializable(typeof(AuthReadQuery))] +[JsonSerializable(typeof(AuthReadValue))] +[JsonSerializable(typeof(AuthStatusDto))] [JsonSerializable(typeof(AuthValidationError))] +[JsonSerializable(typeof(AuthWrite))] +[JsonSerializable(typeof(AuthWriteResult))] [JsonSerializable(typeof(AutopilotObjectiveCreditLimit))] [JsonSerializable(typeof(AutopilotObjectiveGetStateResult))] [JsonSerializable(typeof(AutopilotObjectiveState))] [JsonSerializable(typeof(BuiltInModelCatalog))] [JsonSerializable(typeof(BuiltInModelCatalogEntry))] +[JsonSerializable(typeof(BuiltinAgentSummary))] [JsonSerializable(typeof(BuiltinToolDescriptor))] [JsonSerializable(typeof(BuiltinToolFormat))] [JsonSerializable(typeof(BuiltinToolInputSchema))] @@ -44811,8 +51910,17 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ConnectorConnectResult))] [JsonSerializable(typeof(ConnectorContinueRequest))] [JsonSerializable(typeof(ConnectorDisconnectResult))] +[JsonSerializable(typeof(ConnectorDiscoveryAccount))] +[JsonSerializable(typeof(ConnectorDiscoveryAccountList))] +[JsonSerializable(typeof(ConnectorDiscoveryAccountRequest))] +[JsonSerializable(typeof(ConnectorDiscoveryAuthInfo))] +[JsonSerializable(typeof(ConnectorDiscoveryCapabilities))] +[JsonSerializable(typeof(ConnectorDiscoveryCatalogEntry))] +[JsonSerializable(typeof(ConnectorDiscoveryCatalogResult))] [JsonSerializable(typeof(ConnectorReconcileRequest))] +[JsonSerializable(typeof(ConnectorReconcileRequestWithSession))] [JsonSerializable(typeof(ConnectorRuntimeStatus))] +[JsonSerializable(typeof(ConnectorSessionAccount))] [JsonSerializable(typeof(ConnectorStatus))] [JsonSerializable(typeof(ContentExclusionCheckPathsRequest))] [JsonSerializable(typeof(ContentExclusionCheckPathsResult))] @@ -44828,6 +51936,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(CopilotUserResponseQuotaSnapshotsPremiumInteractions))] [JsonSerializable(typeof(CurrentModel))] [JsonSerializable(typeof(CurrentToolMetadata))] +[JsonSerializable(typeof(CustomizationReloadOutcome))] +[JsonSerializable(typeof(CustomizationsReloadResult))] [JsonSerializable(typeof(DebugCollectLogsCollectedEntry))] [JsonSerializable(typeof(DebugCollectLogsDestination))] [JsonSerializable(typeof(DebugCollectLogsEntry))] @@ -44849,8 +51959,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(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))] @@ -44867,55 +51986,34 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ExternalToolTextResultForLlmBinaryResultsForLlm))] [JsonSerializable(typeof(ExternalToolTextResultForLlmContent))] [JsonSerializable(typeof(ExternalToolTextResultForLlmContentResourceLinkIcon))] -[JsonSerializable(typeof(FactoryAbortRequest))] -[JsonSerializable(typeof(FactoryAckResult))] -[JsonSerializable(typeof(FactoryAgentOptions))] -[JsonSerializable(typeof(FactoryAgentRequest))] -[JsonSerializable(typeof(FactoryAgentResult))] -[JsonSerializable(typeof(FactoryAgentSummary))] -[JsonSerializable(typeof(FactoryCancelRequest))] -[JsonSerializable(typeof(FactoryCurrentPhase))] -[JsonSerializable(typeof(FactoryDeclaredLimits))] -[JsonSerializable(typeof(FactoryExecuteRequest))] -[JsonSerializable(typeof(FactoryExecuteResult))] -[JsonSerializable(typeof(FactoryGetRunProgressRequest))] -[JsonSerializable(typeof(FactoryGetRunRequest))] -[JsonSerializable(typeof(FactoryJournalGetRequest))] -[JsonSerializable(typeof(FactoryJournalGetResult))] -[JsonSerializable(typeof(FactoryJournalPutRequest))] -[JsonSerializable(typeof(FactoryListRunsRequest))] -[JsonSerializable(typeof(FactoryListRunsResult))] -[JsonSerializable(typeof(FactoryLogLine))] -[JsonSerializable(typeof(FactoryLogRequest))] -[JsonSerializable(typeof(FactoryPauseCheckpointRequest))] -[JsonSerializable(typeof(FactoryPauseInfo))] -[JsonSerializable(typeof(FactoryPauseRequest))] -[JsonSerializable(typeof(FactoryPhaseObservation))] -[JsonSerializable(typeof(FactoryProgressLine))] -[JsonSerializable(typeof(FactoryProgressPage))] -[JsonSerializable(typeof(FactoryResumeRequest))] -[JsonSerializable(typeof(FactoryResumeResult))] -[JsonSerializable(typeof(FactoryRunConsumed))] -[JsonSerializable(typeof(FactoryRunDetail))] -[JsonSerializable(typeof(FactoryRunFailure))] -[JsonSerializable(typeof(FactoryRunLimits))] -[JsonSerializable(typeof(FactoryRunRequest))] -[JsonSerializable(typeof(FactoryRunResult))] -[JsonSerializable(typeof(FactoryRunSummary))] -[JsonSerializable(typeof(FactoryRunTerminal))] -[JsonSerializable(typeof(FactoryToolResumeRequest))] -[JsonSerializable(typeof(FactoryToolRunOptions))] -[JsonSerializable(typeof(FactoryToolRunRequest))] [JsonSerializable(typeof(FleetStartRequest))] [JsonSerializable(typeof(FleetStartResult))] [JsonSerializable(typeof(FolderTrustAddParams))] [JsonSerializable(typeof(FolderTrustCheckParams))] [JsonSerializable(typeof(FolderTrustCheckResult))] +[JsonSerializable(typeof(GitCurrentBranchRemoteResult))] +[JsonSerializable(typeof(GitCwdRequest))] +[JsonSerializable(typeof(GitHubEnvironment))] +[JsonSerializable(typeof(GitHubOwnerOption))] +[JsonSerializable(typeof(GitHubOwnersCancelRequest))] +[JsonSerializable(typeof(GitHubOwnersCancelResult))] +[JsonSerializable(typeof(GitHubOwnersListRequest))] +[JsonSerializable(typeof(GitHubOwnersListResult))] +[JsonSerializable(typeof(GitHubOwnersRequestIdResult))] +[JsonSerializable(typeof(GitHubRepositoryAtPathRequest))] +[JsonSerializable(typeof(GitHubRepositoryAtPathResult))] +[JsonSerializable(typeof(GitHubRepositoryIdentity))] [JsonSerializable(typeof(GitHubTelemetryClientInfo))] [JsonSerializable(typeof(GitHubTelemetryEvent))] [JsonSerializable(typeof(GitHubTelemetryNotification))] [JsonSerializable(typeof(GitHubTokenAcquireRequest))] [JsonSerializable(typeof(GitHubTokenAcquireResult))] +[JsonSerializable(typeof(GitRemoteRepository))] +[JsonSerializable(typeof(GitReposFromRemotesRequest))] +[JsonSerializable(typeof(GitReposFromRemotesResult))] +[JsonSerializable(typeof(GlobalStateLoadForConfigDirRequest))] +[JsonSerializable(typeof(GlobalStateLoadResult))] +[JsonSerializable(typeof(GlobalStateWriteKeyRequest))] [JsonSerializable(typeof(HandlePendingToolCallRequest))] [JsonSerializable(typeof(HandlePendingToolCallResult))] [JsonSerializable(typeof(HistoryAbortManualCompactionResult))] @@ -44937,12 +52035,34 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(HistoryTruncateResult))] [JsonSerializable(typeof(HooksDiscoverRequest))] [JsonSerializable(typeof(HooksDiscoverResult))] +[JsonSerializable(typeof(HostCreateSessionRequest))] +[JsonSerializable(typeof(HostCreateSessionResult))] +[JsonSerializable(typeof(HostDisposeRequest))] +[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))] +[JsonSerializable(typeof(HostReadyResult))] +[JsonSerializable(typeof(HostReleaseSessionRequest))] +[JsonSerializable(typeof(HostReleaseSessionResult))] +[JsonSerializable(typeof(HostStartRequest))] +[JsonSerializable(typeof(HostStartResult))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] +[JsonSerializable(typeof(InstallationCatalogueIdentity))] +[JsonSerializable(typeof(InstallationReview))] +[JsonSerializable(typeof(InstallationsConfirmRequest))] +[JsonSerializable(typeof(InstallationsConfirmResult))] [JsonSerializable(typeof(InstalledPlugin))] [JsonSerializable(typeof(InstalledPluginInfo))] [JsonSerializable(typeof(InstructionDiscoveryPath))] @@ -44967,9 +52087,23 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(LocalSessionMetadataValue))] [JsonSerializable(typeof(LogRequest))] [JsonSerializable(typeof(LogResult))] +[JsonSerializable(typeof(LoggedInUser))] [JsonSerializable(typeof(LspInitializeRequest))] +[JsonSerializable(typeof(ManagedSettingMeta))] +[JsonSerializable(typeof(ManagedSettingsComposeLayer))] +[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))] @@ -44979,6 +52113,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(MarketplaceRefreshResult))] [JsonSerializable(typeof(MarketplaceRemoveResult))] [JsonSerializable(typeof(McpAllowedServer))] +[JsonSerializable(typeof(McpApplyInstallRequest))] +[JsonSerializable(typeof(McpApplyUninstallRequest))] [JsonSerializable(typeof(McpAppsCallToolRequest))] [JsonSerializable(typeof(McpAppsDiagnoseCapability))] [JsonSerializable(typeof(McpAppsDiagnoseRequest))] @@ -45003,12 +52139,17 @@ 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))] +[JsonSerializable(typeof(McpDisableRequestWithSession))] [JsonSerializable(typeof(McpDiscoverRequest))] [JsonSerializable(typeof(McpDiscoverResult))] [JsonSerializable(typeof(McpEnableRequest))] +[JsonSerializable(typeof(McpEnableRequestWithSession))] [JsonSerializable(typeof(McpExecuteSamplingParams))] [JsonSerializable(typeof(McpExecuteSamplingRequest))] [JsonSerializable(typeof(McpExecuteSamplingResult))] @@ -45019,17 +52160,32 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpHeadersHandlePendingHeadersRefreshRequestResult))] [JsonSerializable(typeof(McpHostState))] [JsonSerializable(typeof(McpInstallPlan))] +[JsonSerializable(typeof(McpInstallationInput))] +[JsonSerializable(typeof(McpInstallationManagementOutcome))] +[JsonSerializable(typeof(McpInstallationManagementResult))] +[JsonSerializable(typeof(McpInstallationOperationRequest))] +[JsonSerializable(typeof(McpInstallationOperationStatus))] +[JsonSerializable(typeof(McpInstallationOutcome))] +[JsonSerializable(typeof(McpInstallationRemoteConfiguration))] +[JsonSerializable(typeof(McpInstallationResult))] +[JsonSerializable(typeof(McpInstallationReview))] +[JsonSerializable(typeof(McpInstallationSecret))] +[JsonSerializable(typeof(McpInstallationSummary))] +[JsonSerializable(typeof(McpInstallationsRequest))] [JsonSerializable(typeof(McpIsServerRunningRequest))] [JsonSerializable(typeof(McpIsServerRunningResult))] [JsonSerializable(typeof(McpListToolsRequest))] [JsonSerializable(typeof(McpListToolsResult))] [JsonSerializable(typeof(McpOauthAuthenticationStateChangedRequest))] +[JsonSerializable(typeof(McpOauthCompleteRequest))] [JsonSerializable(typeof(McpOauthHandlePendingRequest))] [JsonSerializable(typeof(McpOauthHandlePendingResult))] [JsonSerializable(typeof(McpOauthLoginRequest))] +[JsonSerializable(typeof(McpOauthLoginRequestWithSession))] [JsonSerializable(typeof(McpOauthLoginResult))] [JsonSerializable(typeof(McpOauthPendingRequestResponse))] [JsonSerializable(typeof(McpOauthProbeRequest))] +[JsonSerializable(typeof(McpOauthProbeRequestWithSession))] [JsonSerializable(typeof(McpOauthProbeResult))] [JsonSerializable(typeof(McpOauthRespondRequest))] [JsonSerializable(typeof(McpOauthRespondResult))] @@ -45044,6 +52200,17 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpPlanSecretPlaceholder))] [JsonSerializable(typeof(McpPlanTarget))] [JsonSerializable(typeof(McpPlanTransportChoice))] +[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))] @@ -45059,23 +52226,28 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpResourcesReadRequest))] [JsonSerializable(typeof(McpResourcesReadResult))] [JsonSerializable(typeof(McpRestartServerRequest))] +[JsonSerializable(typeof(McpRestartServerRequestWithSession))] [JsonSerializable(typeof(McpSamplingExecutionResult))] [JsonSerializable(typeof(McpServer))] [JsonSerializable(typeof(McpServerCardReference))] [JsonSerializable(typeof(McpServerFailureInfo))] [JsonSerializable(typeof(McpServerList))] [JsonSerializable(typeof(McpServerNeedsAuthInfo))] +[JsonSerializable(typeof(McpServerOwnership))] [JsonSerializable(typeof(McpSetEnvValueModeParams))] [JsonSerializable(typeof(McpSetEnvValueModeResult))] [JsonSerializable(typeof(McpSourceFile))] [JsonSerializable(typeof(McpSourcePlugin))] [JsonSerializable(typeof(McpSourceRef))] [JsonSerializable(typeof(McpStartServerRequest))] +[JsonSerializable(typeof(McpStartServerRequestWithSession))] [JsonSerializable(typeof(McpStartServersResult))] [JsonSerializable(typeof(McpStopServerRequest))] +[JsonSerializable(typeof(McpStopServerRequestWithSession))] [JsonSerializable(typeof(McpTaskMetadata))] [JsonSerializable(typeof(McpToolUi))] [JsonSerializable(typeof(McpTools))] +[JsonSerializable(typeof(McpUninstallPlan))] [JsonSerializable(typeof(McpUnregisterExternalClientRequest))] [JsonSerializable(typeof(MetadataContextAttributionResult))] [JsonSerializable(typeof(MetadataContextAttributionResultContextAttribution))] @@ -45101,6 +52273,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModeSetResult))] [JsonSerializable(typeof(Model))] [JsonSerializable(typeof(ModelApplyStartupOverlayRequest))] +[JsonSerializable(typeof(ModelArtifactDetails))] [JsonSerializable(typeof(ModelBilling))] [JsonSerializable(typeof(ModelBillingPromo))] [JsonSerializable(typeof(ModelBillingTokenPrices))] @@ -45119,6 +52292,25 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModelPickerSettingsContext))] [JsonSerializable(typeof(ModelPickerSettingsContextEnvironment))] [JsonSerializable(typeof(ModelPolicy))] +[JsonSerializable(typeof(ModelProviderAdapterCatalog))] +[JsonSerializable(typeof(ModelProviderAdapterDescriptor))] +[JsonSerializable(typeof(ModelProviderAdapterOperationDescriptor))] +[JsonSerializable(typeof(ModelProviderAttribution))] +[JsonSerializable(typeof(ModelProviderAutomaticDiscoveryPolicy))] +[JsonSerializable(typeof(ModelProviderConfigurationPlan))] +[JsonSerializable(typeof(ModelProviderDescriptor))] +[JsonSerializable(typeof(ModelProviderDiscoverRequest))] +[JsonSerializable(typeof(ModelProviderDiscoverResult))] +[JsonSerializable(typeof(ModelProviderGetStatusRequest))] +[JsonSerializable(typeof(ModelProviderInstance))] +[JsonSerializable(typeof(ModelProviderInstanceReference))] +[JsonSerializable(typeof(ModelProviderModelsListRequest))] +[JsonSerializable(typeof(ModelProviderOperationOutcome))] +[JsonSerializable(typeof(ModelProviderPrepareConfigurationRequest))] +[JsonSerializable(typeof(ModelProviderProvenance))] +[JsonSerializable(typeof(ModelProviderRef))] +[JsonSerializable(typeof(ModelProviderStatus))] +[JsonSerializable(typeof(ModelProviderWarning))] [JsonSerializable(typeof(ModelSetAllowedModelsRequest))] [JsonSerializable(typeof(ModelSetAllowedModelsResult))] [JsonSerializable(typeof(ModelSetReasoningEffortRequest))] @@ -45233,6 +52425,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ProviderAddResult))] [JsonSerializable(typeof(ProviderConfig))] [JsonSerializable(typeof(ProviderConfigAzure))] +[JsonSerializable(typeof(ProviderDescriptor))] [JsonSerializable(typeof(ProviderEndpoint))] [JsonSerializable(typeof(ProviderModelConfig))] [JsonSerializable(typeof(ProviderSessionToken))] @@ -45240,6 +52433,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))] @@ -45295,7 +52490,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(RemoteSessionMetadataRepository))] [JsonSerializable(typeof(RemoteSessionMetadataValue))] [JsonSerializable(typeof(ResponseFormat))] -[JsonSerializable(typeof(RunOptions))] [JsonSerializable(typeof(SandboxConfig))] [JsonSerializable(typeof(SandboxConfigAuth))] [JsonSerializable(typeof(SandboxConfigUserPolicy))] @@ -45305,11 +52499,18 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SandboxConfigUserPolicyNetwork))] [JsonSerializable(typeof(SandboxConfigUserPolicyNetworkProxy))] [JsonSerializable(typeof(SandboxConfigUserPolicySeatbelt))] +[JsonSerializable(typeof(SandboxCredentialsConfig))] [JsonSerializable(typeof(SandboxDisableForSessionRequest))] [JsonSerializable(typeof(SandboxDisableForSessionResult))] [JsonSerializable(typeof(SandboxEnforcementStatus))] +[JsonSerializable(typeof(SandboxGrantPathForRequestRequest))] +[JsonSerializable(typeof(SandboxGrantPathForRequestResult))] [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))] @@ -45353,6 +52554,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionCommandsListRequestWithSession))] [JsonSerializable(typeof(SessionCompletionItem))] [JsonSerializable(typeof(SessionCompletionsGetTriggerCharactersRequest))] +[JsonSerializable(typeof(SessionConnectedIdeInfo))] +[JsonSerializable(typeof(SessionConnectorsGetAccountRequest))] [JsonSerializable(typeof(SessionConnectorsGetCapabilitiesRequest))] [JsonSerializable(typeof(SessionConnectorsGetStatusRequest))] [JsonSerializable(typeof(SessionConnectorsWithdrawProjectionRequest))] @@ -45362,12 +52565,13 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionEventLogTailRequest))] [JsonSerializable(typeof(SessionExtensionsListRequest))] [JsonSerializable(typeof(SessionExtensionsReloadRequest))] -[JsonSerializable(typeof(SessionFactoryPauseAtCheckpointResult))] [JsonSerializable(typeof(SessionFsAppendFileRequest))] [JsonSerializable(typeof(SessionFsError))] [JsonSerializable(typeof(SessionFsExistsRequest))] [JsonSerializable(typeof(SessionFsExistsResult))] [JsonSerializable(typeof(SessionFsMkdirRequest))] +[JsonSerializable(typeof(SessionFsReadFileBytesRequest))] +[JsonSerializable(typeof(SessionFsReadFileBytesResult))] [JsonSerializable(typeof(SessionFsReadFileRequest))] [JsonSerializable(typeof(SessionFsReadFileResult))] [JsonSerializable(typeof(SessionFsReaddirRequest))] @@ -45390,6 +52594,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionFsSqliteTransactionStatement))] [JsonSerializable(typeof(SessionFsStatRequest))] [JsonSerializable(typeof(SessionFsStatResult))] +[JsonSerializable(typeof(SessionFsWriteFileBytesRequest))] [JsonSerializable(typeof(SessionFsWriteFileRequest))] [JsonSerializable(typeof(SessionGitHubAuthGetAllAuthAvailableRequest))] [JsonSerializable(typeof(SessionGitHubAuthGetCurrentAuthInfoRequest))] @@ -45418,10 +52623,16 @@ 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))] +[JsonSerializable(typeof(SessionMcpOauthCancelLoginResult))] +[JsonSerializable(typeof(SessionMcpOauthPrepareLoginRequest))] +[JsonSerializable(typeof(SessionMcpOauthPrepareLoginResult))] [JsonSerializable(typeof(SessionMcpReloadRequest))] [JsonSerializable(typeof(SessionMcpRemoveGitHubRequest))] +[JsonSerializable(typeof(SessionMcpSetConnectedIdeInfoParams))] [JsonSerializable(typeof(SessionMetadataActivityRequest))] [JsonSerializable(typeof(SessionMetadataGetClientMetadataRequest))] [JsonSerializable(typeof(SessionMetadataGetContextAttributionRequest))] @@ -45452,6 +52663,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionPluginsReloadRequestWithSession))] [JsonSerializable(typeof(SessionProviderGetEndpointRequest))] [JsonSerializable(typeof(SessionProviderGetEndpointRequestWithSession))] +[JsonSerializable(typeof(SessionProvidersGetCatalogRequest))] [JsonSerializable(typeof(SessionPruneResult))] [JsonSerializable(typeof(SessionQueueClearRequest))] [JsonSerializable(typeof(SessionQueueEnqueueResumePendingRequest))] @@ -45499,6 +52711,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionVisibilityGetRequest))] [JsonSerializable(typeof(SessionWorkflowPauseAtCheckpointResult))] [JsonSerializable(typeof(SessionWorkingDirectoryContext))] +[JsonSerializable(typeof(SessionWorkingDirectoryContextWithClient))] [JsonSerializable(typeof(SessionWorkspacesAutopilotObjectiveExistsRequest))] [JsonSerializable(typeof(SessionWorkspacesDeleteAutopilotObjectiveRequest))] [JsonSerializable(typeof(SessionWorkspacesGetWorkspaceRequest))] @@ -45511,6 +52724,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsClientMetadataEntry))] [JsonSerializable(typeof(SessionsCloseRequest))] [JsonSerializable(typeof(SessionsCloseResult))] +[JsonSerializable(typeof(SessionsCreateWorkspaceRequest))] +[JsonSerializable(typeof(SessionsCreateWorkspaceResult))] [JsonSerializable(typeof(SessionsDeleteRequest))] [JsonSerializable(typeof(SessionsEnrichMetadataRequest))] [JsonSerializable(typeof(SessionsFindByPrefixRequest))] @@ -45534,6 +52749,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsListNonEmptySessionIdsResult))] [JsonSerializable(typeof(SessionsListRequest))] [JsonSerializable(typeof(SessionsLoadDeferredRepoHooksRequest))] +[JsonSerializable(typeof(SessionsLoadWorkspaceRequest))] +[JsonSerializable(typeof(SessionsLoadWorkspaceResult))] [JsonSerializable(typeof(SessionsOpenProgress))] [JsonSerializable(typeof(SessionsPruneOldRequest))] [JsonSerializable(typeof(SessionsReadPersistedEventsRequest))] @@ -45549,6 +52766,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsStartRemoteControlRequest))] [JsonSerializable(typeof(SessionsStopRemoteControlRequest))] [JsonSerializable(typeof(SessionsTransferRemoteControlRequest))] +[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsRequest))] +[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsResult))] [JsonSerializable(typeof(SettableAuthInfo))] [JsonSerializable(typeof(ShellCancelUserRequestedRequest))] [JsonSerializable(typeof(ShellCredentials))] @@ -45561,9 +52780,28 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ShellOptions))] [JsonSerializable(typeof(ShutdownRequest))] [JsonSerializable(typeof(Skill))] +[JsonSerializable(typeof(SkillApplyInstallRequest))] +[JsonSerializable(typeof(SkillApplyUninstallRequest))] [JsonSerializable(typeof(SkillDiscoveryPath))] [JsonSerializable(typeof(SkillDiscoveryPathList))] +[JsonSerializable(typeof(SkillInstallPlan))] +[JsonSerializable(typeof(SkillInstallationFileReview))] +[JsonSerializable(typeof(SkillInstallationLocation))] +[JsonSerializable(typeof(SkillInstallationManagementOutcome))] +[JsonSerializable(typeof(SkillInstallationManagementResult))] +[JsonSerializable(typeof(SkillInstallationOperationRequest))] +[JsonSerializable(typeof(SkillInstallationOperationStatus))] +[JsonSerializable(typeof(SkillInstallationOutcome))] +[JsonSerializable(typeof(SkillInstallationResult))] +[JsonSerializable(typeof(SkillInstallationReview))] +[JsonSerializable(typeof(SkillInstallationSource))] +[JsonSerializable(typeof(SkillInstallationSummary))] +[JsonSerializable(typeof(SkillInstallationsRequest))] [JsonSerializable(typeof(SkillList))] +[JsonSerializable(typeof(SkillPlanInstallRequest))] +[JsonSerializable(typeof(SkillPlanUninstallRequest))] +[JsonSerializable(typeof(SkillSetEnabledRequest))] +[JsonSerializable(typeof(SkillUninstallPlan))] [JsonSerializable(typeof(SkillsConfigSetDisabledSkillsRequest))] [JsonSerializable(typeof(SkillsConfigSetSkillDisabledRequest))] [JsonSerializable(typeof(SkillsDisableRequest))] @@ -45629,9 +52867,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ToolsTaskCompleteEventDataRequest))] [JsonSerializable(typeof(ToolsUpdateSubagentSettingsResult))] [JsonSerializable(typeof(UIElicitationRequest))] -[JsonSerializable(typeof(UIElicitationRequestMeta))] [JsonSerializable(typeof(UIElicitationResponse))] -[JsonSerializable(typeof(UIElicitationResponseMeta))] [JsonSerializable(typeof(UIElicitationResult))] [JsonSerializable(typeof(UIElicitationSchema))] [JsonSerializable(typeof(UIEphemeralQueryRequest))] @@ -45664,7 +52900,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(UserSettingMetadata))] [JsonSerializable(typeof(UserSettingsGetResult))] [JsonSerializable(typeof(UserSettingsSetRequest))] -[JsonSerializable(typeof(UserSettingsSetResult))] [JsonSerializable(typeof(VisibilityGetResult))] [JsonSerializable(typeof(VisibilitySetRequest))] [JsonSerializable(typeof(VisibilitySetResult))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 5c3618c35d..48e214df95 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -14,6 +14,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace GitHub.Copilot; @@ -57,12 +58,10 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(ExitPlanModeRequestedEvent), "exit_plan_mode.requested")] [JsonDerivedType(typeof(ExternalToolCompletedEvent), "external_tool.completed")] [JsonDerivedType(typeof(ExternalToolRequestedEvent), "external_tool.requested")] -[JsonDerivedType(typeof(FactoryRunSettledEvent), "factory.run_settled")] -[JsonDerivedType(typeof(FactoryRunStartedEvent), "factory.run_started")] -[JsonDerivedType(typeof(FactoryRunUpdatedEvent), "factory.run_updated")] [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")] @@ -72,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")] @@ -168,11 +168,15 @@ 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")] [JsonDerivedType(typeof(UserInputRequestedEvent), "user_input.requested")] [JsonDerivedType(typeof(UserMessageEvent), "user.message")] +[JsonDerivedType(typeof(WorkflowRunSettledEvent), "workflow.run_settled")] +[JsonDerivedType(typeof(WorkflowRunStartedEvent), "workflow.run_started")] +[JsonDerivedType(typeof(WorkflowRunUpdatedEvent), "workflow.run_updated")] public partial class SessionEvent { /// Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. @@ -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 @@ -1426,7 +1975,7 @@ public sealed partial class PermissionCompletedEvent : SessionEvent public required PermissionCompletedData Data { get; set; } } -/// Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. +/// Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions. /// Represents the permission.carriedForward event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionCarriedForwardEvent : SessionEvent @@ -1440,7 +1989,7 @@ public sealed partial class PermissionCarriedForwardEvent : SessionEvent public required PermissionCarriedForwardData Data { get; set; } } -/// Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. +/// Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it. /// Represents the permission.messageAuthorization event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionMessageAuthorizationEvent : SessionEvent @@ -1454,7 +2003,7 @@ public sealed partial class PermissionMessageAuthorizationEvent : SessionEvent public required PermissionMessageAuthorizationData Data { get; set; } } -/// Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction. +/// Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it. /// Represents the permission.messageAuthorizationRead event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionMessageAuthorizationReadEvent : SessionEvent @@ -1468,7 +2017,7 @@ public sealed partial class PermissionMessageAuthorizationReadEvent : SessionEve public required PermissionMessageAuthorizationReadData Data { get; set; } } -/// Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. +/// Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions. /// Represents the permission.messageAuthorizationDegraded event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionMessageAuthorizationDegradedEvent : SessionEvent @@ -1482,7 +2031,7 @@ public sealed partial class PermissionMessageAuthorizationDegradedEvent : Sessio public required PermissionMessageAuthorizationDegradedData Data { get; set; } } -/// Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation. +/// Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow. /// Represents the permission.assentDetected event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionAssentDetectedEvent : SessionEvent @@ -1496,7 +2045,7 @@ public sealed partial class PermissionAssentDetectedEvent : SessionEvent public required PermissionAssentDetectedData Data { get; set; } } -/// Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify. +/// Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it. /// Represents the permission.contextualAuthorization event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionContextualAuthorizationEvent : 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 @@ -1904,46 +2466,46 @@ public sealed partial class SessionBackgroundTasksChangedEvent : SessionEvent public required SessionBackgroundTasksChangedData Data { get; set; } } -/// Ephemeral invalidation signal for a changed factory run. -/// Represents the factory.run_updated event. +/// Ephemeral invalidation signal for a changed workflow run. +/// Represents the workflow.run_updated event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed partial class FactoryRunUpdatedEvent : SessionEvent +public sealed partial class WorkflowRunUpdatedEvent : SessionEvent { /// [JsonIgnore] - public override string Type => "factory.run_updated"; + public override string Type => "workflow.run_updated"; - /// The factory.run_updated event payload. + /// The workflow.run_updated event payload. [JsonPropertyName("data")] - public required FactoryRunUpdatedData Data { get; set; } + public required WorkflowRunUpdatedData Data { get; set; } } -/// Ephemeral signal that a factory run attempt began executing. -/// Represents the factory.run_started event. +/// Ephemeral signal that a workflow run attempt began executing. +/// Represents the workflow.run_started event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed partial class FactoryRunStartedEvent : SessionEvent +public sealed partial class WorkflowRunStartedEvent : SessionEvent { /// [JsonIgnore] - public override string Type => "factory.run_started"; + public override string Type => "workflow.run_started"; - /// The factory.run_started event payload. + /// The workflow.run_started event payload. [JsonPropertyName("data")] - public required FactoryRunStartedData Data { get; set; } + public required WorkflowRunStartedData Data { get; set; } } -/// Ephemeral signal that a factory run reached a terminal status. -/// Represents the factory.run_settled event. +/// Ephemeral signal that a workflow run reached a terminal status. +/// Represents the workflow.run_settled event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed partial class FactoryRunSettledEvent : SessionEvent +public sealed partial class WorkflowRunSettledEvent : SessionEvent { /// [JsonIgnore] - public override string Type => "factory.run_settled"; + public override string Type => "workflow.run_settled"; - /// The factory.run_settled event payload. + /// The workflow.run_settled event payload. [JsonPropertyName("data")] - public required FactoryRunSettledData Data { get; set; } + public required WorkflowRunSettledData Data { get; set; } } /// Payload of `session.skills_loaded` listing resolved skill metadata. @@ -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")] @@ -3105,12 +3682,18 @@ public sealed partial class SessionCompactionStartData /// Conversation compaction results including success status, metrics, and optional error details. public sealed partial class SessionCompactionCompleteData { - /// Authoritative active-factory reminder appended to the compacted context. + /// Legacy active-workflow reminder retained for replay compatibility. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonInclude] [JsonPropertyName("activeFactorySummary")] internal string? ActiveFactorySummary { get; set; } + /// Authoritative active-workflow reminder appended to the compacted context. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("activeWorkflowSummary")] + internal string? ActiveWorkflowSummary { get; set; } + /// Canonical model identifier used for model-specific behavior when replaying compaction. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("behaviorModelId")] @@ -3359,6 +3942,12 @@ public sealed partial class SessionFusionResolvedData [JsonPropertyName("contractVersion")] public required long ContractVersion { get; set; } + /// 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. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("critics")] + public FusionCritic[]? Critics { get; set; } + /// Concrete model used when the planned primary model cannot execute. [JsonPropertyName("fallbackModel")] public required string FallbackModel { get; set; } @@ -3382,6 +3971,12 @@ public sealed partial class SessionFusionResolvedData [JsonPropertyName("hint")] public string? Hint { get; set; } + /// Concrete model selected for Cascade escalation-gate calls, when required. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("judgeModel")] + public string? JudgeModel { get; set; } + /// Version of the executable model universe used for selection. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("modelUniverseVersion")] @@ -3415,6 +4010,12 @@ public sealed partial class SessionFusionResolvedData [JsonPropertyName("primaryModel")] public required string PrimaryModel { get; set; } + /// Concrete model selected for Cascade repair, when required. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("repairModel")] + public string? RepairModel { get; set; } + /// Router implementation that supplied the plan. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("routeSource")] @@ -3445,7 +4046,7 @@ public sealed partial class SessionFusionResolvedData [JsonPropertyName("scores")] public FusionScores? Scores { get; set; } - /// Concrete model selected for the review or judge phase, when required. + /// Concrete model selected for Critique review, or the legacy Cascade judge/repair model when role-specific fields are absent. [JsonPropertyName("secondaryModel")] public string? SecondaryModel { get; set; } @@ -3655,6 +4256,11 @@ public sealed partial class AssistantTurnStartData [JsonPropertyName("model")] public string? Model { get; set; } + /// Parent task tool call ID when this turn belongs to a sub-agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("parentToolCallId")] + public string? ParentToolCallId { get; set; } + /// Identifier for this turn within the agentic loop, typically a stringified turn number. [JsonPropertyName("turnId")] public required string TurnId { get; set; } @@ -3782,6 +4388,12 @@ public sealed partial class AssistantFusionPhaseStartedData [JsonPropertyName("phaseKind")] public required FusionPhaseKind PhaseKind { get; set; } + /// Explicit reasoning effort selected for this phase, if supplied. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + /// Semantic role assigned to the phase. [JsonPropertyName("role")] public required string Role { get; set; } @@ -3874,6 +4486,12 @@ public sealed partial class AssistantFusionPhaseCompletedData [JsonPropertyName("projectionMode")] internal FusionProjectionMode? ProjectionMode { get; set; } + /// Explicit reasoning effort selected for this phase, if supplied. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + /// Semantic role assigned to the completed phase. [JsonPropertyName("role")] public required string Role { get; set; } @@ -3939,6 +4557,12 @@ public sealed partial class AssistantFusionPhaseFailedData [JsonPropertyName("reason")] public required string Reason { get; set; } + /// Explicit reasoning effort selected for this phase, if supplied. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + /// Semantic role assigned to the failed phase. [JsonPropertyName("role")] public required string Role { get; set; } @@ -4099,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")] @@ -4188,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")] @@ -4202,6 +4830,11 @@ public sealed partial class AssistantTurnEndData [JsonPropertyName("model")] public string? Model { get; set; } + /// Parent task tool call ID when this turn belongs to a sub-agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("parentToolCallId")] + public string? ParentToolCallId { get; set; } + /// Identifier of the turn that has ended, matching the corresponding assistant.turn_start event. [JsonPropertyName("turnId")] public required string TurnId { get; set; } @@ -4240,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] @@ -4351,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] @@ -4372,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")] @@ -4559,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] @@ -4602,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)] @@ -4669,6 +5325,16 @@ 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")] + public string? ParentToolCallId { get; set; } + /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("providerCallId")] @@ -4715,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 { @@ -4760,6 +5443,11 @@ public sealed partial class ModelCallStartData [JsonPropertyName("model")] public string? Model { get; set; } + /// Parent task tool call ID when this model call belongs to a sub-agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("parentToolCallId")] + public string? ParentToolCallId { get; set; } + /// Previous response or interaction identifier included in the model request, when present. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonInclude] @@ -4849,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")] @@ -4888,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; } @@ -4900,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 { @@ -4907,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; } @@ -4920,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)] @@ -4951,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")] @@ -5192,6 +5923,7 @@ public sealed partial class SkillContextDeliveredRefData [JsonDerivedType(typeof(SandboxDecisionDataSpawnCompleted), "spawn_completed")] [JsonDerivedType(typeof(SandboxDecisionDataEnforcementState), "enforcement_state")] [JsonDerivedType(typeof(SandboxDecisionDataAccessDenied), "access_denied")] +[JsonDerivedType(typeof(SandboxDecisionDataAccessRecorded), "access_recorded")] [JsonDerivedType(typeof(SandboxDecisionDataBypassDecided), "bypass_decided")] [JsonDerivedType(typeof(SandboxDecisionDataPermissiveRetryDecided), "permissive_retry_decided")] [JsonDerivedType(typeof(SandboxDecisionDataPermissiveRetryCompleted), "permissive_retry_completed")] @@ -5228,7 +5960,7 @@ public sealed partial class SubagentStartedData [JsonPropertyName("executionMode")] public string? ExecutionMode { get; set; } - /// Root id of the factory run that spawned this sub-agent, when it was spawned by one. + /// Legacy root id of the workflow run that spawned this sub-agent. New consumers should use workflowRunId. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("factoryRunId")] public string? FactoryRunId { get; set; } @@ -5253,7 +5985,7 @@ public sealed partial class SubagentStartedData [JsonPropertyName("resumable")] public bool? Resumable { get; set; } - /// Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and factory agents); absent for sub-agents created through other runtime paths. + /// Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and workflow agents); absent for sub-agents created through other runtime paths. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("taskModelSource")] public SubagentTaskModelSource? TaskModelSource { get; set; } @@ -5261,6 +5993,11 @@ public sealed partial class SubagentStartedData /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + + /// Root id of the workflow run that spawned this sub-agent, when it was spawned by one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("workflowRunId")] + public string? WorkflowRunId { get; set; } } /// Resolved runtime configuration for a configured sub-agent. @@ -5684,7 +6421,7 @@ public sealed partial class PermissionCompletedData public string? ToolCallId { get; set; } } -/// Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. +/// Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionCarriedForwardData { @@ -5709,7 +6446,7 @@ public sealed partial class PermissionCarriedForwardData public required string ToolCallId { get; set; } } -/// Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. +/// Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionMessageAuthorizationData { @@ -5762,7 +6499,7 @@ public sealed partial class PermissionMessageAuthorizationData public JsonElement? World { get; set; } } -/// Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction. +/// Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionMessageAuthorizationReadData { @@ -5778,7 +6515,7 @@ public sealed partial class PermissionMessageAuthorizationReadData public required long TurnIndex { get; set; } } -/// Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. +/// Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionMessageAuthorizationDegradedData { @@ -5788,7 +6525,7 @@ public sealed partial class PermissionMessageAuthorizationDegradedData public required long TurnIndex { get; set; } } -/// Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation. +/// Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionAssentDetectedData { @@ -5803,7 +6540,7 @@ public sealed partial class PermissionAssentDetectedData public required long TurnIndex { get; set; } } -/// Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify. +/// Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class PermissionContextualAuthorizationData { @@ -6328,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 { @@ -6488,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 { @@ -6501,39 +7264,39 @@ public sealed partial class SessionBackgroundTasksChangedData { } -/// Ephemeral invalidation signal for a changed factory run. +/// Ephemeral invalidation signal for a changed workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed partial class FactoryRunUpdatedData +public sealed partial class WorkflowRunUpdatedData { /// Monotonic revision now available for the run. [JsonPropertyName("revision")] public required long Revision { get; set; } - /// Factory run identifier. + /// Workflow run identifier. [JsonPropertyName("runId")] public required string RunId { get; set; } } -/// Ephemeral signal that a factory run attempt began executing. +/// Ephemeral signal that a workflow run attempt began executing. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed partial class FactoryRunStartedData +public sealed partial class WorkflowRunStartedData { /// Attempt number this start committed; a resumed run increments it. [JsonPropertyName("attempt")] public required long Attempt { get; set; } - /// Name of the factory this run executes. Low cardinality by construction. - [JsonPropertyName("factoryName")] - public required string FactoryName { get; set; } - - /// Identifier of the factory run that started. + /// Identifier of the workflow run that started. [JsonPropertyName("runId")] public required string RunId { get; set; } + + /// Name of the workflow this run executes. Low cardinality by construction. + [JsonPropertyName("workflowName")] + public required string WorkflowName { get; set; } } -/// Ephemeral signal that a factory run reached a terminal status. +/// Ephemeral signal that a workflow run reached a terminal status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed partial class FactoryRunSettledData +public sealed partial class WorkflowRunSettledData { /// AI credits this run consumed, in nano-AIU. [JsonPropertyName("consumedNanoAiu")] @@ -6547,18 +7310,18 @@ public sealed partial class FactoryRunSettledData [JsonPropertyName("elapsedMs")] public required long ElapsedMs { get; set; } - /// Typed failure class recorded on the run, when it failed with one (e.g. `factory_limit_reached`). + /// Typed failure class recorded on the run, when it failed with one (e.g. `workflow_limit_reached`). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("failureType")] public string? FailureType { get; set; } - /// Identifier of the factory run that settled. + /// Identifier of the workflow run that settled. [JsonPropertyName("runId")] public required string RunId { get; set; } /// Terminal status the run committed. [JsonPropertyName("status")] - public required FactoryRunSettledStatus Status { get; set; } + public required WorkflowRunSettledStatus Status { get; set; } } /// Payload of `session.skills_loaded` listing resolved skill metadata. @@ -6596,11 +7359,21 @@ 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 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; } + /// Error message if the server entered a failed state. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("error")] public string? Error { get; set; } + /// Runtime-produced classification for the final failed connection; unclassified means no classification was supplied. Additional string values may be introduced. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorClassification")] + public string? ErrorClassification { get; set; } + /// Name of the MCP server whose status changed. [JsonPropertyName("serverName")] public required string ServerName { get; set; } @@ -7428,6 +8201,27 @@ public sealed partial class CompletionReceiptFinalTool public string? ToolName { get; set; } } +/// Nested data type for FusionCritic. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class FusionCritic +{ + /// Concrete model selected for this critic. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonPropertyName("model")] + public required string Model { get; set; } + + /// Unique execution phase identifier for this critic. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Explicit reasoning effort selected for this critic, if supplied. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } +} + /// Durable server recommendation for subsequent HydraFusion turns. /// Nested data type for FusionFollowUpRecommendation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -8322,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; } @@ -8644,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 @@ -8884,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 { @@ -9682,55 +10497,103 @@ public sealed partial class SandboxDecisionDataAccessDenied : SandboxDecisionDat public string? ToolCallId { get; set; } } -/// A request to run outside the process sandbox was resolved. This is what makes an `inactive` `enforcement_state` readable: without it, a command that ran unsandboxed because a person approved a bypass looks identical to one that ran unsandboxed because the session never had a sandbox. Reported only when a sandbox was in force, since bypassing a disabled sandbox bypasses nothing. Carries neither backend nor attestation: the verdict comes from the runtime's own permission flow or the local escalation prompt, not from a containment backend and not from the built-in policy check, so `source` is what records where it came from. -/// The bypass_decided variant of . -public sealed partial class SandboxDecisionDataBypassDecided : SandboxDecisionData +/// Permissive learning mode (record and allow) recorded an access that the enforced policy would have refused, and allowed it. Emitted once per distinct recorded access of a record-and-allow run, bounded per command. The only per-access record of such a run: nothing was refused, so no `access_denied` is raised for it. +/// The access_recorded variant of . +public sealed partial class SandboxDecisionDataAccessRecorded : SandboxDecisionData { /// [JsonIgnore] - public override string Kind => "bypass_decided"; + public override string Kind => "access_recorded"; - /// Command the verdict governs. Populated only when content capture is enabled. + /// Command that made the access. Populated only when content capture is enabled. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("command")] public string? Command { get; set; } - /// How strong the evidence behind `denialClass` was. Present exactly when `denialClass` is, so a verdict that relaxed the sandbox on a guess is distinguishable from one that relaxed it on a recorded refusal. Named to match `access_denied`, which reports the same pair. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("confidence")] - public SandboxDenialConfidence? Confidence { get; set; } - - /// Always `bypass`. + /// Sandbox control the access belongs to. Follows from `denialClass`. [JsonPropertyName("control")] public required SandboxControl Control { get; set; } - /// Bounded class of the access whose refusal raised this escalation. Omitted for a pre-execution bypass, such as a detached command that cannot be sandboxed and therefore resolves no denial. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Bounded class of the access the enforced policy would have refused. [JsonPropertyName("denialClass")] - public SandboxDenialClass? DenialClass { get; set; } + public required SandboxDenialClass DenialClass { get; set; } - /// Resource whose refusal raised this escalation. Populated only when content capture is enabled. + /// Resource the enforced policy would have refused. Populated only when content capture is enabled. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("deniedResource")] public string? DeniedResource { get; set; } - /// Runtime subsystem the bypass applies to. + /// Runtime subsystem that ran the command. [JsonPropertyName("enforcementPoint")] public required SandboxEnforcementPoint EnforcementPoint { get; set; } - /// Whether the bypass was granted: `approved` or `declined`. `declined` also covers the cases where nobody answered, since the sandboxed denial stands either way. + /// Always `allowed`. [JsonPropertyName("outcome")] public required SandboxOutcome Outcome { get; set; } + /// Why the run recorded and allowed instead of enforcing. + [JsonPropertyName("permissiveSource")] + public required SandboxPermissiveSource PermissiveSource { get; set; } + /// Host operating-system family. [JsonPropertyName("platform")] public required SandboxPlatform Platform { get; set; } - /// Executable image associated with the denial that raised this escalation, normalized to a basename. Populated only when content capture is enabled. + /// Tool call the access belongs to, for span correlation only. Never exported as a telemetry attribute or metric dimension. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("processName")] - public string? ProcessName { get; set; } - + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } +} + +/// A request to run outside the process sandbox was resolved. This is what makes an `inactive` `enforcement_state` readable: without it, a command that ran unsandboxed because a person approved a bypass looks identical to one that ran unsandboxed because the session never had a sandbox. Reported only when a sandbox was in force, since bypassing a disabled sandbox bypasses nothing. Carries neither backend nor attestation: the verdict comes from the runtime's own permission flow or the local escalation prompt, not from a containment backend and not from the built-in policy check, so `source` is what records where it came from. +/// The bypass_decided variant of . +public sealed partial class SandboxDecisionDataBypassDecided : SandboxDecisionData +{ + /// + [JsonIgnore] + public override string Kind => "bypass_decided"; + + /// Command the verdict governs. Populated only when content capture is enabled. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("command")] + public string? Command { get; set; } + + /// How strong the evidence behind `denialClass` was. Present exactly when `denialClass` is, so a verdict that relaxed the sandbox on a guess is distinguishable from one that relaxed it on a recorded refusal. Named to match `access_denied`, which reports the same pair. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("confidence")] + public SandboxDenialConfidence? Confidence { get; set; } + + /// Always `bypass`. + [JsonPropertyName("control")] + public required SandboxControl Control { get; set; } + + /// Bounded class of the access whose refusal raised this escalation. Omitted for a pre-execution bypass, such as a detached command that cannot be sandboxed and therefore resolves no denial. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("denialClass")] + public SandboxDenialClass? DenialClass { get; set; } + + /// Resource whose refusal raised this escalation. Populated only when content capture is enabled. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("deniedResource")] + public string? DeniedResource { get; set; } + + /// Runtime subsystem the bypass applies to. + [JsonPropertyName("enforcementPoint")] + public required SandboxEnforcementPoint EnforcementPoint { get; set; } + + /// Whether the bypass was granted: `approved` or `declined`. `declined` also covers the cases where nobody answered, since the sandboxed denial stands either way. + [JsonPropertyName("outcome")] + public required SandboxOutcome Outcome { get; set; } + + /// Host operating-system family. + [JsonPropertyName("platform")] + public required SandboxPlatform Platform { get; set; } + + /// Executable image associated with the denial that raised this escalation, normalized to a basename. Populated only when content capture is enabled. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("processName")] + public string? ProcessName { get; set; } + /// Where the request originated. Orthogonal to `outcome`. [JsonPropertyName("source")] public required SandboxBypassSource Source { get; set; } @@ -10063,16 +10926,16 @@ public sealed partial class SystemNotificationInstructionDiscovered : SystemNoti public required string TriggerTool { get; set; } } -/// The user variant of . -public sealed partial class SystemNotificationFactoryPauseInfoUser : SystemNotificationFactoryPauseInfo +/// The user variant of . +public sealed partial class SystemNotificationWorkflowPauseInfoUser : SystemNotificationWorkflowPauseInfo { /// [JsonIgnore] public override string Type => "user"; } -/// The checkpoint variant of . -public sealed partial class SystemNotificationFactoryPauseInfoCheckpoint : SystemNotificationFactoryPauseInfo +/// The checkpoint variant of . +public sealed partial class SystemNotificationWorkflowPauseInfoCheckpoint : SystemNotificationWorkflowPauseInfo { /// [JsonIgnore] @@ -10083,14 +10946,14 @@ public sealed partial class SystemNotificationFactoryPauseInfoCheckpoint : Syste public required string Key { get; set; } } -/// Durable metadata describing who initiated a factory pause. +/// Durable metadata describing who initiated a workflow pause. /// Polymorphic base type discriminated by type. [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(SystemNotificationFactoryPauseInfoUser), "user")] -[JsonDerivedType(typeof(SystemNotificationFactoryPauseInfoCheckpoint), "checkpoint")] -public partial class SystemNotificationFactoryPauseInfo +[JsonDerivedType(typeof(SystemNotificationWorkflowPauseInfoUser), "user")] +[JsonDerivedType(typeof(SystemNotificationWorkflowPauseInfoCheckpoint), "checkpoint")] +public partial class SystemNotificationWorkflowPauseInfo { /// The type discriminator. [JsonPropertyName("type")] @@ -10098,13 +10961,13 @@ public partial class SystemNotificationFactoryPauseInfo } -/// System notification metadata for a factory execution attempt that reached a terminal state. -/// The factory_completed variant of . -public sealed partial class SystemNotificationFactoryCompleted : SystemNotification +/// System notification metadata for a workflow execution attempt that reached a terminal state. +/// The workflow_completed variant of . +public sealed partial class SystemNotificationWorkflowCompleted : SystemNotification { /// [JsonIgnore] - public override string Type => "factory_completed"; + public override string Type => "workflow_completed"; /// Execution attempt that reached this terminal state. [JsonPropertyName("attempt")] @@ -10122,10 +10985,6 @@ public sealed partial class SystemNotificationFactoryCompleted : SystemNotificat [JsonPropertyName("elapsedMs")] public required long ElapsedMs { get; set; } - /// Persisted factory name. - [JsonPropertyName("factoryName")] - public required string FactoryName { get; set; } - /// Machine-readable terminal failure details, when present. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("failure")] @@ -10134,7 +10993,7 @@ public sealed partial class SystemNotificationFactoryCompleted : SystemNotificat /// Pause initiator metadata when this attempt settled as paused. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("pauseInfo")] - public SystemNotificationFactoryPauseInfo? PauseInfo { get; set; } + public SystemNotificationWorkflowPauseInfo? PauseInfo { get; set; } /// Bounded prompt-safe preview of the completed result. [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] @@ -10143,18 +11002,22 @@ public sealed partial class SystemNotificationFactoryCompleted : SystemNotificat [JsonPropertyName("resultPreview")] public string? ResultPreview { get; set; } - /// Actionable run_factory resume guidance for a resource-limit failure. + /// Actionable run_dynamic_workflow resume guidance for a resource-limit failure. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("retryGuidance")] public string? RetryGuidance { get; set; } - /// Factory run identifier. + /// Workflow run identifier. [JsonPropertyName("runId")] public required string RunId { get; set; } /// Terminal status reached by this execution attempt. [JsonPropertyName("status")] - public required SystemNotificationFactoryCompletedStatus Status { get; set; } + public required SystemNotificationWorkflowCompletedStatus Status { get; set; } + + /// Persisted workflow name. + [JsonPropertyName("workflowName")] + public required string WorkflowName { get; set; } } /// System notification metadata from an external host that does not match a runtime-owned notification kind. @@ -10182,7 +11045,7 @@ public sealed partial class SystemNotificationUnclassified : SystemNotification [JsonDerivedType(typeof(SystemNotificationShellCompleted), "shell_completed")] [JsonDerivedType(typeof(SystemNotificationShellDetachedCompleted), "shell_detached_completed")] [JsonDerivedType(typeof(SystemNotificationInstructionDiscovered), "instruction_discovered")] -[JsonDerivedType(typeof(SystemNotificationFactoryCompleted), "factory_completed")] +[JsonDerivedType(typeof(SystemNotificationWorkflowCompleted), "workflow_completed")] [JsonDerivedType(typeof(SystemNotificationUnclassified), "unclassified")] public partial class SystemNotification { @@ -10227,6 +11090,30 @@ public sealed partial class PermissionRequestShellPossibleUrl public required string Url { get; set; } } +/// A sandbox filesystem policy edit that would let a blocked operation run inside the sandbox instead of outside it. Offered only on a sandbox escalation request whose denial adding this path lifts, and only when managed policy permits the grant. A host accepts it with session.sandbox.grantPathForRequest, which adds the path to the session's sandbox policy and re-runs the operation sandboxed; a host that persists sandbox settings may also save the path there. +/// Nested data type for PermissionSandboxPathGrant. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class PermissionSandboxPathGrant +{ + /// Which access the grant confers, and so which policy list the path is added to. + [JsonPropertyName("access")] + public required PermissionSandboxPathGrantAccess Access { get; set; } + + /// The path the sandbox refused, present only when it differs from path. That happens when a write under a read-only folder moves the folder to the read-write paths, when a path that does not exist yet is granted through its nearest existing folder, because the OS sandbox cannot grant a path before it exists, and when either is spelled through a symlink, because a grant covers its path as written, so path is then the resolved location. Hosts should then name path in the offer, since the denial names this one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("deniedPath")] + public string? DeniedPath { get; set; } + + /// Absolute path to add to the sandbox filesystem policy. + [JsonPropertyName("path")] + public required string Path { get; set; } + + /// readonlyPaths entries the grant removes, exactly as written in the policy, because a read-only entry for the same location would otherwise keep the path read-only. A host that persists the path must remove these entries from its stored readonlyPaths too. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("removedReadonlyPaths")] + public string[]? RemovedReadonlyPaths { get; set; } +} + /// Shell command permission request. /// The shell variant of . public sealed partial class PermissionRequestShell : PermissionRequest @@ -10304,6 +11191,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("resolvedWorkingDirectory")] public string? ResolvedWorkingDirectory { get; set; } + /// Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sandboxPathGrant")] + public PermissionSandboxPathGrant? SandboxPathGrant { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -10369,6 +11262,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("resolvedPath")] public string? ResolvedPath { get; set; } + /// Sandbox policy edit that would let the write run inside the sandbox. Only present when requestSandboxBypass is true. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sandboxPathGrant")] + public PermissionSandboxPathGrant? SandboxPathGrant { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -10416,6 +11315,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("resolvedPath")] public string? ResolvedPath { get; set; } + /// Sandbox policy edit that would let the read run inside the sandbox. Only present when requestSandboxBypass is true. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sandboxPathGrant")] + public PermissionSandboxPathGrant? SandboxPathGrant { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -10703,9 +11608,9 @@ public sealed partial class PermissionRequestExtensionManagement : PermissionReq public string? ToolCallId { get; set; } } -/// A declared phase shown in a factory permission prompt. -/// Nested data type for FactoryPermissionPhase. -public sealed partial class FactoryPermissionPhase +/// A declared phase shown in a workflow permission prompt. +/// Nested data type for WorkflowPermissionPhase. +public sealed partial class WorkflowPermissionPhase { /// Optional phase detail. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -10717,46 +11622,55 @@ public sealed partial class FactoryPermissionPhase public required string Title { get; set; } } -/// Factory run or authoring permission request. -/// The factory variant of . -public sealed partial class PermissionRequestFactory : PermissionRequest +/// Workflow run or authoring permission request. +/// The workflow variant of . +public sealed partial class PermissionRequestWorkflow : PermissionRequest { /// [JsonIgnore] - public override string Kind => "factory"; + public override string Kind => "workflow"; - /// Canonical key used for scoped factory approvals. + /// Canonical key used for scoped workflow approvals. [JsonPropertyName("approvalKey")] public required string ApprovalKey { get; set; } - /// Whether this factory is eligible for persistent approval. + /// Whether this workflow is eligible for persistent approval. [JsonPropertyName("canPersistApproval")] public required bool CanPersistApproval { get; set; } - /// Factory-declared AI-credit limit before any run/resume caller override is applied. + /// Workflow-declared AI-credit limit before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredMaxAiCredits")] public double? DeclaredMaxAiCredits { get; set; } - /// Factory-declared concurrent-subagent limit before any run/resume caller override is applied. + /// Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredMaxConcurrentSubagents")] public long? DeclaredMaxConcurrentSubagents { get; set; } - /// Factory-declared total-subagent limit before any run/resume caller override is applied. + /// Workflow-declared total-subagent limit before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredMaxTotalSubagents")] public long? DeclaredMaxTotalSubagents { get; set; } - /// Factory-declared active-time limit in seconds before any run/resume caller override is applied. + /// Workflow-declared active-time limit in seconds before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredTimeoutSeconds")] public double? DeclaredTimeoutSeconds { get; set; } - /// Factory description. + /// Workflow description. [JsonPropertyName("description")] public required string Description { get; set; } + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("managedApprovalRequired")] + public override bool? ManagedApprovalRequired + { + get => base.ManagedApprovalRequired; + set => base.ManagedApprovalRequired = value; + } + /// Effective AI-credit limit; omitted means unlimited. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("maxAiCredits")] @@ -10772,17 +11686,17 @@ public sealed partial class PermissionRequestFactory : PermissionRequest [JsonPropertyName("maxTotalSubagents")] public long? MaxTotalSubagents { get; set; } - /// Factory name. + /// Workflow name. [JsonPropertyName("name")] public required string Name { get; set; } - /// Factory operation, either run or author. + /// Workflow operation, either run or author. [JsonPropertyName("operation")] - public required FactoryPermissionOperation Operation { get; set; } + public required WorkflowPermissionOperation Operation { get; set; } - /// Declared factory phases. + /// Declared workflow phases. [JsonPropertyName("phases")] - public required FactoryPermissionPhase[] Phases { get; set; } + public required WorkflowPermissionPhase[] Phases { get; set; } /// Effective active-time limit in seconds; omitted means unlimited. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -10853,7 +11767,7 @@ public sealed partial class PermissionRequestExtensionEnvAccess : PermissionRequ [JsonDerivedType(typeof(PermissionRequestCustomTool), "custom-tool")] [JsonDerivedType(typeof(PermissionRequestHook), "hook")] [JsonDerivedType(typeof(PermissionRequestExtensionManagement), "extension-management")] -[JsonDerivedType(typeof(PermissionRequestFactory), "factory")] +[JsonDerivedType(typeof(PermissionRequestWorkflow), "workflow")] [JsonDerivedType(typeof(PermissionRequestExtensionPermissionAccess), "extension-permission-access")] [JsonDerivedType(typeof(PermissionRequestExtensionEnvAccess), "extension-env-access")] public partial class PermissionRequest @@ -10919,6 +11833,12 @@ public sealed partial class PermissionPromptRequestCommands : PermissionPromptRe [JsonPropertyName("requestSandboxPermissive")] public bool? RequestSandboxPermissive { get; set; } + /// Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sandboxPathGrant")] + public PermissionSandboxPathGrant? SandboxPathGrant { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -11221,6 +12141,12 @@ public sealed partial class PermissionPromptRequestPath : PermissionPromptReques [JsonPropertyName("paths")] public required string[] Paths { get; set; } + /// Canonical directory candidates that can be granted for file-tool read access in this logical session. Present only for read path prompts. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("readOnlyDirectories")] + public string[]? ReadOnlyDirectories { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -11290,15 +12216,15 @@ public sealed partial class PermissionPromptRequestExtensionManagement : Permiss public string? ToolCallId { get; set; } } -/// Factory run or authoring permission prompt. -/// The factory variant of . -public sealed partial class PermissionPromptRequestFactory : PermissionPromptRequest +/// Workflow run or authoring permission prompt. +/// The workflow variant of . +public sealed partial class PermissionPromptRequestWorkflow : PermissionPromptRequest { /// [JsonIgnore] - public override string Kind => "factory"; + public override string Kind => "workflow"; - /// Canonical key used for scoped factory approvals. + /// Canonical key used for scoped workflow approvals. [JsonPropertyName("approvalKey")] public required string ApprovalKey { get; set; } @@ -11308,31 +12234,31 @@ public sealed partial class PermissionPromptRequestFactory : PermissionPromptReq [JsonPropertyName("assistedApproval")] public PermissionAssistedApproval? AssistedApproval { get; set; } - /// Whether this factory is eligible for persistent approval. + /// Whether this workflow is eligible for persistent approval. [JsonPropertyName("canPersistApproval")] public required bool CanPersistApproval { get; set; } - /// Factory-declared AI-credit limit before any run/resume caller override is applied. + /// Workflow-declared AI-credit limit before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredMaxAiCredits")] public double? DeclaredMaxAiCredits { get; set; } - /// Factory-declared concurrent-subagent limit before any run/resume caller override is applied. + /// Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredMaxConcurrentSubagents")] public long? DeclaredMaxConcurrentSubagents { get; set; } - /// Factory-declared total-subagent limit before any run/resume caller override is applied. + /// Workflow-declared total-subagent limit before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredMaxTotalSubagents")] public long? DeclaredMaxTotalSubagents { get; set; } - /// Factory-declared active-time limit in seconds before any run/resume caller override is applied. + /// Workflow-declared active-time limit in seconds before any run/resume caller override is applied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("declaredTimeoutSeconds")] public double? DeclaredTimeoutSeconds { get; set; } - /// Factory description. + /// Workflow description. [JsonPropertyName("description")] public required string Description { get; set; } @@ -11356,17 +12282,17 @@ public sealed partial class PermissionPromptRequestFactory : PermissionPromptReq [JsonPropertyName("maxTotalSubagents")] public long? MaxTotalSubagents { get; set; } - /// Factory name. + /// Workflow name. [JsonPropertyName("name")] public required string Name { get; set; } - /// Factory operation, either run or author. + /// Workflow operation, either run or author. [JsonPropertyName("operation")] - public required FactoryPermissionOperation Operation { get; set; } + public required WorkflowPermissionOperation Operation { get; set; } - /// Declared factory phases. + /// Declared workflow phases. [JsonPropertyName("phases")] - public required FactoryPermissionPhase[] Phases { get; set; } + public required WorkflowPermissionPhase[] Phases { get; set; } /// Effective active-time limit in seconds; omitted means unlimited. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -11450,7 +12376,7 @@ public sealed partial class PermissionPromptRequestExtensionEnvAccess : Permissi [JsonDerivedType(typeof(PermissionPromptRequestPath), "path")] [JsonDerivedType(typeof(PermissionPromptRequestHook), "hook")] [JsonDerivedType(typeof(PermissionPromptRequestExtensionManagement), "extension-management")] -[JsonDerivedType(typeof(PermissionPromptRequestFactory), "factory")] +[JsonDerivedType(typeof(PermissionPromptRequestWorkflow), "workflow")] [JsonDerivedType(typeof(PermissionPromptRequestExtensionPermissionAccess), "extension-permission-access")] [JsonDerivedType(typeof(PermissionPromptRequestExtensionEnvAccess), "extension-env-access")] public partial class PermissionPromptRequest @@ -11475,6 +12401,19 @@ public sealed partial class PermissionResultApproved : PermissionResult public bool? ManagedApprovalHandled { get; set; } } +/// Permission response variant that approves a request and records file-tool read authority for specific directories in this logical session. +/// The approved-read-only-for-session variant of . +public sealed partial class PermissionResultApprovedReadOnlyForSession : PermissionResult +{ + /// + [JsonIgnore] + public override string Kind => "approved-read-only-for-session"; + + /// Canonical directories covered by the session read-only grant. + [JsonPropertyName("directories")] + public required string[] Directories { get; set; } +} + /// Session-scoped tool-approval rule for specific shell command identifiers. /// The commands variant of . public sealed partial class UserToolSessionApprovalCommands : UserToolSessionApproval @@ -11559,15 +12498,15 @@ public sealed partial class UserToolSessionApprovalExtensionManagement : UserToo public string? Operation { get; set; } } -/// Session-scoped factory approval, optionally narrowed by approval key. -/// The factory variant of . -public sealed partial class UserToolSessionApprovalFactory : UserToolSessionApproval +/// Session-scoped workflow approval, optionally narrowed by approval key. +/// The workflow variant of . +public sealed partial class UserToolSessionApprovalWorkflow : UserToolSessionApproval { /// [JsonIgnore] - public override string Kind => "factory"; + public override string Kind => "workflow"; - /// Optional factory operation name or canonical approval key. + /// Optional workflow operation name or canonical approval key. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("approvalKey")] public string? ApprovalKey { get; set; } @@ -11615,7 +12554,7 @@ public sealed partial class UserToolSessionApprovalExtensionEnvAccess : UserTool [JsonDerivedType(typeof(UserToolSessionApprovalMemory), "memory")] [JsonDerivedType(typeof(UserToolSessionApprovalCustomTool), "custom-tool")] [JsonDerivedType(typeof(UserToolSessionApprovalExtensionManagement), "extension-management")] -[JsonDerivedType(typeof(UserToolSessionApprovalFactory), "factory")] +[JsonDerivedType(typeof(UserToolSessionApprovalWorkflow), "workflow")] [JsonDerivedType(typeof(UserToolSessionApprovalExtensionPermissionAccess), "extension-permission-access")] [JsonDerivedType(typeof(UserToolSessionApprovalExtensionEnvAccess), "extension-env-access")] public partial class UserToolSessionApproval @@ -11776,6 +12715,7 @@ public sealed partial class PermissionResultDeniedByPermissionRequestHook : Perm TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(PermissionResultApproved), "approved")] +[JsonDerivedType(typeof(PermissionResultApprovedReadOnlyForSession), "approved-read-only-for-session")] [JsonDerivedType(typeof(PermissionResultApprovedForSession), "approved-for-session")] [JsonDerivedType(typeof(PermissionResultApprovedForLocation), "approved-for-location")] [JsonDerivedType(typeof(PermissionResultCancelled), "cancelled")] @@ -11943,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 @@ -13260,6 +14309,9 @@ public ModelChangeSource(string value) /// An SDK or RPC caller selected the model. public static ModelChangeSource Sdk { get; } = new("sdk"); + /// The user accepted a CAPI-issued Auto tier recommendation. + public static ModelChangeSource AutoTierRecommendation { get; } = new("auto_tier_recommendation"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(ModelChangeSource left, ModelChangeSource right) => left.Equals(right); @@ -16006,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}")] @@ -16140,7 +17271,7 @@ public override void Write(Utf8JsonWriter writer, AbortReason value, JsonSeriali } } -/// Configuration source: user, workspace, plugin, builtin, or managed. +/// Configuration source: user, workspace, plugin, builtin, managed, or account. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct McpServerSource : IEquatable @@ -16174,6 +17305,9 @@ public McpServerSource(string value) /// Server supplied by a trusted host-managed catalog. public static McpServerSource Managed { get; } = new("managed"); + /// Server contributed by a signed-in account; enablement and organization policy still apply. + public static McpServerSource Account { get; } = new("account"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(McpServerSource left, McpServerSource right) => left.Equals(right); @@ -16338,6 +17472,135 @@ public override void Write(Utf8JsonWriter writer, ToolExecutionStartToolDescript } } +/// Shell output source. Terminal output has no separate stdout/stderr attribution. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ToolShellOutputStream : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ToolShellOutputStream(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Output from the shell command's standard output stream. This is the default when stream is omitted. + public static ToolShellOutputStream Stdout { get; } = new("stdout"); + + /// Output from the shell command's standard error stream. + public static ToolShellOutputStream Stderr { get; } = new("stderr"); + + /// Inherently merged output that cannot be attributed separately to stdout or stderr. + public static ToolShellOutputStream Terminal { get; } = new("terminal"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ToolShellOutputStream left, ToolShellOutputStream right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ToolShellOutputStream left, ToolShellOutputStream right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ToolShellOutputStream other && Equals(other); + + /// + public bool Equals(ToolShellOutputStream other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ToolShellOutputStream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ToolShellOutputStream value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ToolShellOutputStream)); + } + } +} + +/// Kind of file mutation committed by a built-in editing tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ToolExecutionCompleteFileEditKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ToolExecutionCompleteFileEditKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A file was created. + public static ToolExecutionCompleteFileEditKind Create { get; } = new("create"); + + /// A file was written by an edit operation. + public static ToolExecutionCompleteFileEditKind Edit { get; } = new("edit"); + + /// A file was deleted. + public static ToolExecutionCompleteFileEditKind Delete { get; } = new("delete"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ToolExecutionCompleteFileEditKind left, ToolExecutionCompleteFileEditKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ToolExecutionCompleteFileEditKind left, ToolExecutionCompleteFileEditKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ToolExecutionCompleteFileEditKind other && Equals(other); + + /// + public bool Equals(ToolExecutionCompleteFileEditKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ToolExecutionCompleteFileEditKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ToolExecutionCompleteFileEditKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ToolExecutionCompleteFileEditKind)); + } + } +} + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -16984,7 +18247,7 @@ public override void Write(Utf8JsonWriter writer, SandboxEnforcementPoint value, } } -/// Finite result of a sandbox decision. Each `SandboxDecisionData` variant uses a disjoint subset: `policy_resolved` is `resolved | degraded`, `spawn_completed` and `permissive_retry_completed` are `succeeded | failed`, `enforcement_state` is `engaged | inactive | failed`, `access_denied` is `denied`, and escalation decisions are `approved | declined`. +/// Finite result of a sandbox decision. Each `SandboxDecisionData` variant uses a disjoint subset: `policy_resolved` is `resolved | degraded`, `spawn_completed` and `permissive_retry_completed` are `succeeded | failed`, `enforcement_state` is `engaged | inactive | failed`, `access_denied` is `denied`, `access_recorded` is `allowed`, and escalation decisions are `approved | declined`. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct SandboxOutcome : IEquatable @@ -17030,6 +18293,9 @@ public SandboxOutcome(string value) /// A request to run outside the process sandbox was not granted. public static SandboxOutcome Declined { get; } = new("declined"); + /// Permissive learning mode recorded an access the enforced policy would have refused, and allowed it. + public static SandboxOutcome Allowed { get; } = new("allowed"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(SandboxOutcome left, SandboxOutcome right) => left.Equals(right); @@ -17483,6 +18749,67 @@ public override void Write(Utf8JsonWriter writer, SandboxDenialClass value, Json } } +/// Why a sandboxed run recorded and allowed its process-container access checks instead of enforcing them. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SandboxPermissiveSource : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SandboxPermissiveSource(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A person approved the permissive retry for this command after a sandboxed attempt was blocked. + public static SandboxPermissiveSource ApprovedRetry { get; } = new("approved_retry"); + + /// Device-managed policy (`sandbox.learningMode: "allow"`) starts sandboxed shell commands in permissive learning mode. + public static SandboxPermissiveSource Policy { get; } = new("policy"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SandboxPermissiveSource left, SandboxPermissiveSource right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SandboxPermissiveSource left, SandboxPermissiveSource right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SandboxPermissiveSource other && Equals(other); + + /// + public bool Equals(SandboxPermissiveSource 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 SandboxPermissiveSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SandboxPermissiveSource value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SandboxPermissiveSource)); + } + } +} + /// Where a request to run outside the process sandbox originated. Orthogonal to the outcome: the same verdict means a different thing depending on where the request came from. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -17873,51 +19200,51 @@ public override void Write(Utf8JsonWriter writer, SystemNotificationAgentComplet } } -/// Terminal status reached by a factory execution attempt. +/// Terminal status reached by a workflow execution attempt. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SystemNotificationFactoryCompletedStatus : IEquatable +public readonly struct SystemNotificationWorkflowCompletedStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SystemNotificationFactoryCompletedStatus(string value) + public SystemNotificationWorkflowCompletedStatus(string 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 factory completed successfully. - public static SystemNotificationFactoryCompletedStatus Completed { get; } = new("completed"); + /// The workflow completed successfully. + public static SystemNotificationWorkflowCompletedStatus Completed { get; } = new("completed"); - /// The factory was halted. - public static SystemNotificationFactoryCompletedStatus Halted { get; } = new("halted"); + /// The workflow was halted. + public static SystemNotificationWorkflowCompletedStatus Halted { get; } = new("halted"); - /// The factory attempt paused intentionally. - public static SystemNotificationFactoryCompletedStatus Paused { get; } = new("paused"); + /// The workflow attempt paused intentionally. + public static SystemNotificationWorkflowCompletedStatus Paused { get; } = new("paused"); - /// The factory was cancelled. - public static SystemNotificationFactoryCompletedStatus Cancelled { get; } = new("cancelled"); + /// The workflow was cancelled. + public static SystemNotificationWorkflowCompletedStatus Cancelled { get; } = new("cancelled"); - /// The factory failed. - public static SystemNotificationFactoryCompletedStatus Error { get; } = new("error"); + /// The workflow failed. + public static SystemNotificationWorkflowCompletedStatus Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SystemNotificationFactoryCompletedStatus left, SystemNotificationFactoryCompletedStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SystemNotificationWorkflowCompletedStatus left, SystemNotificationWorkflowCompletedStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SystemNotificationFactoryCompletedStatus left, SystemNotificationFactoryCompletedStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SystemNotificationWorkflowCompletedStatus left, SystemNotificationWorkflowCompletedStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SystemNotificationFactoryCompletedStatus other && Equals(other); + public override bool Equals(object? obj) => obj is SystemNotificationWorkflowCompletedStatus other && Equals(other); /// - public bool Equals(SystemNotificationFactoryCompletedStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SystemNotificationWorkflowCompletedStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -17925,20 +19252,81 @@ public SystemNotificationFactoryCompletedStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SystemNotificationFactoryCompletedStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SystemNotificationWorkflowCompletedStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SystemNotificationFactoryCompletedStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SystemNotificationWorkflowCompletedStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SystemNotificationFactoryCompletedStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SystemNotificationWorkflowCompletedStatus)); + } + } +} + +/// Access a sandbox path grant confers. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct PermissionSandboxPathGrantAccess : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public PermissionSandboxPathGrantAccess(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Read access: the path is added to readonlyPaths. + public static PermissionSandboxPathGrantAccess Read { get; } = new("read"); + + /// Read and write access: the path is added to readwritePaths. + public static PermissionSandboxPathGrantAccess ReadWrite { get; } = new("readWrite"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionSandboxPathGrantAccess left, PermissionSandboxPathGrantAccess right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionSandboxPathGrantAccess left, PermissionSandboxPathGrantAccess right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is PermissionSandboxPathGrantAccess other && Equals(other); + + /// + public bool Equals(PermissionSandboxPathGrantAccess 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 PermissionSandboxPathGrantAccess Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, PermissionSandboxPathGrantAccess value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionSandboxPathGrantAccess)); } } } @@ -18249,6 +19637,9 @@ public PermissionApprovalEvaluationReasonCode(string value) /// The script path was not authorized for inspection. public static PermissionApprovalEvaluationReasonCode PathNotAuthorized { get; } = new("path-not-authorized"); + /// A code source was excluded from review by content exclusion policy. + public static PermissionApprovalEvaluationReasonCode ContentExcluded { get; } = new("content-excluded"); + /// The script working directory was invalid. public static PermissionApprovalEvaluationReasonCode InvalidWorkingDirectory { get; } = new("invalid-working-directory"); @@ -18285,6 +19676,24 @@ public PermissionApprovalEvaluationReasonCode(string value) /// The script argument binding could not be reviewed. public static PermissionApprovalEvaluationReasonCode ArgumentBindingUnreviewable { get; } = new("argument-binding-unreviewable"); + /// The shell command could not be analyzed for execution evidence. + public static PermissionApprovalEvaluationReasonCode UnsupportedCommandShape { get; } = new("unsupported-command-shape"); + + /// The shell command used a code source that cannot be bound for review. + public static PermissionApprovalEvaluationReasonCode UnsupportedSource { get; } = new("unsupported-source"); + + /// The shell command used a code source computed at run time. + public static PermissionApprovalEvaluationReasonCode DynamicSource { get; } = new("dynamic-source"); + + /// The shell command referenced more code sources than can be reviewed. + public static PermissionApprovalEvaluationReasonCode TooManySources { get; } = new("too-many-sources"); + + /// A code-bearing executable could not be inspected. + public static PermissionApprovalEvaluationReasonCode ExecutableUnavailable { get; } = new("executable-unavailable"); + + /// A code-bearing executable exceeded the binding size limit. + public static PermissionApprovalEvaluationReasonCode ExecutableTooLarge { get; } = new("executable-too-large"); + /// The script review metadata was malformed. public static PermissionApprovalEvaluationReasonCode MalformedScriptActionReview { get; } = new("malformed-script-action-review"); @@ -18600,42 +20009,42 @@ public override void Write(Utf8JsonWriter writer, PermissionRequestMemoryScope v } } -/// Operation gated by a factory permission request. +/// Operation gated by a workflow permission request. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FactoryPermissionOperation : IEquatable +public readonly struct WorkflowPermissionOperation : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryPermissionOperation(string value) + public WorkflowPermissionOperation(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Running a registered factory, which spends subagents, active time, and AI credits under the approved limits. - public static FactoryPermissionOperation Run { get; } = new("run"); + /// Running a registered workflow, which spends subagents, active time, and AI credits under the approved limits. + public static WorkflowPermissionOperation Run { get; } = new("run"); - /// Authoring a factory, which writes JavaScript into a session-scoped extension and loads it. - public static FactoryPermissionOperation Author { get; } = new("author"); + /// Authoring a workflow, which writes JavaScript into a session-scoped extension and loads it. + public static WorkflowPermissionOperation Author { get; } = new("author"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryPermissionOperation left, FactoryPermissionOperation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowPermissionOperation left, WorkflowPermissionOperation right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryPermissionOperation left, FactoryPermissionOperation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowPermissionOperation left, WorkflowPermissionOperation right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryPermissionOperation other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowPermissionOperation other && Equals(other); /// - public bool Equals(FactoryPermissionOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowPermissionOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -18643,20 +20052,20 @@ public FactoryPermissionOperation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryPermissionOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowPermissionOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryPermissionOperation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowPermissionOperation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryPermissionOperation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPermissionOperation)); } } } @@ -18756,7 +20165,7 @@ public PermissionDecisionSource(string value) /// The host denied the request because no interactive user response was available. public static PermissionDecisionSource UnattendedFallback { get; } = new("unattended_fallback"); - /// A live authorization record from an earlier human decision in this session contained the proposal, so it ran without another prompt. This is not a new human decision and never mints authority of its own. + /// Historical compatibility value for sessions created while authorization carry-forward was executable. Current runtimes do not produce this source. public static PermissionDecisionSource AuthorizationCarryForward { get; } = new("authorization_carry_forward"); /// Returns a value indicating whether two instances are equivalent. @@ -18795,7 +20204,7 @@ public override void Write(Utf8JsonWriter writer, PermissionDecisionSource value } } -/// Which direction a message-backed authorization claim moves authority in. +/// Direction stored in a historical extractor claim. Current runtimes do not apply it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -18815,10 +20224,10 @@ public PermissionMessageAuthorizationPolarity(string value) /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The human's words authorized an effect. + /// Historical claim recorded as a grant. public static PermissionMessageAuthorizationPolarity Grant { get; } = new("grant"); - /// The human's words refused an effect. + /// Historical claim recorded as a denial. public static PermissionMessageAuthorizationPolarity Denial { get; } = new("denial"); /// Returns a value indicating whether two instances are equivalent. @@ -19778,51 +21187,115 @@ public override void Write(Utf8JsonWriter writer, ExitPlanModeAction value, Json } } -/// Terminal status a factory run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. +/// 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}")] -public readonly struct FactoryRunSettledStatus : IEquatable +public readonly struct WorkflowRunSettledStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FactoryRunSettledStatus(string value) + public WorkflowRunSettledStatus(string 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 factory body resolved and its result was committed. - public static FactoryRunSettledStatus Completed { get; } = new("completed"); + /// The workflow body resolved and its result was committed. + public static WorkflowRunSettledStatus Completed { get; } = new("completed"); /// The run was stopped by a limit, an approval refusal or another policy decision. - public static FactoryRunSettledStatus Halted { get; } = new("halted"); + public static WorkflowRunSettledStatus Halted { get; } = new("halted"); /// The attempt paused intentionally while preserving resumable run state. - public static FactoryRunSettledStatus Paused { get; } = new("paused"); + public static WorkflowRunSettledStatus Paused { get; } = new("paused"); /// The run was cancelled by its caller or by session disposal. - public static FactoryRunSettledStatus Cancelled { get; } = new("cancelled"); + public static WorkflowRunSettledStatus Cancelled { get; } = new("cancelled"); /// The run failed, with `failureType` carrying the class when it has one. - public static FactoryRunSettledStatus Error { get; } = new("error"); + public static WorkflowRunSettledStatus Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FactoryRunSettledStatus left, FactoryRunSettledStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowRunSettledStatus left, WorkflowRunSettledStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FactoryRunSettledStatus left, FactoryRunSettledStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowRunSettledStatus left, WorkflowRunSettledStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FactoryRunSettledStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowRunSettledStatus other && Equals(other); /// - public bool Equals(FactoryRunSettledStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowRunSettledStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -19830,20 +21303,20 @@ public FactoryRunSettledStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FactoryRunSettledStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowRunSettledStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FactoryRunSettledStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowRunSettledStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FactoryRunSettledStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunSettledStatus)); } } } @@ -20022,7 +21495,7 @@ public McpServerStatus(string value) /// The server is configured but disabled. public static McpServerStatus Disabled { get; } = new("disabled"); - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. public static McpServerStatus Stopped { get; } = new("stopped"); /// The server is not configured for this session. @@ -20322,14 +21795,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ExternalToolCompletedEvent))] [JsonSerializable(typeof(ExternalToolRequestedData))] [JsonSerializable(typeof(ExternalToolRequestedEvent))] -[JsonSerializable(typeof(FactoryPermissionPhase))] -[JsonSerializable(typeof(FactoryRunSettledData))] -[JsonSerializable(typeof(FactoryRunSettledEvent))] -[JsonSerializable(typeof(FactoryRunStartedData))] -[JsonSerializable(typeof(FactoryRunStartedEvent))] -[JsonSerializable(typeof(FactoryRunUpdatedData))] -[JsonSerializable(typeof(FactoryRunUpdatedEvent))] [JsonSerializable(typeof(FusionAttribution))] +[JsonSerializable(typeof(FusionCritic))] [JsonSerializable(typeof(FusionFollowUpRecommendation))] [JsonSerializable(typeof(FusionPhasePlanStep))] [JsonSerializable(typeof(FusionPhaseUsage))] @@ -20346,6 +21813,12 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(HookProgressEvent))] [JsonSerializable(typeof(HookStartData))] [JsonSerializable(typeof(HookStartEvent))] +[JsonSerializable(typeof(HumanResponseRecordedData))] +[JsonSerializable(typeof(HumanResponseRecordedEvent))] +[JsonSerializable(typeof(HumanResponseRecordedResponse))] +[JsonSerializable(typeof(HumanResponseRecordedResponseAskUser))] +[JsonSerializable(typeof(HumanResponseRecordedResponseExitPlanMode))] +[JsonSerializable(typeof(HumanResponseRecordedResponseUserInput))] [JsonSerializable(typeof(McpAppToolCallCompleteData))] [JsonSerializable(typeof(McpAppToolCallCompleteError))] [JsonSerializable(typeof(McpAppToolCallCompleteEvent))] @@ -20373,6 +21846,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))] @@ -20402,13 +21877,13 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(PermissionPromptRequestExtensionEnvAccess))] [JsonSerializable(typeof(PermissionPromptRequestExtensionManagement))] [JsonSerializable(typeof(PermissionPromptRequestExtensionPermissionAccess))] -[JsonSerializable(typeof(PermissionPromptRequestFactory))] [JsonSerializable(typeof(PermissionPromptRequestHook))] [JsonSerializable(typeof(PermissionPromptRequestMcp))] [JsonSerializable(typeof(PermissionPromptRequestMemory))] [JsonSerializable(typeof(PermissionPromptRequestPath))] [JsonSerializable(typeof(PermissionPromptRequestRead))] [JsonSerializable(typeof(PermissionPromptRequestUrl))] +[JsonSerializable(typeof(PermissionPromptRequestWorkflow))] [JsonSerializable(typeof(PermissionPromptRequestWrite))] [JsonSerializable(typeof(PermissionRecoveryAttempt))] [JsonSerializable(typeof(PermissionRecoveryData))] @@ -20417,7 +21892,6 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(PermissionRequestExtensionEnvAccess))] [JsonSerializable(typeof(PermissionRequestExtensionManagement))] [JsonSerializable(typeof(PermissionRequestExtensionPermissionAccess))] -[JsonSerializable(typeof(PermissionRequestFactory))] [JsonSerializable(typeof(PermissionRequestHook))] [JsonSerializable(typeof(PermissionRequestMcp))] [JsonSerializable(typeof(PermissionRequestMemory))] @@ -20427,6 +21901,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(PermissionRequestShellCommandSegment))] [JsonSerializable(typeof(PermissionRequestShellPossibleUrl))] [JsonSerializable(typeof(PermissionRequestUrl))] +[JsonSerializable(typeof(PermissionRequestWorkflow))] [JsonSerializable(typeof(PermissionRequestWrite))] [JsonSerializable(typeof(PermissionRequestedData))] [JsonSerializable(typeof(PermissionRequestedEvent))] @@ -20434,6 +21909,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(PermissionResultApproved))] [JsonSerializable(typeof(PermissionResultApprovedForLocation))] [JsonSerializable(typeof(PermissionResultApprovedForSession))] +[JsonSerializable(typeof(PermissionResultApprovedReadOnlyForSession))] [JsonSerializable(typeof(PermissionResultCancelled))] [JsonSerializable(typeof(PermissionResultDeniedByContentExclusionPolicy))] [JsonSerializable(typeof(PermissionResultDeniedByPermissionRequestHook))] @@ -20441,6 +21917,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(PermissionResultDeniedInteractivelyByUser))] [JsonSerializable(typeof(PermissionResultDeniedNoApprovalRuleAndCouldNotRequestFromUser))] [JsonSerializable(typeof(PermissionRule))] +[JsonSerializable(typeof(PermissionSandboxPathGrant))] [JsonSerializable(typeof(PersistedBinaryImage))] [JsonSerializable(typeof(PersistedBinaryResult))] [JsonSerializable(typeof(PromptCacheBreakData))] @@ -20452,6 +21929,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SamplingRequestedEvent))] [JsonSerializable(typeof(SandboxDecisionData))] [JsonSerializable(typeof(SandboxDecisionDataAccessDenied))] +[JsonSerializable(typeof(SandboxDecisionDataAccessRecorded))] [JsonSerializable(typeof(SandboxDecisionDataBypassDecided))] [JsonSerializable(typeof(SandboxDecisionDataEnforcementState))] [JsonSerializable(typeof(SandboxDecisionDataPermissiveRetryCompleted))] @@ -20501,6 +21979,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionErrorData))] [JsonSerializable(typeof(SessionErrorEvent))] [JsonSerializable(typeof(SessionEvent))] +[JsonSerializable(typeof(SessionEventEnvelope))] [JsonSerializable(typeof(SessionExtensionsAttachmentsPushedData))] [JsonSerializable(typeof(SessionExtensionsAttachmentsPushedEvent))] [JsonSerializable(typeof(SessionExtensionsLoadedData))] @@ -20632,15 +22111,15 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SystemNotificationAgentIdle))] [JsonSerializable(typeof(SystemNotificationData))] [JsonSerializable(typeof(SystemNotificationEvent))] -[JsonSerializable(typeof(SystemNotificationFactoryCompleted))] -[JsonSerializable(typeof(SystemNotificationFactoryPauseInfo))] -[JsonSerializable(typeof(SystemNotificationFactoryPauseInfoCheckpoint))] -[JsonSerializable(typeof(SystemNotificationFactoryPauseInfoUser))] [JsonSerializable(typeof(SystemNotificationInstructionDiscovered))] [JsonSerializable(typeof(SystemNotificationNewInboxMessage))] [JsonSerializable(typeof(SystemNotificationShellCompleted))] [JsonSerializable(typeof(SystemNotificationShellDetachedCompleted))] [JsonSerializable(typeof(SystemNotificationUnclassified))] +[JsonSerializable(typeof(SystemNotificationWorkflowCompleted))] +[JsonSerializable(typeof(SystemNotificationWorkflowPauseInfo))] +[JsonSerializable(typeof(SystemNotificationWorkflowPauseInfoCheckpoint))] +[JsonSerializable(typeof(SystemNotificationWorkflowPauseInfoUser))] [JsonSerializable(typeof(TaskBlocker))] [JsonSerializable(typeof(ToolExecutionCompleteContent))] [JsonSerializable(typeof(ToolExecutionCompleteContentAudio))] @@ -20655,6 +22134,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionCompleteData))] [JsonSerializable(typeof(ToolExecutionCompleteError))] [JsonSerializable(typeof(ToolExecutionCompleteEvent))] +[JsonSerializable(typeof(ToolExecutionCompleteFileEdit))] [JsonSerializable(typeof(ToolExecutionCompleteResult))] [JsonSerializable(typeof(ToolExecutionCompleteShellExecution))] [JsonSerializable(typeof(ToolExecutionCompleteToolDescription))] @@ -20681,6 +22161,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionStartToolDescriptionMetaUI))] [JsonSerializable(typeof(ToolSearchActivatedData))] [JsonSerializable(typeof(ToolSearchActivatedEvent))] +[JsonSerializable(typeof(ToolShellOutputData))] +[JsonSerializable(typeof(ToolShellOutputEvent))] [JsonSerializable(typeof(ToolUserRequestedData))] [JsonSerializable(typeof(ToolUserRequestedEvent))] [JsonSerializable(typeof(UiEphemeralQueryData))] @@ -20698,11 +22180,18 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(UserToolSessionApprovalExtensionEnvAccess))] [JsonSerializable(typeof(UserToolSessionApprovalExtensionManagement))] [JsonSerializable(typeof(UserToolSessionApprovalExtensionPermissionAccess))] -[JsonSerializable(typeof(UserToolSessionApprovalFactory))] [JsonSerializable(typeof(UserToolSessionApprovalMcp))] [JsonSerializable(typeof(UserToolSessionApprovalMemory))] [JsonSerializable(typeof(UserToolSessionApprovalRead))] +[JsonSerializable(typeof(UserToolSessionApprovalWorkflow))] [JsonSerializable(typeof(UserToolSessionApprovalWrite))] +[JsonSerializable(typeof(WorkflowPermissionPhase))] +[JsonSerializable(typeof(WorkflowRunSettledData))] +[JsonSerializable(typeof(WorkflowRunSettledEvent))] +[JsonSerializable(typeof(WorkflowRunStartedData))] +[JsonSerializable(typeof(WorkflowRunStartedEvent))] +[JsonSerializable(typeof(WorkflowRunUpdatedData))] +[JsonSerializable(typeof(WorkflowRunUpdatedEvent))] [JsonSerializable(typeof(WorkingDirectoryContext))] [JsonSerializable(typeof(JsonElement))] internal sealed partial class SessionEventsJsonContext : JsonSerializerContext; \ No newline at end of file diff --git a/dotnet/src/Host.cs b/dotnet/src/Host.cs new file mode 100644 index 0000000000..5835ea3a4c --- /dev/null +++ b/dotnet/src/Host.cs @@ -0,0 +1,406 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; +using GitHub.Copilot.Rpc; +using Microsoft.Extensions.Logging; + +namespace GitHub.Copilot; + +/// Reports termination of a connection-owned AHP listener. +/// The listener identity. +/// The reason participation ended. +/// Legacy companion-process exit code, absent for in-process hosting. +/// An explicit startup or cleanup failure, if any. +[Experimental(Diagnostics.Experimental)] +public sealed record AhpHostExit(string HostId, HostExitReason Reason, long? ExitCode, string? Error); + +/// Settings selected by the host; preserve Config and add application callbacks. +/// Configuration for CreateSessionAsync. +/// Cancelled when participation ends, including before the factory returns. +[Experimental(Diagnostics.Experimental)] +public sealed record AhpSessionCreateRequest(SessionConfig Config, CancellationToken CancellationToken); + +/// Resume the requested identity with Config, or return its retained original. +/// The durable application session identity. +/// Configuration for ResumeSessionAsync. +/// Cancelled when participation ends. +[Experimental(Diagnostics.Experimental)] +public sealed record AhpSessionResumeRequest(string SessionId, ResumeSessionConfig Config, CancellationToken CancellationToken); + +/// +/// 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 +{ + /// 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. + public Func>? ResumeSession { get; init; } + /// + /// Receives the original object once per handoff, including late factory results. + /// The SDK never destroys or disconnects that session on the application's behalf. + /// Callback failures are reported through the client's logger. + /// + public Func? OnSessionReleased { get; init; } + /// Reports listener exit once. Callback failures are logged. + public Func? OnExit { get; init; } +} + +/// An AHP listener bound to its original SDK connection, not a later reconnect. +[Experimental(Diagnostics.Experimental)] +public sealed class AhpHost : IAsyncDisposable +{ + private readonly ServerHostApi _rpc; + + internal AhpHost(HostStartResult info, ServerHostApi rpc) + { + HostId = info.HostId; + 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, 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) + => _rpc.PublishSessionAsync(HostId, sessionId, cancellationToken); + + /// Stops and joins listener cleanup without deleting application sessions. + public ValueTask DisposeAsync() => new(DisposeAsync(CancellationToken.None)); + + /// Every disposal reaches the runtime, including repeated and concurrent calls. + public Task DisposeAsync(CancellationToken cancellationToken) => _rpc.DisposeAsync(HostId, cancellationToken); +} + +public sealed partial class CopilotClient +{ + private readonly object _ahpGate = new(); + private readonly Dictionary _ahpHosts = []; + private readonly Dictionary _ahpHandoffs = []; + + /// + /// 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, 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(); + var hostId = Guid.NewGuid().ToString(); + lock (_ahpGate) _ahpHosts.Add(hostId, options); + var rpc = connection.Server.Host; + var startup = rpc.StartAsync(hostId, localServer, githubEnvironment, + options.CreateSession is not null ? true : null, + options.ResumeSession is not null ? true : null, CancellationToken.None); + try + { + var info = await startup.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false); + return new AhpHost(info, rpc); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + _ = DisposeAbandonedAhpStartupAsync(startup, rpc, hostId); + throw; + } + catch + { + ReleaseAhpHost(hostId); + throw; + } + } + + private async Task DisposeAbandonedAhpStartupAsync(Task startup, ServerHostApi rpc, string hostId) + { + try + { + HostStartResult info; + try { info = await startup.ConfigureAwait(false); } + catch { return; } + await rpc.DisposeAsync(info.HostId, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception error) { _logger.LogError(error, "AHP cancelled startup cleanup failed"); } + finally { ReleaseAhpHost(hostId); } + } + + private void RegisterAhpHandlers(JsonRpc rpc) + { + rpc.SetLocalRpcMethod("host.materializeSession", + (Func>)(request => new(MaterializeAhpSessionAsync(request))), + singleObjectParam: true); + rpc.SetLocalRpcMethod("host.sessionReleased", + (Action)(request => ReleaseAhpSession(request.HostId, request.HandoffId)), singleObjectParam: true); + rpc.SetLocalRpcMethod("host.exited", (Action)(request => + HandleAhpExit(new(request.HostId, request.Reason, request.ExitCode, request.Error))), singleObjectParam: true); + } + + private AhpHostOptions? ReleaseAhpHost(string hostId) + { + AhpHostOptions? options; + string[] handoffs; + lock (_ahpGate) + { + _ahpHosts.TryGetValue(hostId, out options); + _ahpHosts.Remove(hostId); + handoffs = _ahpHandoffs.Where(pair => pair.Value.HostId == hostId).Select(pair => pair.Key).ToArray(); + } + foreach (var id in handoffs) ReleaseAhpSession(hostId, id); + return options; + } + + private void HandleAhpExit(AhpHostExit report) + { + var options = ReleaseAhpHost(report.HostId); + if (options?.OnExit is { } callback) + _ = NotifyAhpAsync(() => callback(report), "exit"); + } + + private void DisconnectAhpHosts() + { + string[] ids; + lock (_ahpGate) ids = _ahpHosts.Keys.ToArray(); + foreach (var id in ids) + HandleAhpExit(new(id, HostExitReason.OwnerDisconnected, null, + "Owner connection closed; runtime cleanup cannot be acknowledged on this connection.")); + } + + private void ReleaseAhpSession(string hostId, string handoffId) + { + AhpHandoff entry; + CopilotSession? session; + lock (_ahpGate) + { + if (!_ahpHandoffs.TryGetValue(handoffId, out entry!) || entry.HostId != hostId) return; + _ahpHandoffs.Remove(handoffId); + entry.Released = true; + session = entry.Session; + entry.Session = null; + entry.Configs = null; + entry.Cancelled.TrySetResult(true); + } + _ = CancelAhpSessionAsync(entry); + NotifyAhpReleased(entry, session); + _ = DisposeAhpCancellationAsync(entry); + } + + private async Task CancelAhpSessionAsync(AhpHandoff entry) + { + try + { +#if NET8_0_OR_GREATER + await entry.Cancellation.CancelAsync().ConfigureAwait(false); +#else + await Task.Run(() => entry.Cancellation.Cancel()).ConfigureAwait(false); +#endif + } + catch (AggregateException error) { _logger.LogError(error, "AHP cancellation callback failed"); } + finally { entry.CancellationCompleted.TrySetResult(true); } + } + + private static async Task DisposeAhpCancellationAsync(AhpHandoff entry) + { + await entry.FactoryCompleted.Task.ConfigureAwait(false); + await entry.CancellationCompleted.Task.ConfigureAwait(false); + entry.Cancellation.Dispose(); + } + + private void NotifyAhpReleased(AhpHandoff entry, CopilotSession? session) + { + if (session is not null && entry.Options.OnSessionReleased is { } callback) + _ = NotifyAhpAsync(async () => + { + await entry.CancellationCompleted.Task.ConfigureAwait(false); + await callback(session).ConfigureAwait(false); + }, "session release"); + } + + private async Task NotifyAhpAsync(Func callback, string label) + { + try { await Task.Run(callback).ConfigureAwait(false); } + catch (Exception error) { _logger.LogError(error, "AHP {Callback} callback failed", label); } + } + + private void CaptureAhpSession(CopilotSession session, T request, JsonTypeInfo typeInfo) + { + AhpHandoff[] entries; + lock (_ahpGate) + entries = _ahpHandoffs.Values.Where(entry => entry.SessionId == session.SessionId && entry.Configs is not null).ToArray(); + if (entries.Length == 0) return; + var snapshot = JsonSerializer.SerializeToElement(request, typeInfo); + lock (_ahpGate) + foreach (var entry in entries) + if (!entry.Released && entry.Configs is { } configs) configs[session] = snapshot; + } + + private async Task MaterializeAhpSessionAsync(AhpMaterializeRequest request) + { + var sessionId = request.Config.GetProperty("sessionId").GetString(); + if (sessionId is null || sessionId.Length == 0) throw new ArgumentException("AHP handoff requires sessionId"); + AhpHandoff entry; + lock (_ahpGate) + { + if (!_ahpHosts.TryGetValue(request.HostId, out var options) || _ahpHandoffs.ContainsKey(request.HandoffId) || + (request.Resume ? options.ResumeSession is null : options.CreateSession is null)) + throw new InvalidOperationException("AHP session factory is unavailable or handoff already exists"); + entry = new(request.HostId, sessionId, options); + _ahpHandoffs.Add(request.HandoffId, entry); + } + var cancellationToken = entry.Token; + var materialized = Task.Run(async () => + { + try + { + var config = JsonNode.Parse(request.Config.GetRawText())!.AsObject(); + if (config.TryGetPropertyValue("configDir", out var directory)) + { + config.Remove("configDir"); + config["configDirectory"] = directory; + } + CopilotSession session; + if (request.Resume) + { + config.Remove("sessionId"); + var options = config.Deserialize(ClientJsonContext.Default.ResumeSessionConfig) + ?? throw new InvalidOperationException("Invalid AHP resume configuration"); + session = await entry.Options.ResumeSession!(new(sessionId, options, entry.Token)).ConfigureAwait(false); + } + else + { + var options = config.Deserialize(ClientJsonContext.Default.SessionConfig) + ?? throw new InvalidOperationException("Invalid AHP create configuration"); + session = await entry.Options.CreateSession!(new(options, entry.Token)).ConfigureAwait(false); + } + bool released; + lock (_ahpGate) + { + released = entry.Released; + entry.Session = released ? null : session; + } + if (released) + { + NotifyAhpReleased(entry, session); + throw new AhpHandoffEndedException(); + } + if (session is null || session.SessionId != sessionId || !ReferenceEquals(GetSession(sessionId), session)) + throw new InvalidOperationException("AHP callback must return the requested session from this client"); + lock (_ahpGate) + { + var actual = default(JsonElement); + var captured = entry.Configs?.TryGetValue(session, out actual) == true; + entry.Configs = null; + if ((!request.Resume || captured) && !ContainsAhpConfig(actual, request.Config, root: true)) + throw new InvalidOperationException("AHP callback must preserve the supplied session configuration"); + } + return new AhpMaterializeResult(sessionId); + } + finally { entry.FactoryCompleted.TrySetResult(true); } + }); + _ = ObserveAhpFactoryAsync(materialized, cancellationToken); + await Task.WhenAny(materialized, entry.Cancelled.Task).ConfigureAwait(false); + if (materialized.IsCompleted) + { + try { return await materialized.ConfigureAwait(false); } + catch + { + ReleaseAhpSession(request.HostId, request.HandoffId); + throw; + } + } + throw new AhpHandoffEndedException(); + } + + private async Task ObserveAhpFactoryAsync(Task task, CancellationToken cancellationToken) + { + try { await task.ConfigureAwait(false); } + catch (AhpHandoffEndedException) { } + catch (OperationCanceledException error) when (cancellationToken.IsCancellationRequested && + error.CancellationToken == cancellationToken) + { } + catch (Exception error) { _logger.LogError(error, "AHP session factory failed"); } + } + + private static bool ContainsAhpConfig(JsonElement actual, JsonElement expected, bool root = false) + { + if (expected.ValueKind != JsonValueKind.Object) return JsonElement.DeepEquals(actual, expected); + if (actual.ValueKind != JsonValueKind.Object) return false; + foreach (var property in expected.EnumerateObject()) + { + var name = root ? property.Name switch + { + "suppressResumeEvent" => "disableResume", + "enableExperimentalMode" => "isExperimentalMode", + "enableMcpApps" => "requestMcpApps", + _ => property.Name + } : property.Name; + if (!actual.TryGetProperty(name, out var value)) + { + // These default-false flags are omitted by the session request builder. + if (root && property.Value.ValueKind == JsonValueKind.False && + name is "streaming" or "requestMcpApps" or "disableResume") continue; + return false; + } + if (!ContainsAhpConfig(value, property.Value)) return false; + } + return true; + } + + internal sealed record AhpMaterializeRequest(string HostId, string HandoffId, JsonElement Config, bool Resume); + internal sealed record AhpMaterializeResult(string SessionId); + internal sealed record AhpReleaseRequest(string HostId, string HandoffId); + private sealed class AhpHandoffEndedException() : InvalidOperationException("AHP session handoff ended"); + + private sealed class AhpHandoff(string hostId, string sessionId, AhpHostOptions options) + { + public string HostId { get; } = hostId; + public string SessionId { get; } = sessionId; + public AhpHostOptions Options { get; } = options; + public CancellationTokenSource Cancellation { get; } = new(); + public CancellationToken Token => Cancellation.Token; + public TaskCompletionSource Cancelled { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public TaskCompletionSource CancellationCompleted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public TaskCompletionSource FactoryCompleted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public CopilotSession? Session { get; set; } + public Dictionary? Configs { get; set; } = []; + public bool Released { get; set; } + } +} diff --git a/dotnet/src/InstallationConfirmation.cs b/dotnet/src/InstallationConfirmation.cs new file mode 100644 index 0000000000..a2d0b15ad3 --- /dev/null +++ b/dotnet/src/InstallationConfirmation.cs @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Diagnostics.CodeAnalysis; +using RpcInstallationDecision = GitHub.Copilot.Rpc.InstallationDecision; +using RpcInstallationsConfirmRequest = GitHub.Copilot.Rpc.InstallationsConfirmRequest; +using RpcInstallationsConfirmResult = GitHub.Copilot.Rpc.InstallationsConfirmResult; +using RpcInstallationsHandler = GitHub.Copilot.Rpc.IInstallationsHandler; + +namespace GitHub.Copilot; + +/// +/// The cancellable lifetime of one installation confirmation. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class InstallationConfirmationContext +{ + internal InstallationConfirmationContext(CancellationToken cancellationToken) + { + CancellationToken = cancellationToken; + } + + /// + /// Gets a token cancelled when the runtime retires this request or the original connection closes. + /// + public CancellationToken CancellationToken { get; } +} + +/// +/// Collects a fresh human decision for the complete installation review on its original connection. +/// +/// The generated installation confirmation request. +/// The confirmation context, including a token cancelled by request cancellation or connection closure. +/// An explicit confirm, decline or cancel decision. +[Experimental(Diagnostics.Experimental)] +public delegate ValueTask InstallationConfirmationHandler( + RpcInstallationsConfirmRequest request, + InstallationConfirmationContext context); + +[Experimental(Diagnostics.Experimental)] +internal sealed class InstallationConfirmationAdapter(InstallationConfirmationHandler handler) : RpcInstallationsHandler +{ + private readonly InstallationConfirmationHandler _handler = + handler ?? throw new ArgumentNullException(nameof(handler)); + + private CancellationToken _connectionClosed; + + internal void Attach(JsonRpc rpc) + { + ArgumentNullException.ThrowIfNull(rpc); + _connectionClosed = rpc.ConnectionClosedToken; + } + + public async Task ConfirmAsync( + RpcInstallationsConfirmRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + ThrowIfRequestCancelled(cancellationToken); + ThrowIfConnectionClosed(_connectionClosed); + + var confirmationId = request.ConfirmationId; + var reviewFingerprint = request.ReviewFingerprint; + var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectionClosed); + Task? handlerTask = null; + + try + { + var context = new InstallationConfirmationContext(combinedCancellation.Token); + var cancellationSignal = CreateCancellationSignal(combinedCancellation.Token); + + using var cancellationRegistration = RegisterCancellationSignal(cancellationSignal, combinedCancellation.Token); + + handlerTask = InvokeHandlerAsync(request, context); + var completed = await Task.WhenAny( + handlerTask, + cancellationSignal.Task).ConfigureAwait(false); + + ThrowIfRequestCancelled(cancellationToken); + ThrowIfConnectionClosed(_connectionClosed); + + if (ReferenceEquals(completed, cancellationSignal.Task)) + { + throw new InvalidOperationException("Installation confirmation cancelled."); + } + + var decision = await handlerTask.ConfigureAwait(false); + ThrowIfRequestCancelled(cancellationToken); + ThrowIfConnectionClosed(_connectionClosed); + + return new RpcInstallationsConfirmResult + { + ConfirmationId = confirmationId, + ReviewFingerprint = reviewFingerprint, + Decision = IsValidDecision(decision) + ? decision + : throw new InvalidOperationException("Invalid installation confirmation decision."), + }; + } + finally + { + DisposeCancellationWhenHandlerCompletes(combinedCancellation, handlerTask); + } + } + + private async Task InvokeHandlerAsync( + RpcInstallationsConfirmRequest request, + InstallationConfirmationContext context) + { + return await _handler(request, context).ConfigureAwait(false); + } + + private static bool IsValidDecision(RpcInstallationDecision decision) + { + return decision == RpcInstallationDecision.Confirm || + decision == RpcInstallationDecision.Decline || + decision == RpcInstallationDecision.Cancel; + } + + private static TaskCompletionSource CreateCancellationSignal(CancellationToken cancellationToken) + { + var signal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (cancellationToken.IsCancellationRequested) + { + signal.TrySetResult(); + } + + return signal; + } + + private static CancellationTokenRegistration RegisterCancellationSignal( + TaskCompletionSource signal, + CancellationToken cancellationToken) + { + return cancellationToken.CanBeCanceled + ? cancellationToken.Register(static state => ((TaskCompletionSource)state!).TrySetResult(), signal) + : default; + } + + private static void ThrowIfRequestCancelled(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + throw new LocalRpcInvocationException( + -32800, + "Installation confirmation request cancelled"); + } + } + + private static void ThrowIfConnectionClosed(CancellationToken connectionClosed) + { + if (connectionClosed.IsCancellationRequested) + { + throw new InvalidOperationException("Installation confirmation connection closed."); + } + } + + private static void DisposeCancellationWhenHandlerCompletes( + CancellationTokenSource cancellation, + Task? handlerTask) + { + if (handlerTask is not null && !handlerTask.IsCompleted) + { + _ = handlerTask.ContinueWith( + static (task, state) => + { + _ = task.Exception; + ((CancellationTokenSource)state!).Dispose(); + }, + cancellation, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + return; + } + + cancellation.Dispose(); + } +} diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index 889cccab22..8d45eb6f4a 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -28,6 +28,7 @@ namespace GitHub.Copilot; internal sealed partial class JsonRpc : IDisposable { private const int ErrorCodeMethodNotFound = -32601; + private const int ErrorCodeRequestCancelled = -32800; private const int ErrorCodeInternalError = -32603; private const int InitialReadBufferSize = 256; private const int MaximumRetainedReadBufferSize = 1024 * 1024; @@ -37,10 +38,12 @@ internal sealed partial class JsonRpc : IDisposable private readonly JsonSerializerOptions _serializerOptions; private readonly ILogger _logger; private readonly ConcurrentDictionary _pendingRequests = new(); + private readonly ConcurrentDictionary _incomingRequestCancellations = new(); private readonly ConcurrentDictionary _methods = new(); private readonly TaskCompletionSource _completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly SemaphoreSlim _writeLock = new(1, 1); private readonly CancellationTokenSource _disposeCts = new(); + private readonly CancellationTokenSource _connectionClosedCts = new(); private long _nextId; private int _disposeStarted; private Exception? _terminalError; @@ -65,6 +68,11 @@ public JsonRpc(Stream sendStream, Stream receiveStream, JsonSerializerOptions se /// public Task Completion => _completionSource.Task; + /// + /// Cancels when this JSON-RPC connection is disposed or the peer closes the transport. + /// + internal CancellationToken ConnectionClosedToken => _connectionClosedCts.Token; + /// /// Begins reading messages from the receive stream. Call once after registering all method handlers. /// @@ -159,6 +167,90 @@ await SendMessageAsync(new JsonRpcRequest } } + private void HandleCancelRequest(JsonElement message) + { + if (!message.TryGetProperty("params", out var paramsProp) || + paramsProp.ValueKind != JsonValueKind.Object || + !paramsProp.TryGetProperty("id", out var idProp) || + !TryGetNumericRequestId(idProp, out var id)) + { + return; + } + + if (_incomingRequestCancellations.TryGetValue(id, out var requestCancellation)) + { + requestCancellation.Cancel(); + } + } + + private void CancelConnectionClosed() + { + try + { + _connectionClosedCts.Cancel(); + } + catch (AggregateException ex) + { + _logger.LogDebug(ex, "JSON-RPC connection-closed cancellation callback failed"); + } + } + + private IncomingRequestCancellation? CreateIncomingRequestCancellation(JsonElement? requestId) + { + if (!requestId.HasValue || !TryGetNumericRequestId(requestId.Value, out var numericRequestId)) + { + return null; + } + + var requestCancellation = new IncomingRequestCancellation(numericRequestId, _connectionClosedCts.Token); + if (_incomingRequestCancellations.TryAdd(numericRequestId, requestCancellation)) + { + return requestCancellation; + } + + requestCancellation.Dispose(); + return null; + } + + private void RetireIncomingRequestCancellation( + IncomingRequestCancellation requestCancellation, + Task? handlerTask = null) + { + if (_incomingRequestCancellations.TryGetValue(requestCancellation.Id, out var current) && + ReferenceEquals(current, requestCancellation)) + { + _incomingRequestCancellations.TryRemove(requestCancellation.Id, out _); + } + + if (handlerTask is not null && !handlerTask.IsCompleted) + { + _ = handlerTask.ContinueWith( + static (task, state) => + { + _ = task.Exception; + ((IncomingRequestCancellation)state!).Dispose(); + }, + requestCancellation, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + return; + } + + requestCancellation.Dispose(); + } + + private static bool TryGetNumericRequestId(JsonElement idProp, out long id) + { + if (idProp.ValueKind == JsonValueKind.Number && idProp.TryGetInt64(out id)) + { + return true; + } + + id = default; + return false; + } + private void LogInvokeTiming( LogLevel level, Exception? exception, @@ -204,6 +296,7 @@ internal void Dispose(Exception reason) } FailPendingRequests(reason); + CancelConnectionClosed(); try { _disposeCts.Cancel(); @@ -344,6 +437,7 @@ private async Task ReadLoopAsync(CancellationToken cancellationToken) finally { FailPendingRequests(new ConnectionLostException()); + CancelConnectionClosed(); _completionSource.TrySetResult(); } } @@ -534,6 +628,12 @@ private async Task HandleIncomingMethodAsync(string methodName, JsonElement mess { try { + if (string.Equals(methodName, "$/cancelRequest", StringComparison.Ordinal)) + { + HandleCancelRequest(message); + return; + } + JsonElement? requestId = null; if (message.TryGetProperty("id", out var idProp)) { @@ -551,23 +651,63 @@ private async Task HandleIncomingMethodAsync(string methodName, JsonElement mess } message.TryGetProperty("params", out var paramsProp); + var requestCancellation = CreateIncomingRequestCancellation(requestId); + var requestCancellationRetired = false; try { // Materialize arguments before the first possible suspension so none of // them borrow from the JsonDocument owned by the read loop. - var invokeArgs = DeserializeHandlerArguments(registration, paramsProp, cancellationToken); - var result = await InvokeHandlerAsync(registration, invokeArgs).ConfigureAwait(false); + var requestCancellationToken = requestCancellation?.Token ?? _connectionClosedCts.Token; + var invokeArgs = DeserializeHandlerArguments(registration, paramsProp, requestCancellationToken); + var handlerTask = InvokeHandlerAsync(registration, invokeArgs).AsTask(); + + if (requestId.HasValue && requestCancellation is not null) + { + var completed = await Task.WhenAny(handlerTask, requestCancellation.RequestCancelled).ConfigureAwait(false); + if (ReferenceEquals(completed, requestCancellation.RequestCancelled)) + { + await SendErrorResponseAsync( + requestId.Value, + ErrorCodeRequestCancelled, + "Request cancelled", + cancellationToken).ConfigureAwait(false); + RetireIncomingRequestCancellation(requestCancellation, handlerTask); + requestCancellationRetired = true; + return; + } + } + + var result = await handlerTask.ConfigureAwait(false); if (requestId.HasValue) { - await SendResultResponseAsync( - requestId.Value, - result, - registration.ResultType, - cancellationToken).ConfigureAwait(false); + if (requestCancellation?.IsRequestCancellationRequested == true) + { + await SendErrorResponseAsync( + requestId.Value, + ErrorCodeRequestCancelled, + "Request cancelled", + cancellationToken).ConfigureAwait(false); + } + else + { + await SendResultResponseAsync( + requestId.Value, + result, + registration.ResultType, + cancellationToken).ConfigureAwait(false); + } } } + catch (OperationCanceledException) when (requestCancellation?.IsRequestCancellationRequested == true) + { + await SendErrorResponseAsync( + requestId.GetValueOrDefault(), + ErrorCodeRequestCancelled, + "Request cancelled", + cancellationToken).ConfigureAwait(false); + } catch (Exception ex) when (ex is not OperationCanceledException) { // `InvokeHandlerAsync` dispatches handlers via reflection @@ -594,6 +734,13 @@ await SendResultResponseAsync( } } } + finally + { + if (requestCancellation is not null && !requestCancellationRetired) + { + RetireIncomingRequestCancellation(requestCancellation); + } + } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -705,15 +852,22 @@ await SendResultResponseAsync( { var result = registration.Handler.DynamicInvoke(invokeArgs); - // Handlers return one of: a synchronous value, Task (void async), or ValueTask. + // Handlers return a synchronous value, Task, ValueTask, or ValueTask. if (result is Task task) { // Task handlers are not supported — use ValueTask for results. - Debug.Assert(!task.GetType().IsGenericType, "Task handlers are not supported; use ValueTask."); + // An async Task method can return a generic runtime state-machine box. + Debug.Assert(registration.Handler.Method.ReturnType == typeof(Task), "Task handlers are not supported; use ValueTask."); await task.ConfigureAwait(false); return null; } + if (result is ValueTask valueTask) + { + await valueTask.ConfigureAwait(false); + return null; + } + if (result is not null && registration.ValueTaskAsTaskMethod is { } valueTaskAsTaskMethod) { var asTask = (Task)valueTaskAsTaskMethod.Invoke(result, null)!; @@ -875,6 +1029,52 @@ private sealed class PendingRequest(Action? onResultInline = null) public Action? OnResultInline { get; } = onResultInline; } + private sealed class IncomingRequestCancellation : IDisposable + { + private readonly CancellationTokenSource _requestSource = new(); + private readonly CancellationTokenSource _combinedSource; + private readonly TaskCompletionSource _requestCancelled = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationTokenRegistration _registration; + + public IncomingRequestCancellation(long id, CancellationToken connectionClosedToken) + { + Id = id; + _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; } + + public CancellationToken Token => _combinedSource.Token; + + public Task RequestCancelled => _requestCancelled.Task; + + public bool IsRequestCancellationRequested => _requestSource.IsCancellationRequested; + + public void Cancel() + { + try + { + _requestSource.Cancel(); + } + catch (ObjectDisposedException) + { + Debug.WriteLine("JSON-RPC incoming request cancellation raced with request disposal."); + } + } + + public void Dispose() + { + _registration.Dispose(); + _combinedSource.Dispose(); + _requestSource.Dispose(); + } + } + private static readonly MethodInfo s_taskGetResult = typeof(Task<>).GetProperty(nameof(Task.Result), BindingFlags.Instance | BindingFlags.Public)!.GetMethod!; private static readonly MethodInfo s_valueTaskAsTask = typeof(ValueTask<>).GetMethod(nameof(ValueTask.AsTask), BindingFlags.Instance | BindingFlags.Public)!; diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index ce50859571..88d00cde24 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -58,7 +58,7 @@ namespace GitHub.Copilot; /// public sealed partial class CopilotSession : IAsyncDisposable { - private readonly Dictionary _toolHandlers = []; + private IReadOnlyDictionary _toolHandlers = new Dictionary(StringComparer.Ordinal); private readonly Dictionary> _commandHandlers = []; private readonly Dictionary>> _bearerTokenProviders = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _pendingExternalTools = new(StringComparer.Ordinal); @@ -73,12 +73,14 @@ public sealed partial class CopilotSession : IAsyncDisposable private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; private volatile Func>? _autoModeSwitchHandler; + private volatile ISkillProvider? _skillProvider; private ImmutableArray _eventHandlers = ImmutableArray.Empty; private sealed record EventSubscription(Type EventType, Action Handler, bool RootAgentOnly); private SessionHooks? _hooks; private readonly SemaphoreSlim _hooksLock = new(1, 1); + private readonly SemaphoreSlim _setToolsLock = new(1, 1); private Dictionary>>? _transformCallbacks; private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1); @@ -125,6 +127,9 @@ private sealed record EventSubscription(Type EventType, Action Han /// public string? WorkspacePath { get; internal set; } + /// Details of transcript repair reported by session.resume, if any. + public TranscriptRecoveryReport? TranscriptRecovery { get; internal set; } + /// /// Gets the capabilities reported by the host for this session. /// @@ -229,6 +234,7 @@ internal void CloseEventChannel() /// internal void Unregister() { + ClearSkillProvider(); CancelPendingExternalTools(); CloseEventChannel(); RemoveFromClient(); @@ -602,14 +608,152 @@ private async Task ProcessEventsAsync() /// internal void RegisterTools(ICollection tools) { - _toolHandlers.Clear(); + Volatile.Write(ref _toolHandlers, BuildToolHandlerMap(tools)); + } + + private static Dictionary BuildToolHandlerMap(ICollection tools) + { + var handlers = new Dictionary(StringComparer.Ordinal); foreach (var tool in tools) { if (tool.GetService() is { } function) { - _toolHandlers.Add(tool.Name, function); + handlers.Add(tool.Name, function); } } + + return handlers; + } + + private static ProtocolExternalToolDefinition ToProtocolExternalToolDefinition(AIFunctionDeclaration function) + { + // Start from the create/resume definition so every path sends the same tool fields. + var definition = CopilotClient.ToolDefinition.FromAIFunction(function); + return new ProtocolExternalToolDefinition + { + Name = definition.Name, + // session.tools.set requires a description string. + Description = definition.Description ?? string.Empty, + Parameters = ToJsonElementDictionary(definition.Parameters), + OverridesBuiltInTool = definition.OverridesBuiltInTool, + SkipPermission = definition.SkipPermission, + Defer = definition.Defer switch + { + CopilotToolDefer.Auto => ProtocolExternalToolDefer.Auto, + CopilotToolDefer.Never => ProtocolExternalToolDefer.Never, + _ => (ProtocolExternalToolDefer?)null, + }, + Metadata = definition.Metadata is { } metadata ? ToJsonElementDictionary(metadata) : null, + IsTerminal = definition.IsTerminal, + }; + } + + private static Dictionary? ToJsonElementDictionary(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + { + return null; + } + + var properties = new Dictionary(StringComparer.Ordinal); + foreach (var property in element.EnumerateObject()) + { + properties.Add(property.Name, property.Value.Clone()); + } + + return properties; + } + + private static Dictionary ToJsonElementDictionary(IDictionary nodes) + { + var properties = new Dictionary(StringComparer.Ordinal); + foreach (var (name, value) in nodes) + { + using var document = JsonDocument.Parse(value?.ToJsonString() ?? "null"); + properties.Add(name, document.RootElement.Clone()); + } + + return properties; + } + + /// + /// Replaces the complete custom tool list supplied by this SDK client for the live session. + /// + /// + /// The same tool declarations accepted by when creating or resuming a session. + /// Passing an empty collection removes all externally implemented tools supplied by this client. + /// + /// + /// A token that cancels the call. Cancelling while an earlier replacement is still in flight sends nothing. Once the + /// request is sent, cancelling only stops the wait: an accepted replacement still installs its handlers. + /// + /// A task that completes after the runtime accepts the replacement. + /// + /// + /// This is a complete replacement for this client's externally implemented tools only. Built-in tools, MCP/plugin tools, + /// extension-discovered tools, subagent tools, and tools supplied by other connections are unchanged. The wire definitions + /// are the same as the tools supplied when creating or resuming a session. + /// + /// + /// Tool handlers switch when the runtime's acceptance response arrives, before subsequent tool requests are dispatched. + /// Tool calls already running finish with the handlers that started them. If the runtime rejects the replacement, the + /// previous handlers remain installed and the exception is propagated. Concurrent calls are applied in order. + /// + /// + /// The agent sees the new tools from its next model request, which can fall within a turn in progress. A model request + /// already in flight was made with the previous tools, so the agent can still call a tool you removed. This session doesn't + /// answer that call, and it can stay pending until the turn is aborted. If a running turn might still call a tool you remove, + /// replace tools while the session is idle. + /// + /// + [Experimental(Diagnostics.Experimental)] + public async Task SetToolsAsync(ICollection tools, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(tools); + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + var wireTools = tools.Select(ToProtocolExternalToolDefinition).ToList(); + var handlers = BuildToolHandlerMap(tools); + + // Cancelling before sending the request leaves handlers unchanged. Once sent, the request runs + // to completion even if the caller stops waiting, so an accepted replacement still installs its handlers. + var replacement = ReplaceToolsAsync(wireTools, handlers, cancellationToken); + try + { + await replacement.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + _ = replacement.ContinueWith( + static completed => _ = completed.Exception, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); + throw; + } + } + + private async Task ReplaceToolsAsync( + List wireTools, + Dictionary handlers, + CancellationToken lockCancellationToken) + { + await _setToolsLock.WaitAsync(lockCancellationToken); + try + { + // SemaphoreSlim can grant a released slot while its cancellation continuation is pending. + lockCancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var request = new ToolsSetRequest { SessionId = SessionId, Tools = wireTools }; + await CopilotClient.InvokeRpcAsync( + JsonRpc, "session.tools.set", [request], null, CancellationToken.None, + onResponseInline: _ => Volatile.Write(ref _toolHandlers, handlers)); + } + finally + { + _setToolsLock.Release(); + } } /// @@ -619,7 +763,8 @@ internal void RegisterTools(ICollection tools) /// The tool if found; otherwise, null. internal AIFunction? GetTool(string name) { - return _toolHandlers.TryGetValue(name, out var tool) ? tool : null; + var handlers = Volatile.Read(ref _toolHandlers); + return handlers.TryGetValue(name, out var tool) ? tool : null; } /// @@ -1061,6 +1206,17 @@ await Rpc.Tools.HandlePendingToolCallAsync( static string GetSingleParameterName(AIFunction tool) { + if (tool.Name == "apply_patch" && + tool.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var isOverride) && + isOverride is true && + tool.JsonSchema.TryGetProperty("type", out var schemaType) && + schemaType.ValueKind == JsonValueKind.String && + schemaType.GetString() == "string") + { + // AIFunction uses named arguments even when the override declares a scalar schema. + return "input"; + } + if (tool.JsonSchema.TryGetProperty("properties", out var properties) && properties.ValueKind == JsonValueKind.Object) { @@ -1252,6 +1408,48 @@ internal void RegisterAutoModeSwitchHandler(Func + /// Registers the session-scoped skill provider callback. + /// + internal void RegisterSkillProvider(ISkillProvider? provider) + { + _skillProvider = provider; + } + + internal void ClearSkillProvider() => _skillProvider = null; + + internal async ValueTask HandleSkillProviderListAsync(CancellationToken cancellationToken) + { + var provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}"); + + try + { + var skills = await provider.ListSkillsAsync(cancellationToken).ConfigureAwait(false); + return new CopilotClient.SkillProviderListResult(skills?.ToList() ?? []); + } + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + { + LogSkillProviderFailed(ex, "listSkills", SessionId); + throw new InvalidOperationException("Skill provider listSkills failed", ex); + } + } + + internal async ValueTask HandleSkillProviderReadAsync(string name, CancellationToken cancellationToken) + { + var provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}"); + + try + { + var markdown = await provider.ReadSkillAsync(name, cancellationToken).ConfigureAwait(false); + return new CopilotClient.SkillProviderReadResult(markdown); + } + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + { + LogSkillProviderFailed(ex, "readSkill", SessionId); + throw new InvalidOperationException("Skill provider readSkill failed", ex); + } + } + /// /// Registers per-provider BearerTokenProvider callbacks for BYOK /// providers configured with managed-identity / on-demand bearer-token auth. @@ -1864,6 +2062,16 @@ internal void RegisterHooks(SessionHooks hooks) JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.AgentStopHookInput)!, invocation) : null, + "subagentStart" => hooks.OnSubagentStart != null + ? await hooks.OnSubagentStart( + JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.SubagentStartHookInput)!, + invocation) + : null, + "subagentStop" => hooks.OnSubagentStop != null + ? await hooks.OnSubagentStop( + JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.SubagentStopHookInput)!, + invocation) + : null, _ => null }; } @@ -2232,6 +2440,7 @@ public async ValueTask DisposeAsync() return; } + ClearSkillProvider(); CancelPendingExternalTools(); CloseEventChannel(); @@ -2260,7 +2469,7 @@ public async ValueTask DisposeAsync() } _eventHandlers = ImmutableInterlocked.InterlockedExchange(ref _eventHandlers, ImmutableArray.Empty); - _toolHandlers.Clear(); + Volatile.Write(ref _toolHandlers, new Dictionary(StringComparer.Ordinal)); _commandHandlers.Clear(); _permissionHandler = null; @@ -2268,6 +2477,7 @@ public async ValueTask DisposeAsync() _elicitationHandler = null; _exitPlanModeHandler = null; _autoModeSwitchHandler = null; + _skillProvider = null; } [LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in broadcast event handler")] @@ -2285,6 +2495,9 @@ public async ValueTask DisposeAsync() [LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")] private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId); + [LoggerMessage(Level = LogLevel.Warning, Message = "Skill provider {Operation} failed. SessionId={SessionId}")] + private partial void LogSkillProviderFailed(Exception exception, string operation, string sessionId); + internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; @@ -2370,6 +2583,10 @@ internal void ThrowIfDisposed() [JsonSerializable(typeof(SessionEndHookOutput))] [JsonSerializable(typeof(SessionStartHookInput))] [JsonSerializable(typeof(SessionStartHookOutput))] + [JsonSerializable(typeof(SubagentStartHookInput))] + [JsonSerializable(typeof(SubagentStartHookOutput))] + [JsonSerializable(typeof(SubagentStopHookInput))] + [JsonSerializable(typeof(SubagentStopHookOutput))] [JsonSerializable(typeof(SystemMessageTransformRpcResponse))] [JsonSerializable(typeof(SystemMessageTransformSection))] [JsonSerializable(typeof(Attachment))] diff --git a/dotnet/src/SessionEventJsonConverter.cs b/dotnet/src/SessionEventJsonConverter.cs new file mode 100644 index 0000000000..6b4e9a58a6 --- /dev/null +++ b/dotnet/src/SessionEventJsonConverter.cs @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Buffers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +#if !NETSTANDARD2_0 +using System.Text.Unicode; +#endif + +namespace GitHub.Copilot; + +internal sealed partial class SessionEventJsonConverter +{ + private static readonly Encoding StrictUtf8 = new UTF8Encoding(false, true); + + internal static SessionEvent Deserialize(string json) + { +#if !NETSTANDARD2_0 + const int StackallocThreshold = 512; +#endif + ArgumentNullException.ThrowIfNull(json); + + byte[]? rented = null; + Span buffer = +#if !NETSTANDARD2_0 + json.Length <= StackallocThreshold / 3 ? stackalloc byte[StackallocThreshold] : +#endif + rented = ArrayPool.Shared.Rent(GetUtf8ByteCount(json)); + try + { +#if NETSTANDARD2_0 + int byteCount = StrictUtf8.GetBytes(json, 0, json.Length, rented, 0); +#else + var status = Utf8.FromUtf16(json.AsSpan(), buffer, out _, out int byteCount, replaceInvalidSequences: false); + if (status != OperationStatus.Done) + { + throw new ArgumentException($"Cannot transcode UTF-16 JSON text to UTF-8 ({status}).", nameof(json)); + } +#endif + ReadOnlySpan utf8 = buffer.Slice(0, byteCount); + var reader = new Utf8JsonReader(utf8); + + // Select metadata without entering the converter: reader-based nested deserialization + // would scan the entire object again to scope its input before parsing it. + JsonTypeInfo typeInfo; + try + { + reader.Read(); + typeInfo = ReadEventTypeInfo(ref reader); + } + catch (JsonException ex) when (ex.GetType() != typeof(JsonException)) + { + // Match JsonSerializer's normalization of reader errors. + throw new JsonException(ex.Message, ex.Path, ex.LineNumber, ex.BytePositionInLine, ex); + } + return ToSessionEvent(JsonSerializer.Deserialize(utf8, typeInfo)); + } + finally + { + if (rented is not null) + { + buffer.Clear(); + ArrayPool.Shared.Return(rented); + } + } + } + + private static int GetUtf8ByteCount(string json) + { + try + { + return StrictUtf8.GetByteCount(json); + } + catch (EncoderFallbackException ex) + { + throw new ArgumentException("Cannot transcode invalid UTF-16 JSON text to UTF-8.", nameof(json), ex); + } + } + + private static string ReadString(ref Utf8JsonReader reader) + { + try + { + return reader.GetString()!; + } + catch (InvalidOperationException ex) + { + throw new JsonException(ex.Message, ex); + } + } +} diff --git a/dotnet/src/SessionFsProvider.cs b/dotnet/src/SessionFsProvider.cs index a353c93ad0..9aaf25f4b7 100644 --- a/dotnet/src/SessionFsProvider.cs +++ b/dotnet/src/SessionFsProvider.cs @@ -73,6 +73,22 @@ public interface ISessionFsSqliteProvider Task ExistsAsync(CancellationToken cancellationToken); } +/// Optional interface for providers that support exact binary file reads and writes. +public interface ISessionFsBinaryProvider +{ + /// Reads exact bytes of a file in the session filesystem. + /// Path to read. + /// Cancellation token. + Task ReadFileBytesAsync(string path, CancellationToken cancellationToken); + + /// Writes exact bytes of a file in the session filesystem. + /// Path to write. + /// Exact file bytes. + /// Optional POSIX-style permission mode. + /// Cancellation token. + Task WriteFileBytesAsync(string path, ReadOnlyMemory content, int? mode, CancellationToken cancellationToken); +} + /// /// Optional capability for session filesystem providers that support atomic SQLite transactions. /// @@ -119,73 +135,92 @@ public SessionFsSqliteTransactionException( public SessionFsSqliteTransactionErrorClass ErrorClass { get; } } +/// Thrown from a provider's WriteFileAsync when the failed write changed its target. +public sealed class SessionFsWriteException : Exception +{ + /// Initializes a failed write that changed its target. + /// Human-readable failure description. + /// Optional underlying exception. + public SessionFsWriteException(string message, Exception? innerException = null) + : base(message, innerException) + { + } +} + /// /// Base class for session filesystem providers. Subclasses override the /// virtual methods and use normal C# patterns (return values, throw exceptions). /// The base class catches exceptions and converts them to -/// results expected by the runtime. +/// results expected by the runtime. Existence checks instead return false if the provider throws. /// To add SQLite support, also implement . /// +/// +/// Paths use the configured and are passed to the provider unchanged. +/// Absolute paths refer to the provider's filesystem namespace, not necessarily the runtime host's filesystem. +/// public abstract class SessionFsProvider : ISessionFsHandler { + private const int MaxBinaryBytes = (64 * 1024 * 1024 - 1024) / 4 * 3; + private const int MaxBinaryContentLength = (MaxBinaryBytes + 2) / 3 * 4; + /// Reads the full content of a file. Throw if the file does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. /// The file content as a UTF-8 string. protected abstract Task ReadFileAsync(string path, CancellationToken cancellationToken); - /// Writes content to a file, creating it (and parent directories) if needed. - /// SessionFs-relative path. + /// Writes content to a file, creating it (and parent directories) if needed. Throw if a failed write changed the target. + /// Absolute or relative path in the provider's filesystem namespace. /// Content to write. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Appends content to a file, creating it (and parent directories) if needed. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Content to append. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Checks whether a path exists. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. /// true if the path exists, false otherwise. protected abstract Task ExistsAsync(string path, CancellationToken cancellationToken); /// Gets metadata about a file or directory. Throw if the path does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task StatAsync(string path, CancellationToken cancellationToken); - /// Creates a directory (and optionally parents). Does not fail if it already exists. - /// SessionFs-relative path. + /// Creates a directory. If is true, creates parent directories as needed. + /// Absolute or relative path in the provider's filesystem namespace. /// Whether to create parent directories. /// Optional POSIX-style permission mode (e.g., 0x1FF for 0777). Null means use OS default. /// Cancellation token. protected abstract Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken); /// Lists entry names in a directory. Throw if the directory does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken); /// Lists entries with type info in a directory. Throw if the directory does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken); /// Removes a file or directory. Throw if the path does not exist (unless is true). - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Whether to remove directory contents recursively. /// If true, do not throw when the path does not exist. /// Cancellation token. protected abstract Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken); /// Renames/moves a file or directory. - /// Source path. - /// Destination path. + /// Absolute or relative source path in the provider's filesystem namespace. + /// Absolute or relative destination path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken); @@ -206,6 +241,37 @@ async Task ISessionFsHandler.ReadFileAsync(SessionFsRea } } + async Task ISessionFsHandler.ReadFileBytesAsync(SessionFsReadFileBytesRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + try + { + if (this is not ISessionFsBinaryProvider provider) + { + throw new NotSupportedException("Binary reads are not supported by this provider."); + } + var bytes = await provider.ReadFileBytesAsync(request.Path, cancellationToken).ConfigureAwait(false); + if (bytes.Length > MaxBinaryBytes) + { + return new SessionFsReadFileBytesResult + { + Content = string.Empty, + Error = new SessionFsError + { + Code = SessionFsErrorCode.UNKNOWN, + Message = "sessionFs.readFileBytes content exceeds the binary read limit" + } + }; + } + return new SessionFsReadFileBytesResult { Content = Convert.ToBase64String(bytes) }; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + return new SessionFsReadFileBytesResult { Content = string.Empty, Error = ToSessionFsError(ex) }; + } + } + async Task ISessionFsHandler.WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); @@ -216,6 +282,39 @@ async Task ISessionFsHandler.ReadFileAsync(SessionFsRea return null; } catch (Exception ex) + { + var error = ToSessionFsError(ex); + if (ex is SessionFsWriteException) + { + error.WriteChanged = true; + } + return error; + } + } + + async Task ISessionFsHandler.WriteFileBytesAsync(SessionFsWriteFileBytesRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + try + { + if (this is not ISessionFsBinaryProvider provider) + { + throw new NotSupportedException("Binary writes are not supported by this provider."); + } + if (request.Content.Length > MaxBinaryContentLength) + { + return new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "sessionFs.writeFileBytes content exceeds the binary write limit" }; + } + var content = Convert.FromBase64String(request.Content); + if (Convert.ToBase64String(content) != request.Content) + { + return new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "invalid sessionFs.writeFileBytes base64 content" }; + } + await provider.WriteFileBytesAsync(request.Path, content, (int?)request.Mode, cancellationToken).ConfigureAwait(false); + return null; + } + catch (Exception ex) when (ex is not OperationCanceledException) { return ToSessionFsError(ex); } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 100ff55ed4..82a291e400 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -52,6 +52,74 @@ internal static class Diagnostics internal const string Experimental = "GHCP001"; } +/// +/// Describes a skill supplied by a session-scoped . +/// +/// +/// Experimental. The runtime validates descriptor limits, name syntax, and +/// case-insensitive uniqueness when it reads the provider catalog. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class SkillProviderDescriptor +{ + /// Stable skill name. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Human-readable description of what the skill teaches the model. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// + /// Whether users can invoke the skill explicitly. When unset, the runtime default is . + /// + [JsonPropertyName("userInvocable")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? UserInvocable { get; set; } + + /// + /// Whether model-initiated invocation is disabled. When unset, the runtime default is . + /// + [JsonPropertyName("disableModelInvocation")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DisableModelInvocation { get; set; } + + /// Optional argument hint shown for explicit invocations. + [JsonPropertyName("argumentHint")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ArgumentHint { get; set; } +} + +/// +/// Provides session-scoped skills to the Copilot runtime. +/// +/// +/// Experimental. Implementations can be called concurrently and must be safe for +/// concurrent use. The provider is not persisted; supply it again when resuming a +/// session. Returning from +/// reports that the requested skill was not found. +/// +[Experimental(Diagnostics.Experimental)] +public interface ISkillProvider +{ + /// Lists the skills currently available from this provider. + /// + /// Cancels when the runtime abandons the request (for example, on timeout, session disposal, + /// or provider replacement on resume) or the connection closes. + /// + /// The available skill descriptors. + Task> ListSkillsAsync(CancellationToken cancellationToken); + + /// Reads the SKILL.md markdown for a named skill. + /// The skill name requested by the runtime. + /// + /// Cancels when the runtime abandons the request (for example, on timeout, session disposal, + /// or provider replacement on resume) or the connection closes. + /// + /// The skill markdown, or when the skill is not found. + Task ReadSkillAsync(string name, CancellationToken cancellationToken); +} + /// /// Log level for the Copilot runtime. Use the well-known values exposed as /// static members (, , , @@ -320,6 +388,7 @@ private CopilotClientOptions(CopilotClientOptions? other) SessionFs = other.SessionFs; RequestHandler = other.RequestHandler; ExtensionLaunchProvider = other.ExtensionLaunchProvider; + InstallationConfirmationHandler = other.InstallationConfirmationHandler; OnGitHubTelemetry = other.OnGitHubTelemetry; SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds; EnableRemoteSessions = other.EnableRemoteSessions; @@ -442,6 +511,13 @@ private CopilotClientOptions(CopilotClientOptions? other) [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public IExtensionLaunchProviderHandler? ExtensionLaunchProvider { get; set; } + /// + /// Connection-global human review for experimental installation operations. + /// Does not register or enable installation capabilities on the runtime. + /// + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + public InstallationConfirmationHandler? InstallationConfirmationHandler { get; set; } + /// /// Experimental. Receives GitHub telemetry events the runtime forwards to this /// connection; setting a handler opts created/resumed sessions into forwarding. @@ -2044,6 +2120,165 @@ public sealed class AgentStopHookOutput public string? Reason { get; set; } } +/// +/// Input for a subagent-start hook, before the sub-agent's first turn. +/// +public sealed class SubagentStartHookInput +{ + /// + /// The runtime session ID of the parent session. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Unix timestamp in milliseconds when the hook was triggered. + /// + [JsonPropertyName("timestamp")] + [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] + public DateTimeOffset Timestamp { get; set; } + + /// + /// Current working directory of the parent session. + /// + [JsonPropertyName("cwd")] + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Path to the parent session's on-disk transcript, or empty if unavailable. + /// + [JsonPropertyName("transcriptPath")] + public string TranscriptPath { get; set; } = string.Empty; + + /// + /// Name of the sub-agent being started. + /// + [JsonPropertyName("agentName")] + public string AgentName { get; set; } = string.Empty; + + /// + /// Display name of the sub-agent, when available. + /// + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } + + /// + /// Description of the sub-agent, when available. + /// + [JsonPropertyName("agentDescription")] + public string? AgentDescription { get; set; } +} + +/// +/// Output for a subagent-start hook. +/// +public sealed class SubagentStartHookOutput +{ + /// + /// Additional context prepended to the sub-agent's initial prompt. + /// + [JsonPropertyName("additionalContext")] + public string? AdditionalContext { get; set; } +} + +/// +/// Input for a subagent-stop hook, after a sub-agent turn completes. +/// +public sealed class SubagentStopHookInput +{ + /// + /// The runtime session ID of the parent session. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Unix timestamp in milliseconds when the hook was triggered. + /// + [JsonPropertyName("timestamp")] + [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] + public DateTimeOffset Timestamp { get; set; } + + /// + /// Current working directory of the parent session. + /// + [JsonPropertyName("cwd")] + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Path to the parent session's on-disk transcript, or empty if unavailable. + /// + [JsonPropertyName("transcriptPath")] + public string TranscriptPath { get; set; } = string.Empty; + + /// + /// Name of the sub-agent that stopped. + /// + [JsonPropertyName("agentName")] + public string AgentName { get; set; } = string.Empty; + + /// + /// Display name of the sub-agent, when available. + /// + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } + + /// + /// Description of the sub-agent, when available. + /// + [JsonPropertyName("agentDescription")] + public string? AgentDescription { get; set; } + + /// + /// Identifier of the sub-agent, when available. + /// + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } + + /// + /// Type of the sub-agent that stopped. + /// + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; + + /// + /// Reason the sub-agent stopped, such as "end_turn". + /// + [JsonPropertyName("stopReason")] + public string StopReason { get; set; } = string.Empty; + + /// + /// Last assistant response from the sub-agent. + /// + [JsonPropertyName("response")] + public string Response { get; set; } = string.Empty; +} + +/// +/// Output for a subagent-stop hook. +/// +public sealed class SubagentStopHookOutput +{ + /// + /// Set to "block" to request another sub-agent turn, or "allow" to accept it. + /// Other values fail the sub-agent instead of silently allowing its stop. + /// + [JsonPropertyName("decision")] + public string? Decision { get; set; } + + /// + /// Nonempty instruction required when the stop is blocked; invalid without Decision = "block". + /// + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// + /// Replacement for the sub-agent's final response when the stop is not blocked. + /// + [JsonPropertyName("modifiedResponse")] + public string? ModifiedResponse { get; set; } +} + /// /// Hook handlers configuration for a session. /// @@ -2100,6 +2335,16 @@ public sealed class SessionHooks /// Handler called when the top-level agent reaches a natural stop. /// public Func>? OnAgentStop { get; set; } + + /// + /// Handler called before a sub-agent's first turn. + /// + public Func>? OnSubagentStart { get; set; } + + /// + /// Handler called after a sub-agent turn completes. + /// + public Func>? OnSubagentStop { get; set; } } /// @@ -2276,9 +2521,9 @@ public sealed class SectionOverride public static SystemMessageSection ToolInstructions { get; } = new("tool_instructions"); /// Repository and organization custom instructions. public static SystemMessageSection CustomInstructions { get; } = new("custom_instructions"); - /// Runtime-provided context and instructions (e.g. system notifications, memories, workspace context, mode-specific instructions, content-exclusion policy). + /// Runtime-provided system-prompt context and instructions, such as system notifications, memories, workspace context, and content-exclusion policy. Mode-specific instructions can travel in transition messages instead. public static SystemMessageSection RuntimeInstructions { get; } = new("runtime_instructions"); - /// End-of-prompt instructions: parallel tool calling, persistence, task completion. + /// End-of-prompt instructions: parallel tool calling, persistence, task completion, and configured subagent-model guidance when the task tool is available. public static SystemMessageSection LastInstructions { get; } = new("last_instructions"); /// Gets the underlying string value of this . @@ -2393,6 +2638,15 @@ public sealed class ProviderConfig [JsonPropertyName("transport")] public string? Transport { get; set; } + /// + /// Product serving the model, such as ollama or lm_studio, reported in telemetry + /// as model_provider. Allowed values are openai, anthropic, + /// azure_openai, ollama, lm_studio, foundry_local, and + /// llama_cpp; only affects telemetry. + /// + [JsonPropertyName("modelProvider")] + public string? ModelProvider { get; set; } + /// /// Base URL of the provider's API endpoint. /// @@ -2563,6 +2817,15 @@ public sealed class NamedProviderConfig [JsonPropertyName("wireApi")] public string? WireApi { get; set; } + /// + /// Product serving this provider's models, such as ollama or lm_studio, reported + /// in telemetry as model_provider. Allowed values are openai, anthropic, + /// azure_openai, ollama, lm_studio, foundry_local, and + /// llama_cpp; only affects telemetry. + /// + [JsonPropertyName("modelProvider")] + public string? ModelProvider { get; set; } + /// /// API endpoint URL. /// @@ -3239,6 +3502,15 @@ public sealed class ManagedSettingsPermissions /// Tool-permission patterns that are allowed without prompting. [JsonPropertyName("allow")] public IList? Allow { get; set; } + + /// + /// Closed-world host boundary expressed as Domain(hostname), + /// Domain(IP), or Domain(*.example.com) rules. Schemes, ports, + /// paths, queries, and fragments are rejected. Multiple managed layers + /// intersect their lists. A present empty list denies all hosts. + /// + [JsonPropertyName("limitTo")] + public IList? LimitTo { get; set; } } /// @@ -3377,6 +3649,7 @@ protected SessionConfigBase(SessionConfigBase? other) ExpAssignments = other.ExpAssignments; EnableManagedSettings = other.EnableManagedSettings; ManagedSettings = other.ManagedSettings; + SkillProvider = other.SkillProvider; #pragma warning disable GHCP001 Canvases = other.Canvases is not null ? [.. other.Canvases] : null; RequestCanvasRenderer = other.RequestCanvasRenderer; @@ -3519,6 +3792,20 @@ protected SessionConfigBase(SessionConfigBase? other) /// public bool? EnableSkills { get; set; } + /// + /// Session-scoped skill provider. When set, the runtime can list and read + /// skills by calling back into this SDK host. + /// + /// + /// Experimental. The provider is ephemeral and is never persisted; supply it + /// again when resuming a session. In , + /// set to to make provider + /// skills active. Providers can be called concurrently. + /// + [JsonIgnore] + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + public ISkillProvider? SkillProvider { get; set; } + /// /// Built-in skill names to include in the session. In /// , omitting this option excludes all @@ -4036,6 +4323,15 @@ private SessionConfig(SessionConfig? other) : base(other) public SessionConfig Clone() => new(this); } +/// Details of a transcript repair performed while resuming a session. +/// Path planned for the original transcript backup. +/// Invalid or torn-tail lines affected by repair. +/// Whether the session start event was relocated. +public sealed record TranscriptRecoveryReport( + string PlannedBackupPath, + IList InvalidLineNumbers, + bool SessionStartMoved); + /// /// Configuration options for resuming an existing Copilot session. /// @@ -4054,6 +4350,7 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) SuppressResumeEvent = other.SuppressResumeEvent; ContinuePendingWork = other.ContinuePendingWork; + AllowTranscriptRecovery = other.AllowTranscriptRecovery; OpenCanvases = other.OpenCanvases is not null ? [.. other.OpenCanvases] : null; } @@ -4067,7 +4364,8 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) /// When , instructs the runtime to continue any tool calls /// or permission prompts that were still pending when the session was last suspended. /// When (the default), the runtime treats pending work as - /// interrupted on resume. + /// interrupted on resume. Completed tool results already durably recorded + /// by the runtime are preserved. /// /// For permission requests, the runtime re-emits permission.requested so the /// registered handler can re-prompt; @@ -4077,6 +4375,13 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) /// public bool? ContinuePendingWork { get; set; } + /// + /// Whether to repair a damaged transcript on resume. Defaults to true in + /// all modes. Set false to reject recovery. Recovery can discard a torn tail; + /// inspect after resume. + /// + public bool? AllowTranscriptRecovery { get; set; } + #pragma warning disable GHCP001 /// /// Snapshot of canvases that were already open when the session was suspended. @@ -4664,6 +4969,7 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(SectionOverride))] [JsonSerializable(typeof(SessionMetadata))] [JsonSerializable(typeof(SetForegroundSessionResponse))] +[JsonSerializable(typeof(SkillProviderDescriptor))] [JsonSerializable(typeof(SystemMessageConfig))] [JsonSerializable(typeof(ToolBinaryResult))] [JsonSerializable(typeof(ToolBinaryResultType))] diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 528901d290..1775fb1159 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -58,7 +58,6 @@ COPILOT_CLI_DOWNLOAD_BASE_URL is also honored. --> $(COPILOT_CLI_DOWNLOAD_BASE_URL) - https://github.com/github/copilot-cli/releases/download + +```go +type memorySkillProvider struct{} + +func (memorySkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + return []rpc.SkillProviderDescriptor{{ + Name: "review", + Description: "Review code using the host application's policy", + }}, nil +} + +func (memorySkillProvider) ReadSkill(ctx context.Context, name string) (string, error) { + if name != "review" { + return "", copilot.ErrSkillNotFound + } + return "Review the changes and call out policy violations.", nil +} + +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SkillProvider: memorySkillProvider{}, + // Required only when ClientOptions.Mode is ModeEmpty. + // EnableSkills: copilot.Bool(true), +}) +``` + +`ReadSkill` returns an error for which +`errors.Is(err, copilot.ErrSkillNotFound)` is true when a listed skill no +longer exists. Other errors are reported to the runtime as generic provider +failures so provider error text is not exposed to the model. + +Skill providers are not supported for cloud sessions. Creating a cloud session +with a provider fails before the client connects with: +`Skill providers are not supported for cloud sessions.` + +Provider methods can be called concurrently by the runtime, including from +sub-agents that inherit the root session binding. Implementations must be safe +for concurrent calls and should observe the `context.Context`. The context is +cancelled when the runtime abandons the request: the call times out, the +session disconnects or is deleted, or a resume replaces the provider. It is not +cancelled when the connection closes or the client is force-stopped. + +## Installation confirmation (experimental) + +Set `ClientOptions.InstallationConfirmationHandler` to receive the runtime's +`installations.confirm` callback through `InstallationConfirmationHandler`. The +handler receives the generated `InstallationConfirmationRequest` and an +`InstallationConfirmationContext`, a `context.Context` cancelled when the review +is retired or the original connection closes. It returns only an explicit +`InstallationConfirmationDecision`: `confirm`, `decline` or `cancel`. The SDK +echoes the original challenge and review fingerprint; it never infers approval. + +Match `OperationID` and `PolicySessionID` against the original action on this +exact connection before presenting the complete review. Missing legacy session +metadata does not select a default session. Refuse unknown operations or +incomplete reviews. Concurrent reviews are independent and do not block other +RPCs on the connection. + +The callback context follows the runtime's numeric `$/cancelRequest`, including +runtime-enforced expiry, and is also cancelled for loss of the original +connection or client stop. Any separately spawned UI work must observe that +single context. Dropping an outbound installation or OAuth future does not +cancel that operation. + +Call `client.RPC.MCP.PrepareInstall(...)` before `ApplyInstall(...)`. Register +its inert runtime-issued `OperationID`, original expiry and captured session on +this client before applying. Removal uses `PlanUninstall(...)` then +`ApplyUninstall(...)`; its `OperationID` identifies the operation, while +`PlanHandle` is the one-use removal input. Never interchange them. The +`Installations` namespace exposes `List`, `Recover`, `Status` and `Cancel`. +Control uncertain work using its original connection and operation ID, without +selecting a replacement session or replaying apply. + +Owned OAuth uses `session.RPC.MCP.Oauth().PrepareLogin(...)` to return `LoginID` +before browser, network or cached-reconnect work. Keep that ID with the original +session and `ExpectedInstallationID` for `Login(...)` and `CancelLogin(...)`. +Preparation freezes reauthentication and display options. Dropping the login +future is not a substitute for `CancelLogin(...)`. Manual MCP OAuth retains its +direct `Login(...)` path. + +These methods require a matching runtime and available owned-lifecycle support. +Capability negotiation does not promise availability; preserve typed refusals +instead of falling back to raw configuration writes. Generated presence and +transport tests do not establish live OAuth, activation or cross-process +recovery. + ## Distributing your application with an embedded GitHub Copilot CLI The SDK supports bundling, using Go's `embed` package, the Copilot CLI binary within your application's distribution. @@ -200,7 +306,7 @@ Implemented with pure-Go FFI (via [purego](https://github.com/ebitengine/purego) - `NewClient(options *ClientOptions) *Client` - Create a new client - `Start(ctx context.Context) error` - Start the CLI server -- `Stop() error` - Stop the CLI server +- `Stop() error` - Gracefully stop the CLI server. For an owned stdio process, requests runtime shutdown, closes stdin, and waits up to 10 seconds for host cleanup (including telemetry) and natural exit before falling back to a forced termination. - `ForceStop()` - Forcefully stop without graceful cleanup - `CreateSession(ctx context.Context, config *SessionConfig) (*Session, error)` - Create a new session - `ResumeSession(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume an existing session @@ -246,12 +352,14 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `WorkingDirectory` (string): Working directory for the runtime process (default: current process working directory) - `BaseDirectory` (string): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When empty, the runtime defaults to `~/.copilot`. Ignored with `URIConnection`. This does **not** affect where the Go SDK extracts the embedded CLI binary; use `embeddedcli.Config.Dir` for the extraction/cache location. - `ExtensionLaunchProvider` (ExtensionLaunchProvider): Experimental connection-level resolver for extension launch profiles. `Start` installs the reverse-RPC handler and registers the provider before sessions can be created. +- `InstallationConfirmationHandler` (InstallationConfirmationHandler): Experimental connection-global human review for `installations.confirm`. Receives the typed request, a per-request cancellation context and a separate connection-closed signal, then returns an explicit decision. Does not enable installation capabilities. - `LogLevel` (string): Log level. When empty (default), the runtime uses its own default level (the SDK does not pass `--log-level`). - `Env` ([]string): Environment variables for the runtime process (default: inherits from current process) - `GitHubToken` (string): GitHub token for authentication. When provided, takes priority over other auth methods. - `UseLoggedInUser` (\*bool): Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `URIConnection`. - `EnableRemoteSessions` (bool): Enable remote session support (Mission Control integration). Ignored with `URIConnection`. - `Telemetry` (\*TelemetryConfig): OpenTelemetry configuration for the runtime. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `SessionFS` (\*SessionFSConfig): Custom session filesystem provider configuration. For the `view` tool to read provider-only images, set `Capabilities.Binary` and return a provider implementing `SessionFSBinaryProvider` (`ReadFileBytes` and `WriteFileBytes`) from `CreateSessionFSProvider`. Image reads do not fall back to local files. Binary reads and writes are limited to 50,330,880 bytes before encoding or decoding. **SessionConfig:** @@ -310,6 +418,7 @@ Initial acquisition runs during session creation or resume. Cancellation, provid - `Send(ctx context.Context, options MessageOptions) (string, error)` - Send a message - `On(handler SessionEventHandler) func()` - Subscribe to events (returns unsubscribe function) - `Abort(ctx context.Context) error` - Abort the currently processing message +- `SetTools(ctx context.Context, tools []Tool) error` - Replace this client's live custom tools and handlers for the session. See [changing tools](../docs/features/changing-tools.md) for shared behavior and active-turn limitations. - `GetEvents(ctx context.Context) ([]SessionEvent, error)` - Get event history - `Disconnect() error` - Disconnect the session (releases in-memory resources, preserves disk state) - `UI() *SessionUI` - Interactive UI API for elicitation dialogs @@ -388,6 +497,8 @@ Available section constants: `SectionPreamble`, `SectionIdentity`, `SectionTone` `SectionIdentity` and `SectionToolInstructions` are section _groups_ that target a collection of related sub-sections as a unit. Use `SectionPreamble` to target just the identity preamble without affecting its sibling sub-sections. +`SectionLastInstructions` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a `SectionOverride.Transform` callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SectionRuntimeInstructions` is a separate section: removing it does not remove `SectionLastInstructions`. + Each section override supports five actions: - **`replace`** — Replace the section content entirely @@ -544,6 +655,26 @@ editFile := copilot.DefineTool("edit_file", "Custom file editor with project-spe editFile.OverridesBuiltInTool = true ``` +An explicit `apply_patch` override can declare a root string schema by using a +`string` parameter with `DefineTool`. The model sees a required `input` property, +but the runtime restores the scalar patch text before dispatch. Both the typed +parameter and `ToolInvocation.Arguments` receive a string, not an +`{"input": ...}` object. This example returns trimmed patch text; replace the +handler body with your own patch implementation: + +```go +import "strings" + +applyPatch := copilot.DefineTool("apply_patch", "Apply a patch", + func(patch string, inv copilot.ToolInvocation) (string, error) { + return strings.TrimSpace(patch), nil + }) +applyPatch.OverridesBuiltInTool = true +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `SkipPermission = true` on a tool to allow it to execute without triggering a permission prompt: @@ -898,6 +1029,17 @@ session, err := client.ResumeSession(context.Background(), sessionID, &copilot.R }) ``` +`AllowTranscriptRecovery` controls transcript repair on resume. A nil value omits +the wire field and uses the runtime default (true) in all modes. +Set `copilot.Bool(false)` to reject recovery. +`session.TranscriptRecovery()` returns repair details or nil. The report's +`InvalidLineNumbers` includes any discarded torn-tail lines. On rejection, +use `errors.As(err, &rpcErr)` with `var rpcErr *copilot.RPCError` to inspect +`rpcErr.Code` and `rpcErr.Data` (`invalidLineNumbers`, `sessionStartMoved`); +the original error message remains available. +Disabling recovery still permits adding a missing newline after an intact final +record; it rejects torn tails. + ### Per-Tool Skip Permission To let a specific custom tool bypass the permission prompt entirely, set `SkipPermission = true` on the tool. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. @@ -988,6 +1130,22 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi return nil, nil }, + // Called before a subagent's first turn; context is prepended to its prompt + OnSubagentStart: func(input copilot.SubagentStartHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStartHookOutput, error) { + fmt.Printf("Starting subagent %s\n", input.AgentName) + return &copilot.SubagentStartHookOutput{ + AdditionalContext: "Focus on the requested file.", + }, nil + }, + + // Called after a subagent finishes a turn + OnSubagentStop: func(input copilot.SubagentStopHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStopHookOutput, error) { + fmt.Printf("Subagent %s finished: %s\n", input.AgentType, input.StopReason) + return &copilot.SubagentStopHookOutput{ + ModifiedResponse: copilot.String("Verified: " + input.Response), + }, nil + }, + // Called when an error occurs OnErrorOccurred: func(input copilot.ErrorOccurredHookInput, invocation copilot.HookInvocation) (*copilot.ErrorOccurredHookOutput, error) { fmt.Printf("Error in %s: %s\n", input.ErrorContext, input.Error) @@ -1007,8 +1165,13 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi - `OnUserPromptSubmitted` - Intercept user prompts. Can modify the prompt before processing. - `OnSessionStart` - Run logic when a session starts or resumes. - `OnSessionEnd` - Cleanup or logging when session ends. +- `OnSubagentStart` - Observe a subagent before its first turn and optionally prepend `AdditionalContext` to its prompt. +- `OnSubagentStop` - Observe a completed subagent turn. Return `Decision: "block"` with a non-empty `Reason` to request another turn, **or** `ModifiedResponse: copilot.String("replacement")` to replace its response (including an intentional empty string). +- `OnAgentStop` - Intercept the parent agent's stop; distinct from `OnSubagentStop`. - `OnErrorOccurred` - Handle errors with retry/skip/abort strategies. +Subagent hook inputs identify the **parent** session in `SessionID` and `HookInvocation.SessionID`. Both include a `time.Time` timestamp, `WorkingDirectory` (`cwd`), `TranscriptPath`, and `AgentName`. `AgentDisplayName` and `AgentDescription` may be empty. Stop inputs additionally include an optional `AgentID`, `AgentType`, `StopReason` (`"end_turn"`), and the subagent's `Response`. Registering either hook alone enables hook callbacks for both new and resumed sessions. + ## Commands Register slash-commands that users can invoke from the CLI TUI. When a user types `/deploy production`, the SDK dispatches to your handler and responds via the RPC layer. diff --git a/go/canvas.go b/go/canvas.go index e31598bb1e..0de02e4c38 100644 --- a/go/canvas.go +++ b/go/canvas.go @@ -112,6 +112,11 @@ func CanvasErrorNoHandler() *CanvasError { type CanvasHandler interface { OnOpen(ctx context.Context, c rpc.CanvasProviderOpenRequest) (rpc.CanvasProviderOpenResult, error) OnClose(ctx context.Context, c rpc.CanvasProviderCloseRequest) error + // OnAction handles a non-lifecycle action declared by the canvas. The + // returned value is sent to the model as the invoke_canvas_action tool + // result. To return text and images, as tool handlers do, return a + // ToolResult with BinaryResultsForLLM; any other value is rendered to the + // model as JSON text. OnAction(ctx context.Context, c rpc.CanvasProviderInvokeActionRequest) (any, error) } diff --git a/go/client.go b/go/client.go index 228436de00..1a761612b6 100644 --- a/go/client.go +++ b/go/client.go @@ -34,6 +34,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "log" "net" "net/netip" @@ -41,6 +42,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime/debug" "strconv" "strings" "sync" @@ -59,6 +61,12 @@ import ( // whole-session [ProviderConfig]. Named providers are keyed by their own Name. const defaultBearerTokenProviderName = "default" +type cloudSkillProviderError struct{} + +func (cloudSkillProviderError) Error() string { + return "Skill providers are not supported for cloud sessions." +} + // collectBearerTokenProviders gathers the per-provider [BearerTokenProvider] callbacks // from the singular provider and any named providers, keyed by provider name. The // singular provider uses the implicit name "default"; named providers use their @@ -146,25 +154,29 @@ func validateEnvironmentOptions(connection RuntimeConnection, opts *ClientOption // } // defer client.Stop() type Client struct { - options ClientOptions - process *exec.Cmd - client *jsonrpc2.Client - actualPort int - actualHost string - state connectionState - sessions map[string]*Session - sessionsMux sync.Mutex - gitHubTokenProviders map[string]GitHubTokenProvider - gitHubTokenProvidersMux sync.RWMutex - requestAdapter *copilotRequestAdapter - requestAdapterMux sync.Mutex - sessionOperations map[string]*sessionOperation - sessionOperationsMux sync.Mutex - isExternalServer bool - conn net.Conn // stores net.Conn for external TCP connections - useStdio bool // resolved value from options - useInProcess bool // true for InProcessConnection (FFI transport) - ffiHost inProcessHost + options ClientOptions + process *exec.Cmd + processStdin io.WriteCloser + client *jsonrpc2.Client + actualPort int + actualHost string + state connectionState + sessions map[string]*Session + sessionsMux sync.Mutex + ahp ahpHostState + gitHubTokenProviders map[string]GitHubTokenProvider + gitHubTokenProvidersMux sync.RWMutex + requestAdapter *copilotRequestAdapter + requestAdapterMux sync.Mutex + installationConfirmationAdapter *installationConfirmationAdapter + installationConfirmationAdapterMux sync.Mutex + sessionOperations map[string]*sessionOperation + sessionOperationsMux sync.Mutex + isExternalServer bool + conn net.Conn // stores net.Conn for external TCP connections + useStdio bool // resolved value from options + useInProcess bool // true for InProcessConnection (FFI transport) + ffiHost inProcessHost // resolved process options for the spawned runtime (zero values for URIConnection) cliPath string cliArgs []string @@ -538,8 +550,10 @@ func (c *Client) Start(ctx context.Context) error { } if c.options.SessionFS.Capabilities != nil { sqlite := c.options.SessionFS.Capabilities.Sqlite + binary := c.options.SessionFS.Capabilities.Binary req.Capabilities = &rpc.SessionFSSetProviderCapabilities{ Sqlite: &sqlite, + Binary: &binary, } } _, err := c.RPC.SessionFS.SetProvider(ctx, req) @@ -568,8 +582,8 @@ func (c *Client) Start(ctx context.Context) error { // This method performs graceful cleanup: // 1. Closes all active sessions (releases in-memory resources) // 2. Requests runtime shutdown for SDK-owned CLI processes -// 3. Closes the JSON-RPC connection -// 4. Terminates the CLI server process (if spawned by this client) +// 3. Closes owned stdio input and waits up to 10 seconds for host cleanup and exit +// 4. Terminates any remaining owned CLI process and closes the JSON-RPC connection // // Note: session data on disk is preserved, so sessions can be resumed later. // To permanently remove session data before stopping, call [Client.DeleteSession] @@ -583,6 +597,7 @@ func (c *Client) Start(ctx context.Context) error { // log.Printf("Cleanup error: %v", err) // } func (c *Client) Stop() error { + c.disconnectAhpHosts() var errs []error // Disconnect all active sessions @@ -604,6 +619,7 @@ func (c *Client) Stop() error { c.sessionsMux.Unlock() c.clearGitHubTokenProviders() c.closeCopilotRequestAdapter() + c.closeInstallationConfirmationAdapter() c.startStopMux.Lock() defer c.startStopMux.Unlock() @@ -631,11 +647,24 @@ func (c *Client) Stop() error { } } - // The runtime completes all cleanup before responding to runtime.shutdown - // and then leaves termination to us; it deliberately keeps its JSON-RPC - // server alive to send the response and never self-exits. Waiting for a - // self-exit that will never come just wastes time, so terminate the child - // immediately and only wait to reap it. + // The stdio host finalizes telemetry after EOF, not after runtime.shutdown. + // Keep stdout open while allowing the child to finish that cleanup naturally. + if c.process != nil && !c.isExternalServer && c.processStdin != nil { + processExitStart := time.Now() + if err := c.processStdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + errs = append(errs, fmt.Errorf("failed to close CLI stdin: %w", err)) + } + c.processStdin = nil + select { + case <-c.processDone: + c.logDebugTiming(processExitStart, "CopilotClient.Stop CLI process exited gracefully") + c.osProcess.Store(nil) + c.process = nil + case <-time.After(processExitTimeout): + c.logDebugTiming(processExitStart, "CopilotClient.Stop CLI process exit timed out; killing process") + } + } + if c.process != nil && !c.isExternalServer { if err := c.killProcessAndWait(); err != nil { errs = append(errs, err) @@ -709,6 +738,7 @@ func (c *Client) logDebugTiming(start time.Time, message string) { // client.ForceStop() // } func (c *Client) ForceStop() { + c.disconnectAhpHosts() // Kill the process without waiting for startStopMux, which Start may hold. // This unblocks any I/O Start is doing (connect, version check). if p := c.osProcess.Swap(nil); p != nil { @@ -724,10 +754,12 @@ func (c *Client) ForceStop() { c.sessions = make(map[string]*Session) c.sessionsMux.Unlock() for _, session := range sessions { + session.clearSkillProvider() session.cancelPendingExternalTools() } c.clearGitHubTokenProviders() c.closeCopilotRequestAdapter() + c.closeInstallationConfirmationAdapter() c.startStopMux.Lock() defer c.startStopMux.Unlock() @@ -852,6 +884,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config == nil { config = &SessionConfig{} } + if config.Cloud != nil && config.SkillProvider != nil { + return nil, cloudSkillProviderError{} + } if config.GitHubToken != "" && config.GitHubTokenProvider != nil { return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") } @@ -889,6 +924,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.EnableHostGitOperations = config.EnableHostGitOperations req.EnableSessionStore = config.EnableSessionStore req.EnableSkills = config.EnableSkills + if config.SkillProvider != nil { + req.HasSkillProvider = Bool(true) + } req.Tools = config.Tools systemMessage := c.systemMessageForMode(config.SystemMessage) wireSystemMessage, transformCallbacks := extractTransformCallbacks(systemMessage) @@ -1000,7 +1038,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses config.Hooks.OnSessionStart != nil || config.Hooks.OnSessionEnd != nil || config.Hooks.OnErrorOccurred != nil || - config.Hooks.OnAgentStop != nil) { + config.Hooks.OnAgentStop != nil || + config.Hooks.OnSubagentStart != nil || + config.Hooks.OnSubagentStop != nil) { req.Hooks = Bool(true) } if config.OnPermissionRequest != nil { @@ -1086,6 +1126,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.CanvasHandler != nil { s.registerCanvasHandler(config.CanvasHandler) } + if config.SkillProvider != nil { + s.registerSkillProvider(config.SkillProvider) + } if bearerTokenProviders := collectBearerTokenProviders(config.Provider, config.Providers); bearerTokenProviders != nil { s.registerBearerTokenProviders(bearerTokenProviders) } @@ -1102,6 +1145,12 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return nil, fmt.Errorf("SessionFS capabilities declare SQLite support but the provider does not implement SessionFSSqliteProvider") } } + if c.options.SessionFS.Capabilities != nil && c.options.SessionFS.Capabilities.Binary { + if _, ok := provider.(SessionFSBinaryProvider); !ok { + unregisterSession(sessionID, s) + return nil, fmt.Errorf("SessionFS capabilities declare binary support but the provider does not implement SessionFSBinaryProvider") + } + } s.clientSessionAPIs.SessionFS = newSessionFSAdapter(provider) } @@ -1113,6 +1162,14 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses var session *Session var registeredSessionID string + var orphanedSessionID string + var inlineMu sync.Mutex + var requestFinished bool + deleteOrphan := func(sessionID string) error { + cleanupCtx, cancelCleanup := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) + defer cancelCleanup() + return c.DeleteSession(cleanupCtx, sessionID) + } // Pre-register non-cloud sessions BEFORE issuing the RPC so any // session-scoped requests the CLI emits during session.create processing @@ -1147,19 +1204,48 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return fmt.Errorf("session.create response did not include a sessionId") } s, err := initializeSession(early.SessionID) + inlineMu.Lock() if err != nil { + if requestFinished { + // Cleanup must not wait for an RPC response on the read loop. + go func() { + if cleanupErr := deleteOrphan(early.SessionID); cleanupErr != nil { + log.Printf("failed to delete uninitialized cloud session %s: %v", early.SessionID, cleanupErr) + } + }() + } else { + orphanedSessionID = early.SessionID + } + inlineMu.Unlock() return err } + if requestFinished { + inlineMu.Unlock() + unregisterSession(early.SessionID, s) + return nil + } session = s registeredSessionID = early.SessionID + inlineMu.Unlock() return nil } } result, err := c.client.RequestWithInlineResponse(ctx, "session.create", req, inlineCb) + inlineMu.Lock() + requestFinished = true + cleanupSessionID := orphanedSessionID + sessionToUnregister := session + registeredID := registeredSessionID + inlineMu.Unlock() if err != nil { - if registeredSessionID != "" { - unregisterSession(registeredSessionID, session) + if registeredID != "" { + unregisterSession(registeredID, sessionToUnregister) + } + if cleanupSessionID != "" { + if cleanupErr := deleteOrphan(cleanupSessionID); cleanupErr != nil { + err = errors.Join(err, fmt.Errorf("failed to delete uninitialized cloud session: %w", cleanupErr)) + } } return nil, fmt.Errorf("failed to create session: %w", err) } @@ -1204,6 +1290,10 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return nil, err } + if err := c.captureAhpSession(session, req); err != nil { + unregisterSession(registeredSessionID, session) + return nil, err + } if registrationID != "" { session.setGitHubTokenProviderRegistrationRelease(func() { c.unregisterGitHubTokenProvider(registrationID) @@ -1321,7 +1411,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, config.Hooks.OnSessionStart != nil || config.Hooks.OnSessionEnd != nil || config.Hooks.OnErrorOccurred != nil || - config.Hooks.OnAgentStop != nil) { + config.Hooks.OnAgentStop != nil || + config.Hooks.OnSubagentStart != nil || + config.Hooks.OnSubagentStop != nil) { req.Hooks = Bool(true) } req.WorkingDirectory = config.WorkingDirectory @@ -1336,10 +1428,14 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.EnableHostGitOperations = config.EnableHostGitOperations req.EnableSessionStore = config.EnableSessionStore req.EnableSkills = config.EnableSkills + if config.SkillProvider != nil { + req.HasSkillProvider = Bool(true) + } if config.SuppressResumeEvent { req.DisableResume = Bool(true) } req.ContinuePendingWork = config.ContinuePendingWork + req.AllowTranscriptRecovery = config.AllowTranscriptRecovery req.MCPServers = config.MCPServers req.Diagnostics = config.Diagnostics req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage @@ -1444,6 +1540,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.CanvasHandler != nil { session.registerCanvasHandler(config.CanvasHandler) } + if config.SkillProvider != nil { + session.registerSkillProvider(config.SkillProvider) + } if bearerTokenProviders := collectBearerTokenProviders(config.Provider, config.Providers); bearerTokenProviders != nil { session.registerBearerTokenProviders(bearerTokenProviders) } @@ -1460,6 +1559,12 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, return nil, fmt.Errorf("SessionFS capabilities declare SQLite support but the provider does not implement SessionFSSqliteProvider") } } + if c.options.SessionFS.Capabilities != nil && c.options.SessionFS.Capabilities.Binary { + if _, ok := provider.(SessionFSBinaryProvider); !ok { + session.stopEventProcessing() + return nil, fmt.Errorf("SessionFS capabilities declare binary support but the provider does not implement SessionFSBinaryProvider") + } + } session.clientSessionAPIs.SessionFS = newSessionFSAdapter(provider) } @@ -1511,6 +1616,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } session.workspacePath = response.WorkspacePath + session.transcriptRecovery = response.TranscriptRecovery session.setCapabilities(response.Capabilities) session.setOpenCanvases(response.OpenCanvases) @@ -1525,6 +1631,10 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, return nil, err } + if err := c.captureAhpSession(session, req); err != nil { + restoreReplacedSession() + return nil, err + } if registrationID != "" { session.setGitHubTokenProviderRegistrationRelease(func() { c.unregisterGitHubTokenProvider(registrationID) @@ -1656,6 +1766,7 @@ func (c *Client) DeleteSession(ctx context.Context, sessionID string) error { delete(c.sessions, sessionID) c.sessionsMux.Unlock() if session != nil { + session.clearSkillProvider() session.releaseGitHubTokenProviderRegistration() } @@ -2161,6 +2272,10 @@ func (c *Client) startCLIServer(ctx context.Context) error { c.process.Env = setEnvValue(c.process.Env, "COPILOT_DISABLE_KEYTAR", "1") } + if c.options.Mode != ModeEmpty { + c.process.Env = setEnvValue(c.process.Env, "COPILOT_RUNTIME_PROCESS_FILE_LOGGING", "1") + } + if c.options.Telemetry != nil { t := c.options.Telemetry c.process.Env = setEnvValue(c.process.Env, "COPILOT_OTEL_ENABLED", "true") @@ -2206,6 +2321,7 @@ func (c *Client) startCLIServer(ctx context.Context) error { return fmt.Errorf("failed to start CLI server: %w", err) } + c.processStdin = stdin c.monitorProcess() // Create JSON-RPC client immediately @@ -2388,6 +2504,10 @@ func (c *Client) killProcess() error { return fmt.Errorf("failed to kill CLI process: %w", err) } } + if c.processStdin != nil { + _ = c.processStdin.Close() + c.processStdin = nil + } c.process = nil return nil } @@ -2494,6 +2614,8 @@ func (c *Client) setupNotificationHandler() { c.client.SetRequestHandler("exitPlanMode.request", jsonrpc2.RequestHandlerFor(c.handleExitPlanModeRequest)) c.client.SetRequestHandler("autoModeSwitch.request", jsonrpc2.RequestHandlerFor(c.handleAutoModeSwitchRequest)) c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform)) + c.client.SetRequestContextHandler("skillProvider.list", c.handleSkillProviderList) + c.client.SetRequestContextHandler("skillProvider.read", c.handleSkillProviderRead) rpc.RegisterClientSessionAPIHandlers(c.client, func(sessionID string) *rpc.ClientSessionAPIHandlers { c.sessionsMux.Lock() defer c.sessionsMux.Unlock() @@ -2531,6 +2653,20 @@ func (c *Client) setupNotificationHandler() { handlers.GitHubTelemetry = &gitHubTelemetryAdapter{callback: c.options.OnGitHubTelemetry} } rpc.RegisterClientGlobalAPIHandlers(c.client, handlers) + if c.options.InstallationConfirmationHandler != nil { + adapter := newInstallationConfirmationAdapter(c.options.InstallationConfirmationHandler, c.client.ConnectionClosed()) + c.installationConfirmationAdapterMux.Lock() + previous := c.installationConfirmationAdapter + c.installationConfirmationAdapter = adapter + c.installationConfirmationAdapterMux.Unlock() + if previous != nil { + previous.close() + } + c.client.SetRequestContextHandler("installations.confirm", adapter.handle) + } + c.client.SetRequestHandler("host.materializeSession", jsonrpc2.RequestHandlerFor(c.materializeAhpSession)) + c.client.SetRequestHandler("host.sessionReleased", jsonrpc2.NotificationHandlerFor(c.releaseAhpSession)) + c.client.SetRequestHandler("host.exited", jsonrpc2.NotificationHandlerFor(c.handleAhpExit)) } func (c *Client) registerGitHubTokenProvider(provider GitHubTokenProvider) string { @@ -2563,7 +2699,9 @@ func (c *Client) clearGitHubTokenProviders() { } func (c *Client) handleConnectionClose() { + c.disconnectAhpHosts() c.closeCopilotRequestAdapter() + c.closeInstallationConfirmationAdapter() c.clearGitHubTokenProviders() c.sessionsMux.Lock() sessions := make([]*Session, 0, len(c.sessions)) @@ -2572,6 +2710,7 @@ func (c *Client) handleConnectionClose() { } c.sessionsMux.Unlock() for _, session := range sessions { + session.clearSkillProvider() session.cancelPendingExternalTools() } // Avoid deadlocking with Stop/ForceStop, which hold startStopMux while @@ -2592,6 +2731,16 @@ func (c *Client) closeCopilotRequestAdapter() { } } +func (c *Client) closeInstallationConfirmationAdapter() { + c.installationConfirmationAdapterMux.Lock() + adapter := c.installationConfirmationAdapter + c.installationConfirmationAdapter = nil + c.installationConfirmationAdapterMux.Unlock() + if adapter != nil { + adapter.close() + } +} + func (c *Client) lockSessionOperation(sessionID string) func() { c.sessionOperationsMux.Lock() if c.sessionOperations == nil { @@ -2679,6 +2828,102 @@ func (c *Client) handleSessionEvent(req sessionEventRequest) { } } +func (c *Client) handleSkillProviderList(ctx context.Context, params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var req rpc.SkillProviderListRequest + if err := json.Unmarshal(params, &req); err != nil || req.SessionID == "" { + return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid skill provider list payload"} + } + + provider, rpcErr := c.resolveSkillProvider(req.SessionID) + if rpcErr != nil { + return nil, rpcErr + } + + skills, err := callSkillProvider(ctx, req.SessionID, "listSkills", func() ([]rpc.SkillProviderDescriptor, error) { + return provider.ListSkills(ctx) + }) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "Skill provider listSkills failed"} + } + if skills == nil { + skills = []rpc.SkillProviderDescriptor{} + } + + raw, err := json.Marshal(rpc.SkillProviderListResult{Skills: skills}) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil +} + +func (c *Client) handleSkillProviderRead(ctx context.Context, params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var req rpc.SkillProviderReadRequest + if err := json.Unmarshal(params, &req); err != nil || req.SessionID == "" || req.Name == "" { + return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid skill provider read payload"} + } + + provider, rpcErr := c.resolveSkillProvider(req.SessionID) + if rpcErr != nil { + return nil, rpcErr + } + + // Classify not-found inside the guarded call: errors.Is runs provider + // Is/Unwrap methods, which may panic. + markdown, err := callSkillProvider(ctx, req.SessionID, "readSkill", func() (*string, error) { + markdown, err := provider.ReadSkill(ctx, req.Name) + if errors.Is(err, ErrSkillNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + return &markdown, nil + }) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "Skill provider readSkill failed"} + } + + raw, err := json.Marshal(rpc.SkillProviderReadResult{Markdown: markdown}) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil +} + +func (c *Client) resolveSkillProvider(sessionID string) (SkillProvider, *jsonrpc2.Error) { + c.sessionsMux.Lock() + session := c.sessions[sessionID] + c.sessionsMux.Unlock() + if session == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No skill provider for session: %s", sessionID)} + } + provider := session.getSkillProvider() + if provider == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No skill provider for session: %s", sessionID)} + } + return provider, nil +} + +var errSkillProviderPanic = errors.New("skill provider panicked") + +// callSkillProvider logs provider failures locally and converts panics into +// an error, so the runtime only ever receives a generic failure. +func callSkillProvider[T any](ctx context.Context, sessionID, operation string, call func() (T, error)) (result T, err error) { + defer func() { + if failure := recover(); failure != nil { + log.Printf("skill provider %s panicked: session_id=%s panic=%v\n%s", operation, sessionID, failure, debug.Stack()) + var zero T + result, err = zero, errSkillProviderPanic + } + }() + result, err = call() + // A call the runtime cancelled is expected to fail; it isn't a provider failure. + if err != nil && ctx.Err() == nil { + log.Printf("skill provider %s failed: session_id=%s error=%v", operation, sessionID, err) + } + return result, err +} + // handleUserInputRequest handles a user input request from the CLI server. func (c *Client) handleUserInputRequest(req userInputRequest) (*userInputResponse, *jsonrpc2.Error) { if req.SessionID == "" || req.Question == "" { diff --git a/go/client_shutdown_test.go b/go/client_shutdown_test.go new file mode 100644 index 0000000000..6bcef7c2ef --- /dev/null +++ b/go/client_shutdown_test.go @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot_test + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" +) + +func TestOwnedStdioShutdown(t *testing.T) { + node, err := exec.LookPath("node") + if err != nil { + t.Fatal("shutdown fixture requires Node.js:", err) + } + fixture, err := filepath.Abs("../test/harness/stdio-shutdown-runtime.cjs") + if err != nil { + t.Fatal(err) + } + + for _, mode := range []string{"stop", "force", "fallback", "start-failure"} { + t.Run(mode, func(t *testing.T) { + directory := t.TempDir() + marker := filepath.Join(directory, "telemetry.jsonl") + pidFile := filepath.Join(directory, "runtime.pid") + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.StdioConnection{ + Path: node, + Args: []string{fixture, marker, mode, pidFile}, + }, + UseLoggedInUser: copilot.Bool(false), + }) + t.Cleanup(client.ForceStop) + + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + err := client.Start(ctx) + if mode == "start-failure" { + if err == nil || !strings.Contains(err.Error(), "protocol version") { + t.Fatalf("expected protocol version failure, got %v", err) + } + } else { + if err != nil { + t.Fatal("Start failed:", err) + } + started := time.Now() + if mode == "force" { + runShutdownWithWatchdog(t, func() error { + client.ForceStop() + return nil + }) + if elapsed := time.Since(started); elapsed >= 10*time.Second { + t.Fatalf("ForceStop waited for graceful timeout: %s", elapsed) + } + } else { + runShutdownWithWatchdog(t, client.Stop) + } + if mode == "fallback" && time.Since(started) < 10*time.Second { + t.Fatal("Stop did not allow the full graceful exit timeout") + } + } + + // Force-stop and failed startup kill without waiting for the child to be reaped. + exitWait := "0" + if mode == "force" || mode == "start-failure" { + exitWait = "5000" + } + assertShutdownChildExited(t, node, pidFile, exitWait) + contents, err := os.ReadFile(marker) + if mode == "force" || mode == "start-failure" { + if !os.IsNotExist(err) { + t.Fatalf("forced termination unexpectedly finalized telemetry: %q (error: %v)", contents, err) + } + } else if err != nil || string(contents) != "{\"type\":\"span\"}\n" { + t.Fatalf("Stop returned without EOF cleanup: %q (error: %v)", contents, err) + } + runShutdownWithWatchdog(t, client.Stop) + }) + } +} + +func runShutdownWithWatchdog(t *testing.T, stop func() error) { + t.Helper() + done := make(chan error, 1) + go func() { done <- stop() }() + select { + case err := <-done: + if err != nil { + t.Fatal("shutdown failed:", err) + } + case <-time.After(40 * time.Second): + // Cover shutdown RPC, graceful exit, and forced reap budgets, plus scheduling slack. + t.Fatal("shutdown exceeded all cleanup budgets") + } +} + +func assertShutdownChildExited(t *testing.T, node, pidFile, waitMillis string) { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + // Node's process probe is portable, unlike os.Process.Signal(0) on Windows. + cmd := exec.CommandContext(ctx, node, "-e", ` +const fs = require("node:fs"); +const pid = Number(fs.readFileSync(process.argv[1], "utf8")); +const deadline = Date.now() + Number(process.argv[2]); +function check() { + try { + process.kill(pid, 0); + } catch (error) { + if (error.code === "ESRCH") return; + throw error; + } + if (Date.now() >= deadline) throw new Error("Child still running"); + setTimeout(check, 25); +} +check(); +`, pidFile, waitMillis) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("child process did not exit: %v\n%s", err, output) + } +} diff --git a/go/client_test.go b/go/client_test.go index 9cf8d4e095..0f701a3359 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1,8 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + package copilot import ( + "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -805,6 +809,76 @@ func TestClient_ForwardsAskUserVariantToSessionRequests(t *testing.T) { assertAskUserVariant(t, <-resumeParams, "") } +func TestClient_EnablesSubagentHooksForCreateAndResume(t *testing.T) { + for _, tt := range []struct { + name string + hooks *SessionHooks + }{ + { + name: "start only", + hooks: &SessionHooks{ + OnSubagentStart: func(SubagentStartHookInput, HookInvocation) (*SubagentStartHookOutput, error) { + return nil, nil + }, + }, + }, + { + name: "stop only", + hooks: &SessionHooks{ + OnSubagentStop: func(SubagentStopHookInput, HookInvocation) (*SubagentStopHookOutput, error) { + return nil, nil + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + requests := make(chan json.RawMessage, 2) + for _, method := range []string{"session.create", "session.resume"} { + server.SetRequestHandler(method, func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + requests <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + } + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + created, err := client.CreateSession(t.Context(), &SessionConfig{Hooks: tt.hooks}) + if err != nil { + t.Fatalf("create session: %v", err) + } + if err := created.Disconnect(); err != nil { + t.Fatalf("disconnect session: %v", err) + } + resumed, err := client.ResumeSession(t.Context(), created.SessionID, &ResumeSessionConfig{Hooks: tt.hooks}) + if err != nil { + t.Fatalf("resume session: %v", err) + } + if err := resumed.Disconnect(); err != nil { + t.Fatalf("disconnect resumed session: %v", err) + } + + for _, method := range []string{"session.create", "session.resume"} { + var request map[string]any + if err := json.Unmarshal(<-requests, &request); err != nil { + t.Fatalf("decode %s request: %v", method, err) + } + if request["hooks"] != true { + t.Errorf("%s hooks = %v, want true", method, request["hooks"]) + } + } + }) + } +} + func TestClient_ForwardsDiagnosticsToSessionRequests(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) @@ -1447,6 +1521,63 @@ func (noSQLiteSessionFSProvider) ReadDirectoryWithTypes(string) ([]rpc.SessionFS func (noSQLiteSessionFSProvider) Remove(string, bool, bool) error { return nil } func (noSQLiteSessionFSProvider) Rename(string, string) error { return nil } +type oversizedBinarySessionFSProvider struct{ noSQLiteSessionFSProvider } + +func (oversizedBinarySessionFSProvider) ReadFileBytes(string) ([]byte, error) { + return make([]byte, maxSessionFSBinaryBytes+1), nil +} + +func (oversizedBinarySessionFSProvider) WriteFileBytes(string, []byte, *int) error { return nil } + +type recordingBinarySessionFSProvider struct { + noSQLiteSessionFSProvider + content []byte + mode *int +} + +func (p *recordingBinarySessionFSProvider) ReadFileBytes(string) ([]byte, error) { + return p.content, nil +} +func (p *recordingBinarySessionFSProvider) WriteFileBytes(_ string, content []byte, mode *int) error { + p.content, p.mode = content, mode + return nil +} + +func TestSessionFSAdapterWritesExactBytesAndRejectsInvalidInput(t *testing.T) { + provider := &recordingBinarySessionFSProvider{} + adapter := newSessionFSAdapter(provider) + mode := int64(0o600) + write := &rpc.SessionFSWriteFileBytesRequest{ + SessionID: "session", Path: "/image.png", Content: "AP/+AQ==", Mode: &mode, + } + result, err := adapter.WriteFileBytes(write) + if err != nil || result != nil || !bytes.Equal(provider.content, []byte{0, 255, 254, 1}) || + provider.mode == nil || *provider.mode != 0o600 { + t.Fatalf("binary write lost content/mode: result=%v err=%v bytes=%v mode=%v", result, err, provider.content, provider.mode) + } + for _, content := range []string{"AA==AAAA", "AA==\r\n", strings.Repeat("A", maxSessionFSBinaryContentLength+1)} { + result, err := adapter.WriteFileBytes(&rpc.SessionFSWriteFileBytesRequest{ + SessionID: "session", Path: "/image.png", Content: content, + }) + if err != nil || result == nil || !bytes.Equal(provider.content, []byte{0, 255, 254, 1}) { + t.Fatalf("invalid binary write modified file: result=%v err=%v bytes=%v", result, err, provider.content) + } + } +} + +func TestSessionFSAdapterRejectsOversizedBinaryRead(t *testing.T) { + result, err := newSessionFSAdapter(oversizedBinarySessionFSProvider{}).ReadFileBytes( + &rpc.SessionFSReadFileBytesRequest{SessionID: "session", Path: "/image.png"}, + ) + if err != nil { + t.Fatalf("ReadFileBytes returned a transport error: %v", err) + } + if result.Content != "" || result.Error == nil || result.Error.Code != rpc.SessionFSErrorCodeUNKNOWN || + result.Error.Message == nil || !strings.Contains(*result.Error.Message, "binary read limit") { + t.Fatalf("expected a filesystem error without encoded bytes, got %+v", result) + } +} + func assertRuntimeShutdownNotCalled(t *testing.T, shutdownCalled <-chan struct{}) { t.Helper() select { @@ -1498,6 +1629,113 @@ func TestClient_SessionFSConfig(t *testing.T) { }) } +func TestClient_CreateSessionDeletesCloudSessionWhenBinaryProviderValidationFails(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Binary: true}, + }, + }) + defer cleanup() + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Cloud: &CloudSessionOptions{ + Repository: &CloudSessionRepository{Owner: "github", Name: "copilot-sdk", Branch: "main"}, + }, + OnPermissionRequest: PermissionHandler.ApproveAll, + CreateSessionFSProvider: func(*Session) SessionFSProvider { + return nil + }, + }) + if err == nil || !strings.Contains(err.Error(), "does not implement SessionFSBinaryProvider") { + t.Fatalf("expected binary provider validation error, got %v", err) + } + snapshot := requests.snapshot() + if len(snapshot) != 2 || snapshot[0].Method != "session.create" || snapshot[1].Method != "session.delete" { + t.Fatalf("expected create then orphan cleanup, got %+v", snapshot) + } + if snapshot[1].Params["sessionId"] != "server-assigned-session-1" { + t.Fatalf("expected cleanup of server-assigned session, got %+v", snapshot[1]) + } + client.sessionsMux.Lock() + defer client.sessionsMux.Unlock() + if len(client.sessions) != 0 { + t.Fatalf("failed creation left %d active sessions", len(client.sessions)) + } +} + +func TestClient_CreateSessionDeletesCloudSessionWhenProviderValidationFailsAfterCancellation(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Binary: true}, + }, + }) + defer cleanup() + requests.notify = make(chan struct{}, 1) + + providerStarted := make(chan struct{}) + releaseProvider := make(chan struct{}) + release := sync.OnceFunc(func() { close(releaseProvider) }) + defer release() + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + createDone := make(chan error, 1) + go func() { + _, err := client.CreateSession(ctx, &SessionConfig{ + Cloud: &CloudSessionOptions{Repository: &CloudSessionRepository{Owner: "github", Name: "copilot-sdk", Branch: "main"}}, + OnPermissionRequest: PermissionHandler.ApproveAll, + CreateSessionFSProvider: func(*Session) SessionFSProvider { + close(providerStarted) + <-releaseProvider + return noSQLiteSessionFSProvider{} + }, + }) + createDone <- err + }() + + deadline := time.NewTimer(5 * time.Second) + defer deadline.Stop() + select { + case <-providerStarted: + case <-deadline.C: + t.Fatal("session.create did not reach the provider factory") + } + cancel() + select { + case err := <-createDone: + if !errors.Is(err, context.Canceled) { + t.Fatalf("CreateSession error = %v, want context cancellation", err) + } + case <-deadline.C: + t.Fatal("CreateSession did not return after cancellation") + } + release() + + for { + snapshot := requests.snapshot() + if len(snapshot) >= 2 { + if len(snapshot) != 2 || snapshot[0].Method != "session.create" || snapshot[1].Method != "session.delete" { + t.Fatalf("expected one create and one orphan cleanup, got %+v", snapshot) + } + if snapshot[1].Params["sessionId"] != "server-assigned-session-1" { + t.Fatalf("expected cleanup of server-assigned session, got %+v", snapshot[1]) + } + return + } + select { + case <-requests.notify: + case <-deadline.C: + t.Fatalf("cloud session was not deleted after provider validation failed; requests: %+v", snapshot) + } + } +} + func TestClient_AuthOptions(t *testing.T) { t.Run("should accept GitHubToken option", func(t *testing.T) { client := NewClient(&ClientOptions{ @@ -1637,6 +1875,38 @@ func TestClient_EnvOptions(t *testing.T) { }) } +func TestClient_ProcessFileLoggingEnvironment(t *testing.T) { + for _, tc := range []struct { + name string + mode ClientMode + env []string + want string + }{ + {name: "CLI mode enables process logs", env: []string{"COPILOT_RUNTIME_PROCESS_FILE_LOGGING=0"}, want: "1"}, + {name: "empty mode preserves caller opt-in", mode: ModeEmpty, env: []string{"COPILOT_RUNTIME_PROCESS_FILE_LOGGING=1"}, want: "1"}, + {name: "empty mode preserves caller opt-out", mode: ModeEmpty, env: []string{"COPILOT_RUNTIME_PROCESS_FILE_LOGGING=0"}, want: "0"}, + {name: "empty mode leaves logging unset", mode: ModeEmpty, env: []string{}, want: ""}, + } { + t.Run(tc.name, func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: StdioConnection{Path: os.Args[0]}, + Mode: tc.mode, + BaseDirectory: t.TempDir(), + WorkingDirectory: filepath.Join(t.TempDir(), "missing"), + Env: tc.env, + }) + + err := client.startCLIServer(context.Background()) + if err == nil || !strings.Contains(err.Error(), "failed to start CLI server") { + t.Fatalf("expected process start to fail in missing working directory, got %v", err) + } + if got := getEnvValue(client.process.Env, "COPILOT_RUNTIME_PROCESS_FILE_LOGGING"); got != tc.want { + t.Errorf("process file logging environment = %q, want %q", got, tc.want) + } + }) + } +} + func TestClient_InProcessConnection(t *testing.T) { t.Run("requires build tag", func(t *testing.T) { if inProcessAvailable { @@ -3104,12 +3374,19 @@ type recordedRequest struct { type requestRecorder struct { mu sync.Mutex requests []recordedRequest + notify chan struct{} } func (r *requestRecorder) append(request recordedRequest) { r.mu.Lock() defer r.mu.Unlock() r.requests = append(r.requests, request) + if r.notify != nil { + select { + case r.notify <- struct{}{}: + default: + } + } } func (r *requestRecorder) snapshot() []recordedRequest { @@ -3198,6 +3475,8 @@ func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeW result = map[string]any{} case "session.detach": result = map[string]any{"success": true} + case "session.delete": + result = map[string]any{"success": true} default: t.Errorf("unexpected JSON-RPC method %s", request.Method) return @@ -3860,6 +4139,108 @@ func TestModeCallbackRequestHandlers(t *testing.T) { } } +func TestResumeTranscriptRecoveryDefaultsAndReport(t *testing.T) { + for _, tc := range []struct { + name string + mode ClientMode + explicit *bool + want *bool + }{ + {"CLI default", ModeCopilotCli, nil, nil}, + {"empty default", ModeEmpty, nil, nil}, + {"CLI strict", ModeCopilotCli, Bool(false), Bool(false)}, + {"CLI recovery", ModeCopilotCli, Bool(true), Bool(true)}, + {"empty strict", ModeEmpty, Bool(false), Bool(false)}, + {"empty recovery", ModeEmpty, Bool(true), Bool(true)}, + } { + t.Run(tc.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + options: ClientOptions{Mode: tc.mode}, + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + resumeParams := make(chan json.RawMessage, 2) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + if sessionIDFromParams(t, params) == "unrepaired" { + return []byte(`{"sessionId":"unrepaired"}`), nil + } + return []byte(`{"sessionId":"s1","transcriptRecovery":{"plannedBackupPath":"backup.jsonl","invalidLineNumbers":[3,4],"sessionStartMoved":true}}`), nil + }) + server.SetRequestHandler("session.options.update", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + session, err := client.ResumeSessionWithOptions(t.Context(), "s1", &ResumeSessionConfig{ + AllowTranscriptRecovery: tc.explicit, + AvailableTools: []string{}, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + t.Cleanup(func() { + if err := session.Disconnect(); err != nil { + t.Errorf("session disconnect failed: %v", err) + } + }) + var wire map[string]any + if err := json.Unmarshal(<-resumeParams, &wire); err != nil { + t.Fatal(err) + } + value, present := wire["allowTranscriptRecovery"] + if tc.want == nil { + if present { + t.Fatalf("unexpected allowTranscriptRecovery: %v", value) + } + } else if !present || value != *tc.want { + t.Fatalf("allowTranscriptRecovery = %v (present %v), want %v", value, present, *tc.want) + } + report := session.TranscriptRecovery() + if report == nil || report.PlannedBackupPath != "backup.jsonl" || !reflect.DeepEqual(report.InvalidLineNumbers, []int{3, 4}) || !report.SessionStartMoved { + t.Fatalf("unexpected returned transcript recovery report: %+v", report) + } + unrepaired, err := client.ResumeSessionWithOptions(t.Context(), "unrepaired", &ResumeSessionConfig{ + AvailableTools: []string{}, + }) + if err != nil { + t.Fatalf("unrepaired ResumeSessionWithOptions failed: %v", err) + } + t.Cleanup(func() { + if err := unrepaired.Disconnect(); err != nil { + t.Errorf("unrepaired session disconnect failed: %v", err) + } + }) + if unrepaired.TranscriptRecovery() != nil { + t.Fatalf("unexpected recovery report on unrepaired session: %+v", unrepaired.TranscriptRecovery()) + } + }) + } +} + +func TestTranscriptRecoveryReportCopyIsolation(t *testing.T) { + session := &Session{transcriptRecovery: &TranscriptRecoveryReport{ + PlannedBackupPath: "backup.jsonl", + InvalidLineNumbers: []int{3, 4}, + SessionStartMoved: true, + }} + report := session.TranscriptRecovery() + if report == nil || report.PlannedBackupPath != "backup.jsonl" || !reflect.DeepEqual(report.InvalidLineNumbers, []int{3, 4}) || !report.SessionStartMoved { + t.Fatalf("unexpected transcript recovery report: %+v", report) + } + report.InvalidLineNumbers[0] = 99 + if session.TranscriptRecovery().InvalidLineNumbers[0] != 3 { + t.Fatal("report should not expose the session's slice") + } + if (&Session{}).TranscriptRecovery() != nil { + t.Fatal("new session should have no recovery report") + } +} + func TestResumeSessionRequest_ContinuePendingWork(t *testing.T) { t.Run("forwards continuePendingWork when true", func(t *testing.T) { req := resumeSessionRequest{ @@ -4810,6 +5191,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { Deny: []string{"Shell(git push)"}, Ask: []string{"Domain(publish.example)"}, Allow: []string{"Read(**)"}, + LimitTo: []string{"Domain(github.com)"}, }, } @@ -4818,6 +5200,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { "deny": []any{"Shell(git push)"}, "ask": []any{"Domain(publish.example)"}, "allow": []any{"Read(**)"}, + "limitTo": []any{"Domain(github.com)"}, } t.Run("direct injection enables managed safeguards", func(t *testing.T) { @@ -4909,6 +5292,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { Deny: []string{}, Ask: []string{}, Allow: []string{}, + LimitTo: []string{}, }, }} data, err := json.Marshal(req) @@ -4921,7 +5305,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { if perms["disableBypassPermissionsMode"] != "disable" { t.Errorf("Expected disableBypassPermissionsMode preserved, got %v", perms["disableBypassPermissionsMode"]) } - for _, key := range []string{"deny", "ask", "allow"} { + for _, key := range []string{"deny", "ask", "allow", "limitTo"} { if value, ok := perms[key].([]any); !ok || len(value) != 0 { t.Errorf("Expected %s to be an explicit empty array, got %v", key, perms[key]) } diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index 41b5863347..f7a34413d1 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -41,6 +41,7 @@ const ( packageJSONURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package.json" packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" defaultCLIDownloadBaseURL = "https://github.com/github/copilot-cli/releases/download" + unstableDownloadBaseURL = "https://github.com/github/copilot-sdk/releases/download" cliDownloadBaseURLEnvironment = "COPILOT_CLI_DOWNLOAD_BASE_URL" defaultPackageName = "main" ) @@ -540,7 +541,7 @@ func runtimeWrapperName(binaryName string) string { } var hostlessExcludedTopLevel = map[string]bool{ - "app.js": true, "assets": true, "changelog.json": true, "copilot": true, "copilot.exe": true, + "app.js": true, "assets": true, "changelog.json": true, "cli-main.js": true, "copilot": true, "copilot.exe": true, "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, "LICENSE.md": true, "npm-loader.js": true, "package.json": true, "pvrecorder": true, "queries": true, "README.md": true, "sea-loader.js": true, "webview": true, @@ -925,14 +926,18 @@ func mustDecodeBase64(s string) []byte { } var ( - releaseChecksumCache = map[string]map[string]string{} - releaseHTTPClient = &http.Client{Timeout: 10 * time.Minute} + releaseChecksumCache = map[string]map[string]string{} + releaseHTTPClient = &http.Client{Timeout: 10 * time.Minute} + unstableRuntimeVersion = regexp.MustCompile(`^(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*)){2}(?:-unstable|-(?:0|[1-9][0-9]*)\.unstable)\.r[1-9][0-9]*\.g[0-9a-f]{7}$`) ) -func cliDownloadBaseURL() string { +func cliDownloadBaseURL(version string) string { if override := strings.TrimRight(os.Getenv(cliDownloadBaseURLEnvironment), "/"); override != "" { return override } + if unstableRuntimeVersion.MatchString(version) { + return unstableDownloadBaseURL + } return defaultCLIDownloadBaseURL } @@ -941,7 +946,11 @@ func releaseAssetName(version, runtimePlatform string) string { } func releaseDownloadURL(version, assetName string) string { - return fmt.Sprintf("%s/v%s/%s", cliDownloadBaseURL(), version, assetName) + tag := "v" + version + if unstableRuntimeVersion.MatchString(version) { + tag = "runtime-" + version + } + return fmt.Sprintf("%s/%s/%s", cliDownloadBaseURL(version), tag, assetName) } func parseReleaseChecksums(contents string) map[string]string { @@ -958,11 +967,10 @@ func parseReleaseChecksums(contents string) map[string]string { } func getReleaseChecksum(version, assetName string) (string, error) { - baseURL := cliDownloadBaseURL() - cacheKey := baseURL + "\x00" + version + checksumsURL := releaseDownloadURL(version, "SHA256SUMS.txt") + cacheKey := checksumsURL checksums, ok := releaseChecksumCache[cacheKey] if !ok { - checksumsURL := fmt.Sprintf("%s/v%s/SHA256SUMS.txt", baseURL, version) fmt.Printf("Downloading checksums from %s...\n", checksumsURL) resp, err := releaseHTTPClient.Get(checksumsURL) if err != nil { diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index 7dd376d40b..90bdea1689 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -30,6 +30,7 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t "package/preloads/extension_bootstrap.mjs": "preload", "package/sdk/factory.js": "factory", "package/app.js": "excluded", + "package/cli-main.js": "excluded", "package/LICENSE.md": "excluded", "package/README.md": "excluded", }) @@ -50,7 +51,7 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t t.Fatalf("retained assets = %#v", files) } for _, excluded := range []string{ - "runtime.node", "copilot-runtime", "app.js", "LICENSE.md", "README.md", + "runtime.node", "copilot-runtime", "app.js", "cli-main.js", "LICENSE.md", "README.md", } { if _, ok := files[excluded]; ok { t.Fatalf("excluded asset %q was retained", excluded) @@ -59,46 +60,82 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t } func TestDownloadCLIBinaryUsesVerifiedReleasePackage(t *testing.T) { - dir := t.TempDir() - archivePath := filepath.Join(dir, "source.tgz") - writeTarGz(t, archivePath, map[string]string{ - "package/prebuilds/linux-x64/copilot-runtime": "runtime wrapper", - }) - archive, err := os.ReadFile(archivePath) - if err != nil { - t.Fatal(err) - } - checksum := fmt.Sprintf("%x", sha256.Sum256(archive)) - version := "1.2.3" - assetName := releaseAssetName(version, "linux-x64") - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - switch request.URL.Path { - case "/v1.2.3/SHA256SUMS.txt": - fmt.Fprintf(writer, "%s %s\n", checksum, assetName) - case "/v1.2.3/" + assetName: - writer.Write(archive) - default: - http.NotFound(writer, request) - } - })) - defer server.Close() - t.Setenv(cliDownloadBaseURLEnvironment, server.URL) - releaseChecksumCache = map[string]map[string]string{} + for _, version := range []string{"1.2.3", "1.2.3-4.unstable.r123.gabcdef0"} { + t.Run(version, func(t *testing.T) { + dir := t.TempDir() + archivePath := filepath.Join(dir, "source.tgz") + writeTarGz(t, archivePath, map[string]string{ + "package/prebuilds/linux-x64/copilot-runtime": "runtime wrapper", + }) + archive, err := os.ReadFile(archivePath) + if err != nil { + t.Fatal(err) + } + checksum := fmt.Sprintf("%x", sha256.Sum256(archive)) + tag := "v" + version + if strings.Contains(version, ".unstable.") { + tag = "runtime-" + version + } + assetName := releaseAssetName(version, "linux-x64") + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/" + tag + "/SHA256SUMS.txt": + fmt.Fprintf(writer, "%s %s\n", checksum, assetName) + case "/" + tag + "/" + assetName: + writer.Write(archive) + default: + http.NotFound(writer, request) + } + })) + defer server.Close() + t.Setenv(cliDownloadBaseURLEnvironment, server.URL) + releaseChecksumCache = map[string]map[string]string{} - binaryPath, downloadedArchive, err := downloadCLIBinary( - "linux-x64", - "copilot", - version, - t.TempDir(), - ) - if err != nil { - t.Fatal(err) - } - if got, err := os.ReadFile(binaryPath); err != nil || string(got) != "runtime wrapper" { - t.Fatalf("downloaded CLI = %q, %v", got, err) + binaryPath, downloadedArchive, err := downloadCLIBinary( + "linux-x64", + "copilot", + version, + t.TempDir(), + ) + if err != nil { + t.Fatal(err) + } + if got, err := os.ReadFile(binaryPath); err != nil || string(got) != "runtime wrapper" { + t.Fatalf("downloaded CLI = %q, %v", got, err) + } + if filepath.Base(downloadedArchive) != assetName { + t.Fatalf("downloaded archive = %q, want basename %q", downloadedArchive, assetName) + } + }) } - if filepath.Base(downloadedArchive) != assetName { - t.Fatalf("downloaded archive = %q, want basename %q", downloadedArchive, assetName) +} + +func TestReleaseDownloadURL(t *testing.T) { + for _, tc := range []struct{ version, repository, tag string }{ + {"1.2.3", "copilot-cli", "v1.2.3"}, + {"1.2.3-4", "copilot-cli", "v1.2.3-4"}, + {"1.2.3-unstable.r123.gabcdef0", "copilot-sdk", "runtime-1.2.3-unstable.r123.gabcdef0"}, + {"1.2.3-4.unstable.r123.gabcdef0", "copilot-sdk", "runtime-1.2.3-4.unstable.r123.gabcdef0"}, + {"0.0.0-0.unstable.r1.g0000000", "copilot-sdk", "runtime-0.0.0-0.unstable.r1.g0000000"}, + {"01.2.3-unstable.r123.gabcdef0", "copilot-cli", "v01.2.3-unstable.r123.gabcdef0"}, + {"1.02.3-unstable.r123.gabcdef0", "copilot-cli", "v1.02.3-unstable.r123.gabcdef0"}, + {"1.2.03-unstable.r123.gabcdef0", "copilot-cli", "v1.2.03-unstable.r123.gabcdef0"}, + {"1.2.3-04.unstable.r123.gabcdef0", "copilot-cli", "v1.2.3-04.unstable.r123.gabcdef0"}, + } { + t.Run(tc.version, func(t *testing.T) { + t.Setenv(cliDownloadBaseURLEnvironment, "") + for _, asset := range []string{"SHA256SUMS.txt", releaseAssetName(tc.version, "linux-x64")} { + want := "https://github.com/github/" + tc.repository + "/releases/download/" + tc.tag + "/" + asset + if got := releaseDownloadURL(tc.version, asset); got != want { + t.Fatalf("releaseDownloadURL() = %q, want %q", got, want) + } + } + t.Setenv(cliDownloadBaseURLEnvironment, "https://mirror.example/releases/") + want := "https://mirror.example/releases/" + tc.tag + "/SHA256SUMS.txt" + if got := releaseDownloadURL(tc.version, "SHA256SUMS.txt"); got != want { + t.Fatalf("mirror URL = %q, want %q", got, want) + } + }) } } diff --git a/go/connectors_rpc_test.go b/go/connectors_rpc_test.go index ff92a1b29a..41962b972d 100644 --- a/go/connectors_rpc_test.go +++ b/go/connectors_rpc_test.go @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + package copilot import ( @@ -82,6 +84,91 @@ func TestSessionRPCConnectors(t *testing.T) { !result.OpaqueAccountSelection { t.Fatalf("GetCapabilities result = %#v", result) } + if result.SessionAccountSelection != nil || result.TargetedReconcile != nil { + t.Fatalf("legacy capabilities must not enable new operations: %#v", result) + } + }) + + t.Run("session account", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getCapabilities", map[string]any{ + "sessionId": "session-1", + }, `{ + "apiVersion":1,"availability":"enabled","consentContinuation":true, + "maxDeadlineMs":60000,"maxPollAttempts":10,"maxPollIntervalMs":5000, + "opaqueAccountSelection":true,"sessionAccountSelection":true,"targetedReconcile":true + }`) + capabilities, err := session.RPC.Connectors.GetCapabilities(t.Context()) + if err != nil { + t.Fatalf("GetCapabilities: %v", err) + } + if capabilities.SessionAccountSelection == nil || !*capabilities.SessionAccountSelection || + capabilities.TargetedReconcile == nil || !*capabilities.TargetedReconcile { + t.Fatalf("new capabilities not enabled: %#v", capabilities) + } + + const account = `{"accountId":"` + connectorAccountID + `","authInfo":{"type":"token","host":"github.com","login":"alice"}}` + expectConnectorRPC(t, server, "session.connectors.getAccount", map[string]any{ + "sessionId": "session-1", + }, account) + result, err := session.RPC.Connectors.GetAccount(t.Context()) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if result == nil || result.AccountID != connectorAccountID || result.AuthInfo.Type != "token" || + result.AuthInfo.Host != "github.com" || result.AuthInfo.Login != "alice" { + t.Fatalf("GetAccount result = %#v", result) + } + wire, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + var got, want any + if err := json.Unmarshal(wire, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(account), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("credential-free account wire = %s", wire) + } + }) + + t.Run("unavailable session account is nil", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getAccount", map[string]any{ + "sessionId": "session-1", + }, `null`) + result, err := session.RPC.Connectors.GetAccount(t.Context()) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if result != nil { + t.Fatalf("null must not become an empty account: %#v", result) + } + // The exported result type models the schema's null branch, so it must + // accept GetAccount's return and a nil absent account. + exported := rpc.ConnectorSessionAccountResult(result) + if exported != nil { + t.Fatalf("exported result type must hold the absent account: %#v", exported) + } + }) + + t.Run("false capabilities remain unsupported", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getCapabilities", map[string]any{ + "sessionId": "session-1", + }, `{ + "apiVersion":1,"availability":"enabled","consentContinuation":true, + "maxDeadlineMs":60000,"maxPollAttempts":10,"maxPollIntervalMs":5000, + "opaqueAccountSelection":true,"sessionAccountSelection":false,"targetedReconcile":false + }`) + result, err := session.RPC.Connectors.GetCapabilities(t.Context()) + if err != nil { + t.Fatalf("GetCapabilities: %v", err) + } + if result.SessionAccountSelection == nil || *result.SessionAccountSelection || + result.TargetedReconcile == nil || *result.TargetedReconcile { + t.Fatalf("false capabilities must stay false: %#v", result) + } }) t.Run("get status", func(t *testing.T) { @@ -143,6 +230,33 @@ func TestSessionRPCConnectors(t *testing.T) { t.Fatalf("Refresh: %v", err) } assertConnectorCatalog(t, result, 8, rpc.ConnectorCatalogStatusNotConnected) + if result.Connectors[0].Logo != nil || result.Connectors[0].Tier != nil || result.Connectors[0].ReleaseTag != nil { + t.Fatalf("legacy catalog presentation metadata must stay absent: %#v", result.Connectors[0]) + } + }) + + t.Run("catalog presentation metadata", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.list", map[string]any{ + "accountId": connectorAccountID, + "sessionId": "session-1", + }, `{ + "connectors":[{ + "displayName":"Outlook","name":"outlook","runtimeServerIds":[],"status":"connected", + "logo":"https://example.com/outlook.svg","tier":"standard","releaseTag":"preview" + }], + "refreshedAtMs":1700000000100,"revision":8 + }`) + result, err := session.RPC.Connectors.List(t.Context(), &rpc.ConnectorAccountRequest{AccountID: connectorAccountID}) + if err != nil { + t.Fatalf("List: %v", err) + } + assertConnectorCatalog(t, result, 8, rpc.ConnectorCatalogStatusConnected) + entry := result.Connectors[0] + if entry.Logo == nil || *entry.Logo != "https://example.com/outlook.svg" || + entry.Tier == nil || *entry.Tier != "standard" || + entry.ReleaseTag == nil || *entry.ReleaseTag != "preview" { + t.Fatalf("catalog presentation metadata = %#v", entry) + } }) t.Run("connect", func(t *testing.T) { @@ -214,6 +328,33 @@ func TestSessionRPCConnectors(t *testing.T) { assertConnectorStatus(t, &connected.Status) }) + t.Run("unknown continuation outcome is not connected", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.continueConnection", map[string]any{ + "continuationId": "continuation-2", + "deadlineMs": float64(30000), + "maxAttempts": float64(4), + "pollIntervalMs": float64(500), + "sessionId": "session-1", + }, `{"kind":"future_outcome","continuationId":"continuation-2","status":`+connectorStatusResult+`}`) + + result, err := session.RPC.Connectors.ContinueConnection(t.Context(), &rpc.ConnectorContinueRequest{ + ContinuationID: "continuation-2", + DeadlineMs: 30000, + MaxAttempts: 4, + PollIntervalMs: 500, + }) + if err != nil { + t.Fatalf("ContinueConnection: %v", err) + } + raw, ok := result.(*rpc.RawConnectorConnectResultData) + if !ok || raw.Kind() != "future_outcome" { + t.Fatalf("unknown outcome must remain raw, got %#v", result) + } + if _, connected := result.(*rpc.ConnectorConnectResultConnected); connected { + t.Fatal("unknown outcome was treated as connected") + } + }) + t.Run("disconnect", func(t *testing.T) { expectConnectorRPC(t, server, "session.connectors.disconnect", map[string]any{ "accountId": connectorAccountID, @@ -250,6 +391,25 @@ func TestSessionRPCConnectors(t *testing.T) { } assertConnectorStatus(t, result) }) + + t.Run("targeted reconcile", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.reconcile", map[string]any{ + "accountId": connectorAccountID, + "refreshCatalog": true, + "forceConnectorName": "outlook", + "sessionId": "session-1", + }, connectorStatusResult) + + result, err := session.RPC.Connectors.Reconcile(t.Context(), &rpc.ConnectorReconcileRequest{ + AccountID: connectorAccountID, + RefreshCatalog: Bool(true), + ForceConnectorName: String("outlook"), + }) + if err != nil { + t.Fatalf("Reconcile: %v", err) + } + assertConnectorStatus(t, result) + }) } func expectConnectorRPC(t *testing.T, server *jsonrpc2.Client, method string, wantParams map[string]any, result string) { diff --git a/go/host.go b/go/host.go new file mode 100644 index 0000000000..0abdd5dc15 --- /dev/null +++ b/go/host.go @@ -0,0 +1,445 @@ +package copilot + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "reflect" + "sync" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" + "github.com/google/uuid" +) + +// AhpHostExit reports termination of a connection-owned listener. +// Experimental: AHP hosting may change or be removed. +type AhpHostExit = rpc.HostExitedNotification + +// AhpSessionCreateRequest contains the settings selected by the AHP host. +// Preserve Config when creating the session; add application prompts, tools and callbacks. +// Experimental: AHP hosting may change or be removed. +type AhpSessionCreateRequest struct { + Config *SessionConfig +} + +// AhpSessionResumeRequest identifies a durable application session to resume. +// Preserve Config, or return the exact retained original from the owning client. +// Experimental: AHP hosting may change or be removed. +type AhpSessionResumeRequest struct { + SessionID string + Config *ResumeSessionConfig +} + +// AhpHostOptions configures an experimental, in-process AHP listener. +// Factory contexts are cancelled when participation ends. A late returned session +// is still released exactly once; the SDK never disconnects or destroys it. +// Notification callbacks run asynchronously, with errors reported to the Go logger. +// At least one transport must be supplied. Both may be enabled. +// Experimental: AHP hosting may change or be removed. +type AhpHostOptions struct { + // LocalServer explicitly enables the local WebSocket listener. + LocalServer *rpc.HostLocalServerOptions + // GitHubEnvironment registers the host with Mission Control; Name and ComputeID are required. + GitHubEnvironment *rpc.HostGitHubEnvironmentOptions + CreateSession func(context.Context, AhpSessionCreateRequest) (*Session, error) + ResumeSession func(context.Context, AhpSessionResumeRequest) (*Session, error) + OnSessionReleased func(*Session) error + OnExit func(AhpHostExit) error +} + +// AhpHost is an experimental listener owned by one SDK connection. +// Reconnecting the client does not transfer this handle to its new connection. +// Experimental: AHP hosting may change or be removed. +type AhpHost struct { + HostID string + // URL is absent when no local listener was requested. + URL *string + Token *string + // EnvironmentID identifies the optional Mission Control registration. + EnvironmentID *string + // PID is absent for in-process listeners, not the owning runtime's PID. + PID *int64 + rpc *rpc.ServerHostAPI +} + +// Dispose stops the listener and joins runtime cleanup without deleting application +// sessions. Every call reaches the runtime, including repeated or concurrent calls. +func (h *AhpHost) Dispose(ctx context.Context) error { + _, err := h.rpc.Dispose(ctx, &rpc.HostDisposeRequest{HostID: h.HostID}) + return err +} + +// PublishSession exposes the exact resident session attached to the owning client. +// It does not invoke factories or transfer ownership. +func (h *AhpHost) PublishSession(ctx context.Context, sessionID string) (*rpc.HostPublishSessionResult, error) { + return h.rpc.PublishSession(ctx, &rpc.HostPublishSessionRequest{HostID: h.HostID, SessionID: sessionID}) +} + +type ahpHostState struct { + mu sync.Mutex + hosts map[string]AhpHostOptions + handoffs map[string]*ahpHandoff +} + +type ahpHandoff struct { + hostID string + sessionID string + options AhpHostOptions + ctx context.Context + cancel context.CancelFunc + session *Session + configs map[*Session]map[string]any + released bool +} + +var errAhpHandoffEnded = errors.New("AHP session handoff ended") + +// StartAhpHost starts a full AHP listener in the connected runtime, without a +// companion process. The listener ends when this connection closes. +// Cancellation abandons the wait; a listener that subsequently starts is disposed. +// Experimental: AHP hosting may change or be removed. +func (c *Client) StartAhpHost(ctx context.Context, options *AhpHostOptions) (*AhpHost, error) { + if options == nil || (options.LocalServer == nil && options.GitHubEnvironment == nil) { + return nil, errors.New("AHP hosting requires localServer or githubEnvironment") + } + if err := c.ensureConnected(ctx); err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, err + } + opts := *options + c.startStopMux.RLock() + if c.RPC == nil { + c.startStopMux.RUnlock() + return nil, errors.New("client is not connected") + } + hostRPC := c.RPC.Host + c.startStopMux.RUnlock() + hostID := uuid.NewString() + c.ahp.mu.Lock() + if c.ahp.hosts == nil { + c.ahp.hosts = make(map[string]AhpHostOptions) + c.ahp.handoffs = make(map[string]*ahpHandoff) + } + c.ahp.hosts[hostID] = opts + c.ahp.mu.Unlock() + params := &rpc.HostStartRequest{ + HostID: hostID, LocalServer: opts.LocalServer, + GitHubEnvironment: opts.GitHubEnvironment, + } + if opts.CreateSession != nil { + params.SessionFactory = Bool(true) + } + if opts.ResumeSession != nil { + params.ResumeFactory = Bool(true) + } + type startResult struct { + info *rpc.HostStartResult + err error + } + started := make(chan startResult, 1) + go func() { + info, err := hostRPC.Start(context.WithoutCancel(ctx), params) + started <- startResult{info, err} + }() + select { + case result := <-started: + if result.err != nil { + c.releaseAhpHost(hostID) + return nil, result.err + } + info := result.info + return &AhpHost{HostID: info.HostID, URL: info.URL, Token: info.Token, PID: info.Pid, EnvironmentID: info.EnvironmentID, rpc: hostRPC}, nil + case <-ctx.Done(): + // Wait for registration before disposing: an early dispose can miss the listener. + go func() { + defer c.releaseAhpHost(hostID) + if result := <-started; result.err == nil { + if _, err := hostRPC.Dispose(context.Background(), &rpc.HostDisposeRequest{HostID: result.info.HostID}); err != nil { + log.Printf("AHP cancelled startup cleanup failed: %v", err) + } + } + }() + return nil, ctx.Err() + } +} + +func (c *Client) releaseAhpHost(hostID string) (AhpHostOptions, bool) { + c.ahp.mu.Lock() + options, exists := c.ahp.hosts[hostID] + delete(c.ahp.hosts, hostID) + var handoffs []string + for id, entry := range c.ahp.handoffs { + if entry.hostID == hostID { + handoffs = append(handoffs, id) + } + } + c.ahp.mu.Unlock() + for _, id := range handoffs { + c.releaseAhpSession(&rpc.HostSessionReleasedNotification{HostID: hostID, HandoffID: id}) + } + return options, exists +} + +func (c *Client) handleAhpExit(event *rpc.HostExitedNotification) { + options, exists := c.releaseAhpHost(event.HostID) + if exists && options.OnExit != nil { + go reportAhpCallback("exit", func() error { return options.OnExit(*event) }) + } +} + +func (c *Client) disconnectAhpHosts() { + c.ahp.mu.Lock() + var ids []string + for id := range c.ahp.hosts { + ids = append(ids, id) + } + c.ahp.mu.Unlock() + for _, id := range ids { + c.handleAhpExit(&rpc.HostExitedNotification{ + HostID: id, Reason: "ownerDisconnected", + Error: String("Owner connection closed; runtime cleanup cannot be acknowledged on this connection."), + }) + } +} + +func (c *Client) releaseAhpSession(notification *rpc.HostSessionReleasedNotification) { + c.ahp.mu.Lock() + entry := c.ahp.handoffs[notification.HandoffID] + if entry == nil || entry.hostID != notification.HostID { + c.ahp.mu.Unlock() + return + } + delete(c.ahp.handoffs, notification.HandoffID) + entry.released = true + entry.cancel() + session := entry.session + entry.session = nil + entry.configs = nil + c.ahp.mu.Unlock() + notifyAhpReleased(entry.options, session) +} + +func notifyAhpReleased(options AhpHostOptions, session *Session) { + if session != nil && options.OnSessionReleased != nil { + go reportAhpCallback("session release", func() error { return options.OnSessionReleased(session) }) + } +} + +func reportAhpCallback(label string, callback func() error) { + defer func() { + if failure := recover(); failure != nil { + log.Printf("AHP %s callback panicked: %v", label, failure) + } + }() + if err := callback(); err != nil { + log.Printf("AHP %s callback failed: %v", label, err) + } +} + +func (c *Client) captureAhpSession(session *Session, request any) error { + c.ahp.mu.Lock() + var entries []*ahpHandoff + for _, entry := range c.ahp.handoffs { + if entry.sessionID == session.SessionID && entry.configs != nil { + entries = append(entries, entry) + } + } + c.ahp.mu.Unlock() + if len(entries) == 0 { + return nil + } + data, err := json.Marshal(request) + if err != nil { + return fmt.Errorf("capture AHP session settings: %w", err) + } + var snapshot map[string]any + if err := json.Unmarshal(data, &snapshot); err != nil { + return fmt.Errorf("capture AHP session settings: %w", err) + } + c.ahp.mu.Lock() + defer c.ahp.mu.Unlock() + for _, entry := range entries { + if !entry.released && entry.configs != nil { + entry.configs[session] = snapshot + } + } + return nil +} + +func decodeAhpConfig(config map[string]any, target any) error { + copy := make(map[string]any, len(config)) + var token string + for key, value := range config { + if key == "gitHubToken" { + var ok bool + token, ok = value.(string) + if !ok { + return errors.New("AHP gitHubToken must be a string") + } + continue + } + if key == "configDir" { + key = "configDirectory" + } + copy[key] = value + } + if _, resume := target.(*ResumeSessionConfig); resume { + delete(copy, "sessionId") + } + data, err := json.Marshal(copy) + if err != nil { + return err + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(target); err != nil { + return err + } + // Per-session credentials are intentionally excluded from ordinary config JSON. + switch options := target.(type) { + case *SessionConfig: + options.GitHubToken = token + case *ResumeSessionConfig: + options.GitHubToken = token + } + return nil +} + +func containsAhpConfig(actual, expected any, root bool) bool { + if required, ok := expected.(map[string]any); ok { + value, ok := actual.(map[string]any) + if !ok { + return false + } + for key, expectedValue := range required { + actualValue, exists := value[key] + // These default-false flags are omitted by the session request builder. + if !exists && root && expectedValue == false && (key == "requestMcpApps" || key == "disableResume") { + continue + } + if !exists || !containsAhpConfig(actualValue, expectedValue, false) { + return false + } + } + return true + } + return reflect.DeepEqual(actual, expected) +} + +func (c *Client) materializeAhpSession(params *rpc.HostSessionCreateCallback) (*rpc.HostSessionCreateResult, *jsonrpc2.Error) { + fail := func(err error) (*rpc.HostSessionCreateResult, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: jsonrpc2.ErrInternal.Code, Message: err.Error()} + } + resume := params.Resume != nil && *params.Resume + sessionID, _ := params.Config["sessionId"].(string) + c.ahp.mu.Lock() + options, exists := c.ahp.hosts[params.HostID] + if !exists || c.ahp.handoffs[params.HandoffID] != nil || + (resume && options.ResumeSession == nil) || (!resume && options.CreateSession == nil) || sessionID == "" { + c.ahp.mu.Unlock() + return fail(errors.New("AHP session factory is unavailable or handoff already exists")) + } + ctx, cancel := context.WithCancel(context.Background()) + entry := &ahpHandoff{ + hostID: params.HostID, sessionID: sessionID, options: options, + ctx: ctx, cancel: cancel, configs: make(map[*Session]map[string]any), + } + c.ahp.handoffs[params.HandoffID] = entry + c.ahp.mu.Unlock() + + done := make(chan error, 1) + go func() { + session, err := invokeAhpFactory(entry, params.Config, resume) + if err == nil { + c.sessionsMux.Lock() + owned := session != nil && session.SessionID == sessionID && c.sessions[sessionID] == session + c.sessionsMux.Unlock() + if !owned { + err = errors.New("AHP callback must return the requested session from this client") + } + } + expected := make(map[string]any, len(params.Config)) + for key, value := range params.Config { + switch key { + case "suppressResumeEvent": + key = "disableResume" + case "enableExperimentalMode": + key = "isExperimentalMode" + case "enableMcpApps": + key = "requestMcpApps" + } + expected[key] = value + } + c.ahp.mu.Lock() + entry.session = session + released := entry.released + if released { + entry.session = nil + if err == nil { + err = errAhpHandoffEnded + } + } else if err == nil { + actual, captured := entry.configs[session] + if (!resume || captured) && !containsAhpConfig(actual, expected, true) { + err = errors.New("AHP callback must preserve the supplied session configuration") + } + } + entry.configs = nil + c.ahp.mu.Unlock() + if released { + notifyAhpReleased(options, session) + } + if err != nil { + if !errors.Is(err, errAhpHandoffEnded) && (!released || !errors.Is(err, context.Canceled)) { + log.Printf("AHP session factory failed: %v", err) + } + } + done <- err + if err != nil { + c.releaseAhpSession(&rpc.HostSessionReleasedNotification{HostID: params.HostID, HandoffID: params.HandoffID}) + } + }() + complete := func(err error) (*rpc.HostSessionCreateResult, *jsonrpc2.Error) { + if err != nil { + return fail(err) + } + return &rpc.HostSessionCreateResult{SessionID: sessionID}, nil + } + select { + case err := <-done: + return complete(err) + case <-ctx.Done(): + select { + case err := <-done: + return complete(err) + default: + return fail(errAhpHandoffEnded) + } + } +} + +func invokeAhpFactory(entry *ahpHandoff, config map[string]any, resume bool) (session *Session, err error) { + defer func() { + if failure := recover(); failure != nil { + err = fmt.Errorf("AHP session factory panicked: %v", failure) + } + }() + if resume { + var options ResumeSessionConfig + if err := decodeAhpConfig(config, &options); err != nil { + return nil, err + } + return entry.options.ResumeSession(entry.ctx, AhpSessionResumeRequest{SessionID: entry.sessionID, Config: &options}) + } + var options SessionConfig + if err := decodeAhpConfig(config, &options); err != nil { + return nil, err + } + return entry.options.CreateSession(entry.ctx, AhpSessionCreateRequest{Config: &options}) +} diff --git a/go/host_test.go b/go/host_test.go new file mode 100644 index 0000000000..c660b9b3ea --- /dev/null +++ b/go/host_test.go @@ -0,0 +1,457 @@ +package copilot + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +type ahpTestLog struct { + sync.Mutex + bytes.Buffer +} + +func (b *ahpTestLog) Write(data []byte) (int, error) { + b.Lock() + defer b.Unlock() + return b.Buffer.Write(data) +} + +func (b *ahpTestLog) text() string { + b.Lock() + defer b.Unlock() + return b.String() +} + +func TestAhpHostRequiresExplicitTransport(t *testing.T) { + for _, options := range []*AhpHostOptions{nil, {}} { + client := NewClient(nil) + if _, err := client.StartAhpHost(t.Context(), options); err == nil || !strings.Contains(err.Error(), "requires localServer or githubEnvironment") { + t.Fatalf("expected missing transport error, got %v", err) + } + } +} + +func TestAhpHostTransportsAndOptionalResults(t *testing.T) { + for _, transport := range []string{"local", "github", "both"} { + t.Run(transport, func(t *testing.T) { + client, server, _ := ahpFixture(t) + options := &AhpHostOptions{} + want := map[string]any{} + if transport != "github" { + port := int32(0) + options.LocalServer = &rpc.HostLocalServerOptions{ + Hostname: String("127.0.0.1"), Port: &port, + Token: String("secret"), RequireConnectionToken: Bool(true), + } + want["localServer"] = map[string]any{ + "hostname": "127.0.0.1", "port": float64(0), + "token": "secret", "requireConnectionToken": true, + } + } + if transport != "local" { + options.GitHubEnvironment = &rpc.HostGitHubEnvironmentOptions{Name: "SDK host", ComputeID: "compute"} + want["githubEnvironment"] = map[string]any{"name": "SDK host", "computeId": "compute"} + } + server.SetRequestHandler("host.start", func(data json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var params map[string]any + if err := json.Unmarshal(data, ¶ms); err != nil { + t.Error(err) + } + hostID := params["hostId"] + delete(params, "hostId") + got, _ := json.Marshal(params) + expected, _ := json.Marshal(want) + if !bytes.Equal(got, expected) { + t.Errorf("transport request = %s, want %s", got, expected) + } + result := map[string]any{"hostId": hostID} + if transport != "github" { + result["url"] = "ws://127.0.0.1:12345" + } + if transport != "local" { + result["environmentId"] = "environment" + } + response, _ := json.Marshal(result) + return response, nil + }) + host, err := client.StartAhpHost(t.Context(), options) + if err != nil { + t.Fatal(err) + } + if host.Token != nil || host.PID != nil { + t.Fatal("absent result fields must remain nil") + } + if transport == "github" { + if host.URL != nil { + t.Fatal("Mission Control-only host has a local URL") + } + } else if host.URL == nil || *host.URL != "ws://127.0.0.1:12345" { + t.Fatalf("incorrect local URL: %v", host.URL) + } + if transport == "local" { + if host.EnvironmentID != nil { + t.Fatal("local-only host has an environment ID") + } + } else if host.EnvironmentID == nil || *host.EnvironmentID != "environment" { + t.Fatalf("incorrect environment ID: %v", host.EnvironmentID) + } + }) + } +} + +func TestAhpFactoryCancellationLogging(t *testing.T) { + output := new(ahpTestLog) + previous := log.Writer() + log.SetOutput(output) + defer log.SetOutput(previous) + for _, released := range []bool{true, false} { + client, server, _ := ahpFixture(t) + entered := make(chan struct{}, 1) + finish := make(chan struct{}) + host, err := client.StartAhpHost(t.Context(), &AhpHostOptions{ + LocalServer: &rpc.HostLocalServerOptions{}, + CreateSession: func(ctx context.Context, _ AhpSessionCreateRequest) (*Session, error) { + entered <- struct{}{} + <-finish + err := ctx.Err() + if err == nil { + err = context.Canceled + } + return nil, fmt.Errorf("factory cancelled: %w", err) + }, + }) + if err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { + _, err := server.Request(t.Context(), "host.materializeSession", &rpc.HostSessionCreateCallback{ + HostID: host.HostID, HandoffID: "cancelled", Config: map[string]any{"sessionId": "requested"}, + }) + done <- err + }() + awaitAhpTest(t, entered) + if released { + client.releaseAhpSession(&rpc.HostSessionReleasedNotification{HostID: host.HostID, HandoffID: "cancelled"}) + } + close(finish) + if err := awaitAhpTest(t, done); err == nil { + t.Fatal("expected failed handoff") + } + if released { + time.Sleep(200 * time.Millisecond) + if strings.Contains(output.text(), "AHP session factory failed") { + t.Fatal("expected cooperative cancellation was logged") + } + } else if !strings.Contains(output.text(), "AHP session factory failed") { + t.Fatal("unexpected factory cancellation was not logged") + } + } +} + +func TestAhpCancelledStartupDisposesAfterStartCompletes(t *testing.T) { + client, server, _ := ahpFixture(t) + entered := make(chan string, 1) + finish := make(chan struct{}) + disposed := make(chan string, 1) + server.SetRequestHandler("host.start", jsonrpc2.RequestHandlerFor(func(params *rpc.HostStartRequest) (*rpc.HostStartResult, *jsonrpc2.Error) { + entered <- params.HostID + <-finish + return &rpc.HostStartResult{HostID: params.HostID, URL: String("ws://127.0.0.1:12345")}, nil + })) + server.SetRequestHandler("host.dispose", jsonrpc2.RequestHandlerFor(func(params *rpc.HostDisposeRequest) (map[string]any, *jsonrpc2.Error) { + disposed <- params.HostID + return map[string]any{}, nil + })) + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan error, 1) + go func() { + _, err := client.StartAhpHost(ctx, &AhpHostOptions{LocalServer: &rpc.HostLocalServerOptions{}}) + done <- err + }() + hostID := awaitAhpTest(t, entered) + cancel() + if err := awaitAhpTest(t, done); !errors.Is(err, context.Canceled) { + t.Fatalf("expected cancellation, got %v", err) + } + if len(disposed) != 0 { + t.Fatal("disposed before startup settled") + } + client.startStopMux.Lock() + client.RPC = nil + client.startStopMux.Unlock() + close(finish) + if got := awaitAhpTest(t, disposed); got != hostID { + t.Fatalf("disposed %s instead of abandoned %s", got, hostID) + } +} + +func ahpFixture(t *testing.T) (*Client, *jsonrpc2.Client, *atomic.Int32) { + t.Helper() + transport, server, _ := newRuntimeShutdownRpcPair(t) + client := NewClient(nil) + client.client, client.RPC, client.state = transport, rpc.NewServerRPC(transport), stateConnected + client.setupNotificationHandler() + t.Cleanup(func() { client.ForceStop(); server.Stop() }) + disposals := new(atomic.Int32) + server.SetRequestHandler("host.start", jsonrpc2.RequestHandlerFor(func(params *rpc.HostStartRequest) (*rpc.HostStartResult, *jsonrpc2.Error) { + return &rpc.HostStartResult{HostID: params.HostID, URL: String("ws://127.0.0.1:12345"), Token: String("secret")}, nil + })) + server.SetRequestHandler("host.dispose", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + disposals.Add(1) + return []byte(`{}`), nil + }) + for _, method := range []string{"session.create", "session.resume"} { + server.SetRequestHandler(method, func(data json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request struct { + SessionID string `json:"sessionId"` + } + if err := json.Unmarshal(data, &request); err != nil { + t.Error(err) + } + response, err := json.Marshal(map[string]any{"sessionId": request.SessionID}) + if err != nil { + t.Error(err) + } + return response, nil + }) + } + for _, method := range []string{"session.options.update", "session.detach"} { + server.SetRequestHandler(method, func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{}`), nil + }) + } + return client, server, disposals +} + +func awaitAhpTest[T any](t *testing.T, values <-chan T) T { + t.Helper() + select { + case value := <-values: + return value + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for AHP callback") + var zero T + return zero + } +} + +func TestAhpHostOriginalTransportAndExitOnce(t *testing.T) { + client, _, disposals := ahpFixture(t) + exits := make(chan AhpHostExit, 4) + host, err := client.StartAhpHost(t.Context(), &AhpHostOptions{LocalServer: &rpc.HostLocalServerOptions{}, OnExit: func(event AhpHostExit) error { + exits <- event + return nil + }}) + if err != nil { + t.Fatal(err) + } + client.RPC = nil + done := make(chan error, 2) + for range 2 { + go func() { done <- host.Dispose(t.Context()) }() + } + for range 2 { + if err := awaitAhpTest(t, done); err != nil { + t.Fatal(err) + } + } + if disposals.Load() != 2 || host.PID != nil { + t.Fatalf("incorrect disposal/PID: %d %v", disposals.Load(), host.PID) + } + for range 2 { + client.handleAhpExit(&rpc.HostExitedNotification{HostID: host.HostID, Reason: "disposed"}) + } + if awaitAhpTest(t, exits).Reason != "disposed" { + t.Fatal("wrong exit") + } + client.disconnectAhpHosts() + if len(exits) != 0 { + t.Fatal("duplicate exit callback") + } +} + +func TestAhpFactoriesPreserveSettingsAndReleaseExactOriginal(t *testing.T) { + for _, flags := range []struct{ resume, enabled bool }{{false, false}, {false, true}, {true, false}, {true, true}} { + resume, enabled := flags.resume, flags.enabled + t.Run(fmt.Sprintf("resume=%t/enabled=%t", resume, enabled), func(t *testing.T) { + client, server, _ := ahpFixture(t) + released := make(chan *Session, 4) + original := make(chan *Session, 1) + factoryCtx := make(chan context.Context, 1) + host, err := client.StartAhpHost(t.Context(), &AhpHostOptions{ + LocalServer: &rpc.HostLocalServerOptions{}, + CreateSession: func(ctx context.Context, request AhpSessionCreateRequest) (*Session, error) { + factoryCtx <- ctx + session, err := client.CreateSession(ctx, request.Config) + original <- session + return session, err + }, + ResumeSession: func(ctx context.Context, request AhpSessionResumeRequest) (*Session, error) { + factoryCtx <- ctx + session, err := client.ResumeSessionWithOptions(ctx, request.SessionID, request.Config) + original <- session + return session, err + }, + OnSessionReleased: func(session *Session) error { released <- session; return nil }, + }) + if err != nil { + t.Fatal(err) + } + config := map[string]any{ + "sessionId": "original", "workingDirectory": "/workspace", + "additionalDirectories": []string{}, "gitHubToken": "test-auth", + "mcpOAuthTokenStorage": "in-memory", "enableMcpApps": enabled, "streaming": enabled, + "enableExperimentalMode": false, + "infiniteSessions": map[string]any{"enabled": false, "backgroundCompactionThreshold": 0.7}, + "featureFlags": map[string]any{"Arbitrary.MixedCase": true}, + } + if resume { + config["continuePendingWork"] = false + config["suppressResumeEvent"] = enabled + } + if _, err := server.Request(t.Context(), "host.materializeSession", &rpc.HostSessionCreateCallback{ + HostID: host.HostID, HandoffID: "handoff", Resume: Bool(resume), Config: config, + }); err != nil { + t.Fatal(err) + } + session := awaitAhpTest(t, original) + context := awaitAhpTest(t, factoryCtx) + client.releaseAhpSession(&rpc.HostSessionReleasedNotification{ + HostID: "wrong-host", HandoffID: "handoff", + }) + if context.Err() != nil { + t.Fatal("wrong host cancelled participation") + } + for range 2 { + client.releaseAhpSession(&rpc.HostSessionReleasedNotification{ + HostID: host.HostID, HandoffID: "handoff", + }) + } + if awaitAhpTest(t, released) != session || context.Err() == nil || len(released) != 0 { + t.Fatal("release lost identity, cancellation, or exactly-once semantics") + } + if client.sessions["original"] != session { + t.Fatal("release destroyed the application session") + } + }) + } +} + +func TestAhpRejectsChangedBooleanSettings(t *testing.T) { + for _, setting := range []string{"streaming", "enableMcpApps", "suppressResumeEvent"} { + for _, expected := range []bool{false, true} { + t.Run(fmt.Sprintf("%s=%t", setting, expected), func(t *testing.T) { + client, server, _ := ahpFixture(t) + host, err := client.StartAhpHost(t.Context(), &AhpHostOptions{ + LocalServer: &rpc.HostLocalServerOptions{}, + ResumeSession: func(ctx context.Context, request AhpSessionResumeRequest) (*Session, error) { + switch setting { + case "streaming": + request.Config.Streaming = Bool(!expected) + case "enableMcpApps": + request.Config.EnableMCPApps = !expected + case "suppressResumeEvent": + request.Config.SuppressResumeEvent = !expected + } + return client.ResumeSessionWithOptions(ctx, request.SessionID, request.Config) + }, + }) + if err != nil { + t.Fatal(err) + } + _, err = server.Request(t.Context(), "host.materializeSession", &rpc.HostSessionCreateCallback{ + HostID: host.HostID, HandoffID: "changed", Resume: Bool(true), + Config: map[string]any{"sessionId": "changed", setting: expected}, + }) + if err == nil || !strings.Contains(err.Error(), "preserve") { + t.Fatalf("expected configuration preservation error, got %v", err) + } + }) + } + } +} + +func TestAhpCancellationReleasesLateResultAndRejectsDuplicate(t *testing.T) { + client, server, _ := ahpFixture(t) + entered := make(chan context.Context, 1) + unblock := make(chan struct{}) + released := make(chan *Session, 2) + late := &Session{SessionID: "late"} + host, err := client.StartAhpHost(t.Context(), &AhpHostOptions{ + LocalServer: &rpc.HostLocalServerOptions{}, + CreateSession: func(ctx context.Context, _ AhpSessionCreateRequest) (*Session, error) { + entered <- ctx + <-unblock + return late, nil + }, + OnSessionReleased: func(session *Session) error { released <- session; return nil }, + }) + if err != nil { + t.Fatal(err) + } + params := &rpc.HostSessionCreateCallback{ + HostID: host.HostID, HandoffID: "pending", Config: map[string]any{"sessionId": "late"}, + } + done := make(chan error, 1) + go func() { _, err := server.Request(t.Context(), "host.materializeSession", params); done <- err }() + ctx := awaitAhpTest(t, entered) + if _, err := server.Request(t.Context(), "host.materializeSession", params); err == nil { + t.Fatal("duplicate handoff accepted") + } + client.releaseAhpSession(&rpc.HostSessionReleasedNotification{ + HostID: host.HostID, HandoffID: "pending", + }) + if err := awaitAhpTest(t, done); err == nil || !strings.Contains(err.Error(), "handoff ended") || ctx.Err() == nil { + t.Fatalf("pending handoff did not cancel: %v", err) + } + close(unblock) + if awaitAhpTest(t, released) != late { + t.Fatal("late result was not released") + } +} + +func TestAhpRejectsModifiedSettingsAndAllowsRetainedResume(t *testing.T) { + client, server, _ := ahpFixture(t) + released := make(chan *Session, 1) + var original *Session + host, err := client.StartAhpHost(t.Context(), &AhpHostOptions{ + LocalServer: &rpc.HostLocalServerOptions{}, + CreateSession: func(ctx context.Context, request AhpSessionCreateRequest) (*Session, error) { + request.Config.WorkingDirectory = "/wrong" + session, err := client.CreateSession(ctx, request.Config) + original = session + return session, err + }, + ResumeSession: func(context.Context, AhpSessionResumeRequest) (*Session, error) { return original, nil }, + OnSessionReleased: func(session *Session) error { released <- session; return nil }, + }) + if err != nil { + t.Fatal(err) + } + params := &rpc.HostSessionCreateCallback{ + HostID: host.HostID, HandoffID: "create", Config: map[string]any{"sessionId": "retained", "workingDirectory": "/selected"}, + } + if _, err := server.Request(t.Context(), "host.materializeSession", params); err == nil || !strings.Contains(err.Error(), "configuration") { + t.Fatalf("modified settings accepted: %v", err) + } + if awaitAhpTest(t, released) != original { + t.Fatal("rejected session was not released") + } + params.HandoffID, params.Resume = "resume", Bool(true) + if _, err := server.Request(t.Context(), "host.materializeSession", params); err != nil { + t.Fatal(err) + } +} diff --git a/go/installation_confirmation.go b/go/installation_confirmation.go new file mode 100644 index 0000000000..a9690e1622 --- /dev/null +++ b/go/installation_confirmation.go @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +const installationConfirmationRequestCancelledCode = -32800 + +// InstallationConfirmationRequest is the typed review request for one sealed +// installation operation on its original connection. +// +// Experimental: InstallationConfirmationRequest may change or be removed. +type InstallationConfirmationRequest = rpc.InstallationConfirmationRequest + +// InstallationConfirmationDecision is the explicit human decision returned by +// an installation confirmation handler. +// +// Experimental: InstallationConfirmationDecision may change or be removed. +type InstallationConfirmationDecision = rpc.InstallationDecision + +const ( + // InstallationConfirmationDecisionCancel cancels the pending decision without granting consent. + InstallationConfirmationDecisionCancel = rpc.InstallationDecisionCancel + // InstallationConfirmationDecisionConfirm explicitly approves the exact review. + InstallationConfirmationDecisionConfirm = rpc.InstallationDecisionConfirm + // InstallationConfirmationDecisionDecline declines the reviewed operation. + InstallationConfirmationDecisionDecline = rpc.InstallationDecisionDecline +) + +// InstallationConfirmationContext is cancelled when the runtime retires this +// JSON-RPC request, including via numeric $/cancelRequest, or when the original +// connection is closed or the client is stopped. It does not cancel outbound +// installation or OAuth RPCs. +// +// Experimental: InstallationConfirmationContext may change or be removed. +type InstallationConfirmationContext = context.Context + +// InstallationConfirmationHandler collects a fresh human decision for the +// complete runtime review. +// +// Match OperationID and PolicySessionID against the exact action registered by +// this client before displaying the review. Refuse unknown actions or +// incomplete reviews; never infer authority from the current session. The SDK +// echoes the original challenge and fingerprint, so the handler returns only a +// decision. +// +// Experimental: InstallationConfirmationHandler may change or be removed. +type InstallationConfirmationHandler interface { + ConfirmInstallation(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) +} + +// InstallationConfirmationHandlerFunc adapts a function to +// [InstallationConfirmationHandler]. +// +// Experimental: InstallationConfirmationHandlerFunc may change or be removed. +type InstallationConfirmationHandlerFunc func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) + +// ConfirmInstallation calls f(request, context). +func (f InstallationConfirmationHandlerFunc) ConfirmInstallation(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + return f(request, context) +} + +type installationConfirmationAdapter struct { + handler InstallationConfirmationHandler + connectionClosed <-chan struct{} + closed chan struct{} + closeOnce sync.Once +} + +func newInstallationConfirmationAdapter(handler InstallationConfirmationHandler, connectionClosed <-chan struct{}) *installationConfirmationAdapter { + adapter := &installationConfirmationAdapter{ + handler: handler, + connectionClosed: connectionClosed, + closed: make(chan struct{}), + } + go adapter.watchConnectionClosed() + return adapter +} + +func (a *installationConfirmationAdapter) watchConnectionClosed() { + select { + case <-a.connectionClosed: + a.close() + case <-a.closed: + } +} + +func (a *installationConfirmationAdapter) close() { + a.closeOnce.Do(func() { + close(a.closed) + }) +} + +func (a *installationConfirmationAdapter) connectionClosedSignal() <-chan struct{} { + return a.closed +} + +func (a *installationConfirmationAdapter) handle(ctx context.Context, params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request rpc.InstallationConfirmationRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{ + Code: jsonrpc2.ErrInvalidParams.Code, + Message: fmt.Sprintf("Invalid installation confirmation review: %v", err), + } + } + confirmationID := request.ConfirmationID + reviewFingerprint := request.ReviewFingerprint + connectionClosed := a.connectionClosedSignal() + if cancellation := installationConfirmationCancellationError(ctx, connectionClosed); cancellation != nil { + return nil, cancellation + } + handlerContext, stopWatchingConnection := a.handlerContext(ctx, connectionClosed) + defer stopWatchingConnection() + + type outcome struct { + decision InstallationConfirmationDecision + err error + } + outcomes := make(chan outcome, 1) + go func() { + defer func() { + if r := recover(); r != nil { + outcomes <- outcome{err: fmt.Errorf("installation confirmation handler panicked: %v", r)} + } + }() + decision, err := a.handler.ConfirmInstallation(request, handlerContext) + outcomes <- outcome{decision: decision, err: err} + }() + + select { + case <-ctx.Done(): + return nil, &jsonrpc2.Error{ + Code: installationConfirmationRequestCancelledCode, + Message: "Installation confirmation request cancelled", + } + case <-connectionClosed: + return nil, &jsonrpc2.Error{ + Code: jsonrpc2.ErrInternal.Code, + Message: "Installation confirmation connection closed", + } + case outcome := <-outcomes: + if cancellation := installationConfirmationCancellationError(ctx, connectionClosed); cancellation != nil { + return nil, cancellation + } + if outcome.err != nil { + return nil, &jsonrpc2.Error{Code: jsonrpc2.ErrInternal.Code, Message: outcome.err.Error()} + } + if outcome.decision != InstallationConfirmationDecisionCancel && outcome.decision != InstallationConfirmationDecisionConfirm && outcome.decision != InstallationConfirmationDecisionDecline { + return nil, &jsonrpc2.Error{ + Code: jsonrpc2.ErrInternal.Code, + Message: "Invalid installation confirmation decision", + } + } + response := rpc.InstallationConfirmationResponse{ + ConfirmationID: confirmationID, + Decision: outcome.decision, + ReviewFingerprint: reviewFingerprint, + } + raw, err := json.Marshal(response) + if err != nil { + return nil, &jsonrpc2.Error{ + Code: jsonrpc2.ErrInternal.Code, + Message: fmt.Sprintf("Installation confirmation serialisation failed: %v", err), + } + } + return raw, nil + } +} + +func (a *installationConfirmationAdapter) handlerContext(ctx context.Context, connectionClosed <-chan struct{}) (InstallationConfirmationContext, func()) { + handlerContext, cancel := context.WithCancel(ctx) + watcherDone := make(chan struct{}) + go func() { + select { + case <-connectionClosed: + cancel() + case <-watcherDone: + } + }() + return handlerContext, func() { + close(watcherDone) + cancel() + } +} + +func installationConfirmationCancellationError(ctx context.Context, connectionClosed <-chan struct{}) *jsonrpc2.Error { + select { + case <-ctx.Done(): + return &jsonrpc2.Error{ + Code: installationConfirmationRequestCancelledCode, + Message: "Installation confirmation request cancelled", + } + default: + } + select { + case <-connectionClosed: + return &jsonrpc2.Error{ + Code: jsonrpc2.ErrInternal.Code, + Message: "Installation confirmation connection closed", + } + default: + return nil + } +} diff --git a/go/installation_confirmation_test.go b/go/installation_confirmation_test.go new file mode 100644 index 0000000000..a12be5ac0b --- /dev/null +++ b/go/installation_confirmation_test.go @@ -0,0 +1,685 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +const installationConfirmationFixture = `{ + "confirmationId": "confirmation-a", + "operationId": "operation-a", + "policySessionId": "original-session", + "expiresAt": "2026-09-24T03:00:00Z", + "reviewFingerprint": "fingerprint-a", + "review": { + "resource": "mcp", + "review": { + "action": "install", + "identity": { + "canonicalName": "io.example/server", + "serverName": "example" + }, + "provenance": { + "authority": "cards.example.test", + "validatedAt": "2026-09-24T02:59:00Z", + "cardDigest": { + "algorithm": "sha256-rfc8785", + "value": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + }, + "mediaType": "application/mcp-server-card+json" + }, + "target": { "scope": "user", "configKey": "example" }, + "policy": { "decision": "allowed", "source": "none" }, + "selectedChoice": { + "choiceId": "remote-choice", + "installMethod": "remote", + "endpoint": "https://example.test/mcp", + "transport": "streamable-http", + "requiredValues": [], + "secretPlaceholders": [] + }, + "configurationChange": { + "operation": "add", + "scope": "user", + "configKey": "example", + "changedFields": ["type", "url", "headers", "tools"], + "secretReferences": [] + }, + "inputs": [], + "suppliedSecrets": [], + "secretStorage": "keychain", + "effectiveConfiguration": { + "transport": "streamable-http", + "url": "https://example.test/mcp", + "headers": { "X-Region": "eu" }, + "tools": ["*"] + } + } + } +}` + +const skillInstallationConfirmationFixture = `{ + "confirmationId": "confirmation-skill", + "operationId": "operation-skill", + "policySessionId": "original-session", + "expiresAt": "2026-09-24T03:00:00Z", + "reviewFingerprint": "fingerprint-skill", + "review": { + "resource": "skill", + "review": { + "action": "install", + "name": "demo", + "description": "Demo Skill", + "catalogue": { + "resourceId": "123", + "displayName": "Demo Skill", + "description": "Demo Skill", + "publisher": "Octo", + "source": "https://agentfinder.github.com" + }, + "source": { + "resourceId": "123", + "catalogRevisionId": "1", + "repositoryId": "42", + "repository": "octo/demo", + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "root": "", + "descriptorDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "target": { + "scope": "personal", + "displayLabel": "~/.copilot/skills/demo", + "relativePath": "skills/demo", + "diagnosticsAbsolutePath": "/home/skills/demo" + }, + "installsDisabled": true, + "files": [{ + "path": "SKILL.md", + "sizeBytes": 12, + "mediaType": "text/markdown", + "executable": false, + "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333" + }], + "totalBytes": 12, + "entrypointPath": "SKILL.md", + "entrypointContent": "---\nname: demo\n---" + } + } +}` + +type installationConfirmationCall struct { + request InstallationConfirmationRequest + context InstallationConfirmationContext +} + +type installationConfirmationPeer struct { + t *testing.T + conn net.Conn + reader *bufio.Reader + writeM sync.Mutex +} + +type installationConfirmationMessage struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id,omitempty"` + Method string `json:"method,omitempty"` + Params json.RawMessage `json:"params,omitempty"` + Result json.RawMessage `json:"result,omitempty"` + Error *jsonrpc2.Error `json:"error,omitempty"` +} + +func newInstallationConfirmationHarness(t *testing.T, handler InstallationConfirmationHandler) (*Client, *installationConfirmationPeer) { + t.Helper() + + clientConn, serverConn := net.Pipe() + rpcClient := jsonrpc2.NewClient(clientConn, clientConn) + client := &Client{ + client: rpcClient, + options: ClientOptions{ + InstallationConfirmationHandler: handler, + }, + sessions: make(map[string]*Session), + gitHubTokenProviders: make(map[string]GitHubTokenProvider), + state: stateConnected, + } + client.setupNotificationHandler() + rpcClient.Start() + peer := &installationConfirmationPeer{t: t, conn: serverConn, reader: bufio.NewReader(serverConn)} + t.Cleanup(func() { + client.ForceStop() + _ = serverConn.Close() + _ = clientConn.Close() + }) + return client, peer +} + +func installationConfirmationRequest(t *testing.T, operation string) InstallationConfirmationRequest { + t.Helper() + var request InstallationConfirmationRequest + if err := json.Unmarshal([]byte(installationConfirmationFixture), &request); err != nil { + t.Fatal(err) + } + request.OperationID = operation + request.ConfirmationID = "challenge-" + operation + request.ReviewFingerprint = "fingerprint-" + operation + return request +} + +func (p *installationConfirmationPeer) request(id any, method string, params any) { + p.t.Helper() + p.write(map[string]any{ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + }) +} + +func (p *installationConfirmationPeer) notify(method string, params any) { + p.t.Helper() + p.write(map[string]any{ + "jsonrpc": "2.0", + "method": method, + "params": params, + }) +} + +func (p *installationConfirmationPeer) write(message any) { + p.t.Helper() + body, err := json.Marshal(message) + if err != nil { + p.t.Fatal(err) + } + p.writeM.Lock() + defer p.writeM.Unlock() + if _, err := fmt.Fprintf(p.conn, "Content-Length: %d\r\n\r\n%s", len(body), body); err != nil { + p.t.Fatalf("write frame: %v", err) + } +} + +func (p *installationConfirmationPeer) readResponse(id any) installationConfirmationMessage { + p.t.Helper() + want := fmt.Sprint(id) + for { + message := p.readMessage() + if len(message.ID) == 0 { + continue + } + got, ok := jsonIDString(message.ID) + if ok && got == want { + return message + } + p.t.Fatalf("received response id %s, want %s: %+v", message.ID, want, message) + } +} + +func (p *installationConfirmationPeer) readMessage() installationConfirmationMessage { + p.t.Helper() + if err := p.conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { + p.t.Fatal(err) + } + defer p.conn.SetReadDeadline(time.Time{}) + length := 0 + for { + line, err := p.reader.ReadString('\n') + if err != nil { + p.t.Fatalf("read header: %v", err) + } + line = strings.TrimSpace(line) + if line == "" { + break + } + name, value, ok := strings.Cut(line, ":") + if ok && name == "Content-Length" { + length, err = strconv.Atoi(strings.TrimSpace(value)) + if err != nil { + p.t.Fatal(err) + } + } + } + if length <= 0 || length > 1024*1024 { + p.t.Fatalf("invalid Content-Length %d", length) + } + body := make([]byte, length) + if _, err := io.ReadFull(p.reader, body); err != nil { + p.t.Fatalf("read body: %v", err) + } + var message installationConfirmationMessage + if err := json.Unmarshal(body, &message); err != nil { + p.t.Fatalf("unmarshal message %s: %v", body, err) + } + return message +} + +func jsonIDString(id json.RawMessage) (string, bool) { + decoder := json.NewDecoder(strings.NewReader(string(id))) + decoder.UseNumber() + var value any + if err := decoder.Decode(&value); err != nil { + return "", false + } + switch value := value.(type) { + case json.Number: + return value.String(), true + case string: + return value, true + default: + return "", false + } +} + +func waitInstallationConfirmationCall(t *testing.T, calls <-chan installationConfirmationCall) installationConfirmationCall { + t.Helper() + select { + case call := <-calls: + return call + case <-time.After(10 * time.Second): + t.Fatal("timed out waiting for installation confirmation handler") + return installationConfirmationCall{} + } +} + +func waitForClosed(t *testing.T, ch <-chan struct{}, name string) { + t.Helper() + select { + case <-ch: + case <-time.After(10 * time.Second): + t.Fatalf("timed out waiting for %s", name) + } +} + +func assertContextOpen(t *testing.T, ctx context.Context, name string) { + t.Helper() + select { + case <-ctx.Done(): + t.Fatalf("%s was cancelled", name) + default: + } +} + +func decodeInstallationConfirmationResult(t *testing.T, message installationConfirmationMessage) rpc.InstallationConfirmationResponse { + t.Helper() + if message.Error != nil { + t.Fatalf("unexpected error response: %v", message.Error) + } + var response rpc.InstallationConfirmationResponse + if err := json.Unmarshal(message.Result, &response); err != nil { + t.Fatal(err) + } + return response +} + +func TestInstallationConfirmationCancelledBeforeCallbackDoesNotStartHandler(t *testing.T) { + for i := 0; i < 50; i++ { + called := false + connectionClosed := make(chan struct{}) + adapter := newInstallationConfirmationAdapter(InstallationConfirmationHandlerFunc(func(InstallationConfirmationRequest, InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + called = true + return InstallationConfirmationDecisionConfirm, nil + }), connectionClosed) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + raw, err := json.Marshal(installationConfirmationRequest(t, fmt.Sprintf("pre-cancel-%d", i))) + if err != nil { + t.Fatal(err) + } + result, rpcErr := adapter.handle(ctx, raw) + adapter.close() + + if rpcErr == nil || rpcErr.Code != installationConfirmationRequestCancelledCode { + t.Fatalf("iteration %d: expected request-cancelled response, got result %s error %+v", i, result, rpcErr) + } + if called { + t.Fatalf("iteration %d: handler started after cancellation", i) + } + } +} + +func TestInstallationConfirmationCancellationBeatsDecisionReadyTogether(t *testing.T) { + for i := 0; i < 200; i++ { + connectionClosed := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) + called := make(chan struct{}, 1) + adapter := newInstallationConfirmationAdapter(InstallationConfirmationHandlerFunc(func(InstallationConfirmationRequest, InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + called <- struct{}{} + cancel() + return InstallationConfirmationDecisionConfirm, nil + }), connectionClosed) + + raw, err := json.Marshal(installationConfirmationRequest(t, fmt.Sprintf("race-%d", i))) + if err != nil { + t.Fatal(err) + } + result, rpcErr := adapter.handle(ctx, raw) + adapter.close() + + if rpcErr == nil || rpcErr.Code != installationConfirmationRequestCancelledCode { + t.Fatalf("iteration %d: expected request-cancelled response, got result %s error %+v", i, result, rpcErr) + } + select { + case <-called: + default: + t.Fatalf("iteration %d: handler was not called", i) + } + } +} + +func TestInstallationConfirmationSkillReviewDeserializesAsTypedVariant(t *testing.T) { + var request InstallationConfirmationRequest + if err := json.Unmarshal([]byte(skillInstallationConfirmationFixture), &request); err != nil { + t.Fatal(err) + } + skillReview, ok := request.Review.(*rpc.InstallationReviewSkill) + if !ok { + t.Fatalf("review type = %T, want *rpc.InstallationReviewSkill", request.Review) + } + install, ok := skillReview.Review.(*rpc.SkillInstallationReviewInstall) + if !ok { + t.Fatalf("skill review type = %T, want *rpc.SkillInstallationReviewInstall", skillReview.Review) + } + if install.Name != "demo" || !install.InstallsDisabled { + t.Fatalf("unexpected skill install review: %+v", install) + } +} + +func TestInstallationConfirmationTypedReviewEchoesChallengeAndFingerprint(t *testing.T) { + calls := make(chan installationConfirmationCall, 1) + release := make(chan struct{}) + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + calls <- installationConfirmationCall{request: request, context: context} + mcpReview, ok := request.Review.(*rpc.InstallationReviewMCP) + if !ok { + t.Fatalf("review type = %T, want *rpc.InstallationReviewMCP", request.Review) + } + if mcpReview.Review.Action() != rpc.MCPInstallationReviewActionInstall { + t.Fatalf("review action = %q", mcpReview.Review.Action()) + } + request.ConfirmationID = "mutated-by-handler" + request.ReviewFingerprint = "mutated-by-handler" + <-release + return InstallationConfirmationDecisionConfirm, nil + })) + + peer.request(101, "installations.confirm", installationConfirmationRequest(t, "a")) + call := waitInstallationConfirmationCall(t, calls) + if call.request.OperationID != "a" || call.request.PolicySessionID == nil || *call.request.PolicySessionID != "original-session" { + t.Fatalf("unexpected request identity: %+v", call.request) + } + assertContextOpen(t, call.context, "confirmation context") + + close(release) + response := decodeInstallationConfirmationResult(t, peer.readResponse(101)) + if response.ConfirmationID != "challenge-a" || response.ReviewFingerprint != "fingerprint-a" || response.Decision != InstallationConfirmationDecisionConfirm { + t.Fatalf("unexpected response: %+v", response) + } +} + +func TestInstallationConfirmationConcurrentOutOfOrderAndOtherRPC(t *testing.T) { + decisions := map[string]chan InstallationConfirmationDecision{ + "a": make(chan InstallationConfirmationDecision, 1), + "b": make(chan InstallationConfirmationDecision, 1), + } + calls := make(chan installationConfirmationCall, 2) + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + calls <- installationConfirmationCall{request: request, context: context} + select { + case decision := <-decisions[request.OperationID]: + return decision, nil + case <-context.Done(): + return InstallationConfirmationDecisionConfirm, nil + } + })) + + peer.request(201, "installations.confirm", installationConfirmationRequest(t, "a")) + peer.request(202, "installations.confirm", installationConfirmationRequest(t, "b")) + waitInstallationConfirmationCall(t, calls) + waitInstallationConfirmationCall(t, calls) + + peer.request(203, "gitHubToken.getToken", &rpc.GitHubTokenAcquireRequest{ + RegistrationID: "missing", + Host: "github.com", + Reason: rpc.GitHubTokenAcquireReasonInitial, + }) + tokenResponse := peer.readResponse(203) + if tokenResponse.Error == nil || !strings.Contains(tokenResponse.Error.Message, "unknown GitHub token provider") { + t.Fatalf("expected token provider error while reviews are pending, got %+v", tokenResponse) + } + + decisions["b"] <- InstallationConfirmationDecisionDecline + responseB := decodeInstallationConfirmationResult(t, peer.readResponse(202)) + if responseB.Decision != InstallationConfirmationDecisionDecline { + t.Fatalf("response B decision = %q", responseB.Decision) + } + decisions["a"] <- InstallationConfirmationDecisionConfirm + responseA := decodeInstallationConfirmationResult(t, peer.readResponse(201)) + if responseA.Decision != InstallationConfirmationDecisionConfirm { + t.Fatalf("response A decision = %q", responseA.Decision) + } +} + +func TestInstallationConfirmationCancelRequestRetiresOnlyCancelledReview(t *testing.T) { + decisions := map[string]chan InstallationConfirmationDecision{ + "a": make(chan InstallationConfirmationDecision, 1), + "b": make(chan InstallationConfirmationDecision, 1), + } + calls := make(chan installationConfirmationCall, 2) + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + calls <- installationConfirmationCall{request: request, context: context} + select { + case decision := <-decisions[request.OperationID]: + return decision, nil + case <-context.Done(): + return InstallationConfirmationDecisionConfirm, nil + } + })) + + peer.request(301, "installations.confirm", installationConfirmationRequest(t, "a")) + peer.request(302, "installations.confirm", installationConfirmationRequest(t, "b")) + first := waitInstallationConfirmationCall(t, calls) + second := waitInstallationConfirmationCall(t, calls) + contexts := map[string]InstallationConfirmationContext{ + first.request.OperationID: first.context, + second.request.OperationID: second.context, + } + peer.notify("$/cancelRequest", map[string]any{"id": 301}) + cancelled := peer.readResponse(301) + if cancelled.Error == nil || cancelled.Error.Code != installationConfirmationRequestCancelledCode { + t.Fatalf("expected request-cancelled response, got %+v", cancelled) + } + waitForClosed(t, contexts["a"].Done(), "request A cancellation") + assertContextOpen(t, contexts["b"], "request B context") + + decisions["a"] <- InstallationConfirmationDecisionConfirm + decisions["b"] <- InstallationConfirmationDecisionDecline + responseB := decodeInstallationConfirmationResult(t, peer.readResponse(302)) + if responseB.Decision != InstallationConfirmationDecisionDecline { + t.Fatalf("response B decision = %q", responseB.Decision) + } +} + +func TestInstallationConfirmationStaleUnknownAndStringCancelDontTouchSuccessor(t *testing.T) { + decisions := make(map[string]chan InstallationConfirmationDecision) + calls := make(chan installationConfirmationCall, 2) + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + calls <- installationConfirmationCall{request: request, context: context} + return <-decisions[request.OperationID], nil + })) + + peer.notify("$/cancelRequest", map[string]any{"id": 402}) + decisions["a"] = make(chan InstallationConfirmationDecision, 1) + peer.request(401, "installations.confirm", installationConfirmationRequest(t, "a")) + callA := waitInstallationConfirmationCall(t, calls) + peer.notify("$/cancelRequest", map[string]any{"id": "401"}) + peer.notify("$/cancelRequest", map[string]any{"id": 999}) + assertContextOpen(t, callA.context, "request A context") + decisions["a"] <- InstallationConfirmationDecisionConfirm + responseA := decodeInstallationConfirmationResult(t, peer.readResponse(401)) + if responseA.Decision != InstallationConfirmationDecisionConfirm { + t.Fatalf("response A decision = %q", responseA.Decision) + } + + decisions["b"] = make(chan InstallationConfirmationDecision, 1) + peer.request(402, "installations.confirm", installationConfirmationRequest(t, "b")) + callB := waitInstallationConfirmationCall(t, calls) + assertContextOpen(t, callB.context, "successor request context") + decisions["b"] <- InstallationConfirmationDecisionDecline + responseB := decodeInstallationConfirmationResult(t, peer.readResponse(402)) + if responseB.Decision != InstallationConfirmationDecisionDecline { + t.Fatalf("response B decision = %q", responseB.Decision) + } +} + +func TestInstallationConfirmationConnectionCloseDistinctAndPerConnection(t *testing.T) { + firstCalls := make(chan installationConfirmationCall, 1) + _, firstPeer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + firstCalls <- installationConfirmationCall{request: request, context: context} + <-context.Done() + return InstallationConfirmationDecisionConfirm, nil + })) + _, secondPeer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(InstallationConfirmationRequest, InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + return InstallationConfirmationDecisionDecline, nil + })) + + firstPeer.request(501, "installations.confirm", installationConfirmationRequest(t, "a")) + firstCall := waitInstallationConfirmationCall(t, firstCalls) + if err := firstPeer.conn.Close(); err != nil { + t.Fatal(err) + } + waitForClosed(t, firstCall.context.Done(), "first confirmation context") + + secondPeer.request(502, "installations.confirm", installationConfirmationRequest(t, "b")) + response := decodeInstallationConfirmationResult(t, secondPeer.readResponse(502)) + if response.Decision != InstallationConfirmationDecisionDecline { + t.Fatalf("second response decision = %q", response.Decision) + } +} + +func TestInstallationConfirmationConnectionCloseCancelsAllOutstandingReviews(t *testing.T) { + calls := make(chan installationConfirmationCall, 2) + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + calls <- installationConfirmationCall{request: request, context: context} + <-context.Done() + return InstallationConfirmationDecisionConfirm, nil + })) + + peer.request(551, "installations.confirm", installationConfirmationRequest(t, "a")) + peer.request(552, "installations.confirm", installationConfirmationRequest(t, "b")) + first := waitInstallationConfirmationCall(t, calls) + second := waitInstallationConfirmationCall(t, calls) + contexts := map[string]InstallationConfirmationContext{ + first.request.OperationID: first.context, + second.request.OperationID: second.context, + } + + if err := peer.conn.Close(); err != nil { + t.Fatal(err) + } + waitForClosed(t, contexts["a"].Done(), "request A connection cancellation") + waitForClosed(t, contexts["b"].Done(), "request B connection cancellation") +} + +func TestInstallationConfirmationClientStopRetiresPendingReviews(t *testing.T) { + calls := make(chan installationConfirmationCall, 1) + client, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + calls <- installationConfirmationCall{request: request, context: context} + <-context.Done() + return InstallationConfirmationDecisionConfirm, nil + })) + + peer.request(601, "installations.confirm", installationConfirmationRequest(t, "a")) + call := waitInstallationConfirmationCall(t, calls) + client.ForceStop() + waitForClosed(t, call.context.Done(), "client stop signal") +} + +func TestInstallationConfirmationRefusalsNeverApprove(t *testing.T) { + t.Run("missing handler", func(t *testing.T) { + _, peer := newInstallationConfirmationHarness(t, nil) + peer.request(701, "installations.confirm", installationConfirmationRequest(t, "a")) + response := peer.readResponse(701) + if response.Error == nil || !strings.Contains(response.Error.Message, "No installations client-global handler registered") { + t.Fatalf("expected missing handler error, got %+v", response) + } + }) + + t.Run("invalid review", func(t *testing.T) { + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(InstallationConfirmationRequest, InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + return InstallationConfirmationDecisionConfirm, nil + })) + peer.request(702, "installations.confirm", map[string]any{"confirmationId": 1}) + response := peer.readResponse(702) + if response.Error == nil || response.Error.Code != jsonrpc2.ErrInvalidParams.Code { + t.Fatalf("expected invalid params error, got %+v", response) + } + }) + + t.Run("handler error", func(t *testing.T) { + refusal := errors.New("unknown original operation") + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(InstallationConfirmationRequest, InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + return "", refusal + })) + peer.request(703, "installations.confirm", installationConfirmationRequest(t, "a")) + response := peer.readResponse(703) + if response.Error == nil || !strings.Contains(response.Error.Message, refusal.Error()) { + t.Fatalf("expected handler error, got %+v", response) + } + }) + + t.Run("handler panic", func(t *testing.T) { + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(InstallationConfirmationRequest, InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + panic("review UI failed") + })) + peer.request(704, "installations.confirm", installationConfirmationRequest(t, "a")) + response := peer.readResponse(704) + if response.Error == nil || !strings.Contains(response.Error.Message, "review UI failed") { + t.Fatalf("expected panic error, got %+v", response) + } + }) + + t.Run("unknown decision", func(t *testing.T) { + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(InstallationConfirmationRequest, InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + return InstallationConfirmationDecision("approve"), nil + })) + peer.request(705, "installations.confirm", installationConfirmationRequest(t, "a")) + response := peer.readResponse(705) + if response.Error == nil || !strings.Contains(response.Error.Message, "Invalid installation confirmation decision") { + t.Fatalf("expected invalid decision error, got %+v", response) + } + }) +} + +func TestInstallationConfirmationDoesNotInferOptionalLegacySession(t *testing.T) { + calls := make(chan installationConfirmationCall, 1) + _, peer := newInstallationConfirmationHarness(t, InstallationConfirmationHandlerFunc(func(request InstallationConfirmationRequest, context InstallationConfirmationContext) (InstallationConfirmationDecision, error) { + calls <- installationConfirmationCall{request: request, context: context} + return InstallationConfirmationDecisionDecline, nil + })) + request := installationConfirmationRequest(t, "legacy") + request.PolicySessionID = nil + peer.request(801, "installations.confirm", request) + call := waitInstallationConfirmationCall(t, calls) + if call.request.PolicySessionID != nil { + t.Fatalf("policy session was inferred: %q", *call.request.PolicySessionID) + } + response := decodeInstallationConfirmationResult(t, peer.readResponse(801)) + if response.Decision != InstallationConfirmationDecisionDecline { + t.Fatalf("response decision = %q", response.Decision) + } +} diff --git a/go/internal/e2e/agent_and_compact_rpc_e2e_test.go b/go/internal/e2e/agent_and_compact_rpc_e2e_test.go deleted file mode 100644 index c02a8571d3..0000000000 --- a/go/internal/e2e/agent_and_compact_rpc_e2e_test.go +++ /dev/null @@ -1,374 +0,0 @@ -package e2e - -import ( - "fmt" - "slices" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestAgentSelectionRPCE2E(t *testing.T) { - cliPath := testharness.CLIPath() - if cliPath == "" { - t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.") - } - - t.Run("should list available custom agents", func(t *testing.T) { - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - CustomAgents: []copilot.CustomAgentConfig{ - { - Name: "test-agent", - DisplayName: "Test Agent", - Description: "A test agent", - Prompt: "You are a test agent.", - }, - { - Name: "another-agent", - DisplayName: "Another Agent", - Description: "Another test agent", - Prompt: "You are another agent.", - }, - }, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - result, err := session.RPC.Agent.List(t.Context()) - if err != nil { - t.Fatalf("Failed to list agents: %v", err) - } - - if len(result.Agents) != 2 { - t.Fatalf("Expected 2 agents, got %d", len(result.Agents)) - } - if result.Agents[0].Name != "test-agent" { - t.Errorf("Expected first agent name 'test-agent', got %q", result.Agents[0].Name) - } - if result.Agents[0].DisplayName != "Test Agent" { - t.Errorf("Expected first agent displayName 'Test Agent', got %q", result.Agents[0].DisplayName) - } - if result.Agents[1].Name != "another-agent" { - t.Errorf("Expected second agent name 'another-agent', got %q", result.Agents[1].Name) - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) - - t.Run("should return null when no agent is selected", func(t *testing.T) { - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - CustomAgents: []copilot.CustomAgentConfig{ - { - Name: "test-agent", - DisplayName: "Test Agent", - Description: "A test agent", - Prompt: "You are a test agent.", - }, - }, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - result, err := session.RPC.Agent.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Failed to get current agent: %v", err) - } - - if result.Agent != nil { - t.Errorf("Expected no agent selected, got %v", result.Agent) - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) - - t.Run("should select and get current agent", func(t *testing.T) { - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - CustomAgents: []copilot.CustomAgentConfig{ - { - Name: "test-agent", - DisplayName: "Test Agent", - Description: "A test agent", - Prompt: "You are a test agent.", - }, - }, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - // Select the agent - selectResult, err := session.RPC.Agent.Select(t.Context(), &rpc.AgentSelectRequest{Name: "test-agent"}) - if err != nil { - t.Fatalf("Failed to select agent: %v", err) - } - if selectResult.Agent.Name != "test-agent" { - t.Errorf("Expected selected agent 'test-agent', got %q", selectResult.Agent.Name) - } - if selectResult.Agent.DisplayName != "Test Agent" { - t.Errorf("Expected displayName 'Test Agent', got %q", selectResult.Agent.DisplayName) - } - - // Verify getCurrent returns the selected agent - currentResult, err := session.RPC.Agent.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Failed to get current agent: %v", err) - } - if currentResult.Agent == nil { - t.Fatal("Expected an agent to be selected") - } - if currentResult.Agent.Name != "test-agent" { - t.Errorf("Expected current agent 'test-agent', got %q", currentResult.Agent.Name) - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) - - t.Run("should deselect current agent", func(t *testing.T) { - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - CustomAgents: []copilot.CustomAgentConfig{ - { - Name: "test-agent", - DisplayName: "Test Agent", - Description: "A test agent", - Prompt: "You are a test agent.", - }, - }, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - // Select then deselect - _, err = session.RPC.Agent.Select(t.Context(), &rpc.AgentSelectRequest{Name: "test-agent"}) - if err != nil { - t.Fatalf("Failed to select agent: %v", err) - } - - _, err = session.RPC.Agent.Deselect(t.Context()) - if err != nil { - t.Fatalf("Failed to deselect agent: %v", err) - } - - // Verify no agent is selected - currentResult, err := session.RPC.Agent.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Failed to get current agent: %v", err) - } - if currentResult.Agent != nil { - t.Errorf("Expected no agent selected after deselect, got %v", currentResult.Agent) - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) - - t.Run("should return no custom agents when none configured", func(t *testing.T) { - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - result, err := session.RPC.Agent.List(t.Context()) - if err != nil { - t.Fatalf("Failed to list agents: %v", err) - } - - // The CLI may return built-in/default agents even when no custom agents - // are configured, so just verify none of the known custom agent names appear. - customNames := map[string]bool{"test-agent": true, "another-agent": true} - for _, agent := range result.Agents { - if customNames[agent.Name] { - t.Errorf("Expected no custom agents, but found %q", agent.Name) - } - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) - - t.Run("should call agent reload", func(t *testing.T) { - reloadAgent := copilot.CustomAgentConfig{ - Name: fmt.Sprintf("reload-test-agent-%d", time.Now().UnixNano()), - DisplayName: "Reload Test Agent", - Description: "Used by the agent reload RPC test.", - Prompt: "You are a reload test agent.", - } - - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - CustomAgents: []copilot.CustomAgentConfig{ - reloadAgent, - }, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - before, err := session.RPC.Agent.List(t.Context()) - if err != nil { - t.Fatalf("Failed to list agents: %v", err) - } - assertReloadAgent(t, before.Agents, reloadAgent) - - result, err := session.RPC.Agent.Reload(t.Context()) - if err != nil { - t.Fatalf("Failed to reload agents: %v", err) - } - if result.Agents == nil { - t.Errorf("Expected non-nil Agents after reload") - } - current, err := session.RPC.Agent.List(t.Context()) - if err != nil { - t.Fatalf("Failed to list agents after reload: %v", err) - } - if got, want := agentSummaries(result.Agents), agentSummaries(current.Agents); !slices.Equal(got, want) { - t.Errorf("Expected reload result agents to match current agents.\nGot: %v\nWant: %v", got, want) - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) -} - -func assertReloadAgent(t *testing.T, agents []rpc.AgentInfo, expected copilot.CustomAgentConfig) { - t.Helper() - - var matches []rpc.AgentInfo - for _, agent := range agents { - if agent.Name == expected.Name { - matches = append(matches, agent) - } - } - if len(matches) != 1 { - t.Fatalf("Expected exactly one %q in Agent.List, got %+v", expected.Name, agents) - } - if matches[0].DisplayName != expected.DisplayName { - t.Errorf("Expected reload agent display name %q, got %q", expected.DisplayName, matches[0].DisplayName) - } - if matches[0].Description != expected.Description { - t.Errorf("Expected reload agent description %q, got %q", expected.Description, matches[0].Description) - } -} - -func agentSummaries(agents []rpc.AgentInfo) []string { - summaries := make([]string, len(agents)) - for i, agent := range agents { - summaries[i] = fmt.Sprintf("%s\x00%s", agent.Name, agent.DisplayName) - } - slices.Sort(summaries) - return summaries -} - -func TestSessionCompactionRPCE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - t.Run("should compact session history after messages", func(t *testing.T) { - ctx.ConfigureForTest(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - // Send a message to create some history - _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{ - Prompt: "What is 2+2?", - }) - if err != nil { - t.Fatalf("Failed to send message: %v", err) - } - - // Compact the session - result, err := session.RPC.History.Compact(t.Context()) - if err != nil { - t.Fatalf("Failed to compact session: %v", err) - } - - // Verify result has expected fields (just check it returned valid data) - if result == nil { - t.Fatal("Expected non-nil compact result") - } - }) -} diff --git a/go/internal/e2e/builtin_tools_e2e_test.go b/go/internal/e2e/builtin_tools_e2e_test.go deleted file mode 100644 index 46d3f1dca4..0000000000 --- a/go/internal/e2e/builtin_tools_e2e_test.go +++ /dev/null @@ -1,275 +0,0 @@ -package e2e - -import ( - "context" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" -) - -// 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 SendAndWait default, so give it extra headroom while -// still failing fast on a genuine hang. -const sendTimeout = 120 * time.Second - -func TestBuiltinToolsE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should capture exit code in output", func(t *testing.T) { - ctx.ConfigureForTest(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Run 'echo hello && echo world'. Tell me the exact output.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - content := assistantContent(t, msg) - if !strings.Contains(content, "hello") || !strings.Contains(content, "world") { - t.Fatalf("Expected output to contain hello and world, got %q", content) - } - }) - - t.Run("should capture stderr output", func(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("stderr prompt uses bash syntax") - } - - ctx.ConfigureForTest(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Run 'echo error_msg >&2; sleep 0.5; echo ok' and tell me what stderr said. Reply with just the stderr content.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - if content := assistantContent(t, msg); !strings.Contains(content, "error_msg") { - t.Fatalf("Expected stderr response to contain error_msg, got %q", content) - } - }) - - t.Run("should read file with line range", func(t *testing.T) { - ctx.ConfigureForTest(t) - - if err := os.WriteFile(filepath.Join(ctx.WorkDir, "lines.txt"), []byte("line1\nline2\nline3\nline4\nline5\n"), 0644); err != nil { - t.Fatalf("Failed to write lines.txt: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Read lines 2 through 4 of the file 'lines.txt' in this directory. Tell me what those lines contain.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - content := assistantContent(t, msg) - if !strings.Contains(content, "line2") || !strings.Contains(content, "line4") { - t.Fatalf("Expected response to contain line2 and line4, got %q", content) - } - }) - - t.Run("should handle nonexistent file gracefully", func(t *testing.T) { - ctx.ConfigureForTest(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Try to read the file 'does_not_exist.txt'. If it doesn't exist, say 'FILE_NOT_FOUND'.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - content := strings.ToUpper(assistantContent(t, msg)) - if !strings.Contains(content, "NOT FOUND") && - !strings.Contains(content, "NOT EXIST") && - !strings.Contains(content, "NO SUCH") && - !strings.Contains(content, "FILE_NOT_FOUND") && - !strings.Contains(content, "DOES NOT EXIST") && - !strings.Contains(content, "ERROR") { - t.Fatalf("Expected a not-found style response, got %q", content) - } - }) - - t.Run("should edit a file successfully", func(t *testing.T) { - ctx.ConfigureForTest(t) - - if err := os.WriteFile(filepath.Join(ctx.WorkDir, "edit_me.txt"), []byte("Hello World\nGoodbye World\n"), 0644); err != nil { - t.Fatalf("Failed to write edit_me.txt: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Edit the file 'edit_me.txt': replace 'Hello World' with 'Hi Universe'. Then read it back and tell me its contents.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - if content := assistantContent(t, msg); !strings.Contains(content, "Hi Universe") { - t.Fatalf("Expected response to contain Hi Universe, got %q", content) - } - }) - - t.Run("should create a new file", func(t *testing.T) { - ctx.ConfigureForTest(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Create a file called 'new_file.txt' with the content 'Created by test'. Then read it back to confirm.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - if content := assistantContent(t, msg); !strings.Contains(content, "Created by test") { - t.Fatalf("Expected response to contain Created by test, got %q", content) - } - }) - - t.Run("should search for patterns in files", func(t *testing.T) { - ctx.ConfigureForTest(t) - - if err := os.WriteFile(filepath.Join(ctx.WorkDir, "data.txt"), []byte("apple\nbanana\napricot\ncherry\n"), 0644); err != nil { - t.Fatalf("Failed to write data.txt: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Search for lines starting with 'ap' in the file 'data.txt'. Tell me which lines matched.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - content := assistantContent(t, msg) - if !strings.Contains(content, "apple") || !strings.Contains(content, "apricot") { - t.Fatalf("Expected response to contain apple and apricot, got %q", content) - } - }) - - t.Run("should find files by pattern", func(t *testing.T) { - ctx.ConfigureForTest(t) - - if err := os.MkdirAll(filepath.Join(ctx.WorkDir, "src"), 0755); err != nil { - t.Fatalf("Failed to create src directory: %v", err) - } - if err := os.WriteFile(filepath.Join(ctx.WorkDir, "src", "index.ts"), []byte("export const index = 1;"), 0644); err != nil { - t.Fatalf("Failed to write index.ts: %v", err) - } - if err := os.WriteFile(filepath.Join(ctx.WorkDir, "README.md"), []byte("# Readme"), 0644); err != nil { - t.Fatalf("Failed to write README.md: %v", err) - } - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - sendCtx, cancel := context.WithTimeout(t.Context(), sendTimeout) - defer cancel() - msg, err := session.SendAndWait(sendCtx, copilot.MessageOptions{ - Prompt: "Find all .ts files in this directory (recursively). List the filenames you found.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - - if content := assistantContent(t, msg); !strings.Contains(content, "index.ts") { - t.Fatalf("Expected response to contain index.ts, got %q", content) - } - }) -} - -func assistantContent(t *testing.T, event *copilot.SessionEvent) string { - t.Helper() - - if event == nil { - t.Fatal("Expected assistant message, got nil") - return "" - } - data, ok := event.Data.(*copilot.AssistantMessageData) - if !ok { - t.Fatalf("Expected AssistantMessageData, got %T", event.Data) - } - return data.Content -} diff --git a/go/internal/e2e/client_e2e_test.go b/go/internal/e2e/client_e2e_test.go index d7fc3f06ac..83372d3db0 100644 --- a/go/internal/e2e/client_e2e_test.go +++ b/go/internal/e2e/client_e2e_test.go @@ -162,6 +162,45 @@ func TestClientE2E(t *testing.T) { client.Stop() }) + t.Run("should use configured github host for authentication", func(t *testing.T) { + for _, testCase := range []struct { + name, copilotHost, ghHost, expectedHost string + }{ + {"precedence", "tenant.ghe.example", "fallback.ghe.example", "https://tenant.ghe.example"}, + {"fallback", "", "fallback.ghe.example", "https://fallback.ghe.example"}, + {"default", "", "", "https://github.com"}, + } { + t.Run(testCase.name, func(t *testing.T) { + if testharness.RunInIsolatedProcess(t) { + return + } + ctx := testharness.NewTestContext(t) + ctx.ConfigureWithoutSnapshot(t) + client := ctx.NewClient(func(options *copilot.ClientOptions) { + options.Env = append(options.Env, + "COPILOT_GH_HOST="+testCase.copilotHost, + "GH_HOST="+testCase.ghHost, + ) + }) + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + status, err := client.GetAuthStatus(t.Context()) + if err != nil { + t.Fatalf("Failed to get auth status: %v", err) + } + if !status.IsAuthenticated { + t.Fatalf("Expected authenticated status, got %+v", status) + } + if status.Host == nil || *status.Host != testCase.expectedHost { + t.Errorf("Expected %s auth host, got %v", testCase.expectedHost, status.Host) + } + }) + } + }) + t.Run("should list models when authenticated", func(t *testing.T) { client := copilot.NewClient(&copilot.ClientOptions{ Connection: copilot.StdioConnection{Path: cliPath}, diff --git a/go/internal/e2e/commands_and_elicitation_e2e_test.go b/go/internal/e2e/commands_and_elicitation_e2e_test.go index 77187d165a..92af7bbff1 100644 --- a/go/internal/e2e/commands_and_elicitation_e2e_test.go +++ b/go/internal/e2e/commands_and_elicitation_e2e_test.go @@ -84,54 +84,6 @@ func TestCommandsE2E(t *testing.T) { } }) - t.Run("session commands invoke known builtin returns expected result", func(t *testing.T) { - session, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - defer session.Disconnect() - - builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{ - IncludeBuiltins: rpcPtr(true), - IncludeClientCommands: rpcPtr(false), - IncludeSkills: rpcPtr(false), - }) - if err != nil { - t.Fatalf("Commands.List builtins failed: %v", err) - } - commandName := firstKnownBuiltinCommand(builtinCommands.Commands) - if commandName == "" { - t.Fatalf("Expected a known builtin command, got %+v", builtinCommands.Commands) - } - - result, err := session.RPC.Commands.Invoke(t.Context(), &rpc.CommandsInvokeRequest{Name: commandName}) - if err != nil { - t.Fatalf("Commands.Invoke(%q) failed: %v", commandName, err) - } - switch r := result.(type) { - case *rpc.SlashCommandTextResult: - if strings.TrimSpace(r.Text) == "" { - t.Fatalf("Expected non-empty text result, got %+v", r) - } - case *rpc.SlashCommandSelectSubcommandResult: - if strings.TrimSpace(r.Title) == "" || len(r.Options) == 0 { - t.Fatalf("Expected select-subcommand title and options, got %+v", r) - } - case *rpc.SlashCommandAgentPromptResult: - if strings.TrimSpace(r.DisplayPrompt) == "" || strings.TrimSpace(r.Prompt) == "" { - t.Fatalf("Expected non-empty agent prompt result, got %+v", r) - } - case *rpc.SlashCommandCompletedResult: - if r.Message != nil && strings.TrimSpace(*r.Message) == "" { - t.Fatalf("Expected nil or non-empty completed message, got %+v", r) - } - default: - t.Fatalf("Unexpected slash command result type %T", result) - } - }) - t.Run("session commands execute runs registered command handler", func(t *testing.T) { var captured *copilot.CommandContext session, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ diff --git a/go/internal/e2e/compaction_e2e_test.go b/go/internal/e2e/compaction_e2e_test.go index 29ddfd0b1a..9bf2084e52 100644 --- a/go/internal/e2e/compaction_e2e_test.go +++ b/go/internal/e2e/compaction_e2e_test.go @@ -2,6 +2,7 @@ package e2e import ( "errors" + "os" "strings" "testing" "time" @@ -118,8 +119,40 @@ func TestCompactionE2E(t *testing.T) { } if completeData.CompactionTokensUsed == nil { t.Errorf("Expected compaction tokens-used data") - } else if completeData.CompactionTokensUsed.InputTokens == nil || *completeData.CompactionTokensUsed.InputTokens <= 0 { - t.Errorf("Expected compaction call to consume input tokens, got %v", completeData.CompactionTokensUsed.InputTokens) + } else { + actualInputTokens := completeData.CompactionTokensUsed.InputTokens + // Replay has no prompt usage; recording must preserve the provider's value or absence. + if os.Getenv("GITHUB_ACTIONS") == "true" { + if actualInputTokens != nil && *actualInputTokens != 0 { + t.Errorf("Expected replay compaction input tokens to be zero or unavailable, got %d", *actualInputTokens) + } + } else { + var expectedInputTokens *int64 + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("Failed to get provider responses: %v", err) + } + var compactions []*testharness.CompactionProviderUsage + for _, exchange := range exchanges { + if exchange.CompactionUsage != nil && + completeData.SummaryContent != nil && exchange.CompactionUsage.Summary == *completeData.SummaryContent { + compactions = append(compactions, exchange.CompactionUsage) + } + } + if len(compactions) != 1 { + t.Fatalf("Expected one provider compaction chain for the completed summary, got %d", len(compactions)) + } + expectedInputTokens = compactions[0].InputTokens + if expectedInputTokens == nil { + if actualInputTokens != nil { + t.Errorf("Expected provider input tokens to be unavailable, got %d", *actualInputTokens) + } + } else if actualInputTokens == nil { + t.Errorf("Expected provider input tokens %d, got unavailable", *expectedInputTokens) + } else if *actualInputTokens != *expectedInputTokens { + t.Errorf("Expected provider input tokens %d, got %d", *expectedInputTokens, *actualInputTokens) + } + } } summary := "" if completeData.SummaryContent != nil { @@ -193,62 +226,4 @@ func TestCompactionE2E(t *testing.T) { } }) - t.Run("should return empty handoff summary for fresh session", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - result, err := session.RPC.History.SummarizeForHandoff(t.Context()) - if err != nil { - t.Fatalf("History.SummarizeForHandoff failed: %v", err) - } - if result.Summary != "" { - t.Fatalf("Expected empty handoff summary for fresh session, got %+v", result) - } - }) - - t.Run("should summarize for handoff after non ephemeral log event", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - if err := session.Log(t.Context(), "handoff summary log coverage", nil); err != nil { - t.Fatalf("Session.Log failed: %v", err) - } - - result, err := session.RPC.History.SummarizeForHandoff(t.Context()) - if err != nil { - t.Fatalf("History.SummarizeForHandoff failed: %v", err) - } - _ = result.Summary - }) - - t.Run("should report no op when cancelling compaction without in flight work", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - background, err := session.RPC.History.CancelBackgroundCompaction(t.Context()) - if err != nil { - t.Fatalf("History.CancelBackgroundCompaction failed: %v", err) - } - if background.Cancelled { - t.Fatalf("Expected CancelBackgroundCompaction Cancelled=false, got %+v", background) - } - manual, err := session.RPC.History.AbortManualCompaction(t.Context()) - if err != nil { - t.Fatalf("History.AbortManualCompaction failed: %v", err) - } - if manual.Aborted { - t.Fatalf("Expected AbortManualCompaction Aborted=false, got %+v", manual) - } - }) } diff --git a/go/internal/e2e/e2e_helpers_test.go b/go/internal/e2e/e2e_helpers_test.go new file mode 100644 index 0000000000..cb01760155 --- /dev/null +++ b/go/internal/e2e/e2e_helpers_test.go @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package e2e + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" +) + +func assistantContent(t *testing.T, event *copilot.SessionEvent) string { + t.Helper() + + if event == nil { + t.Fatal("Expected assistant message, got nil") + return "" + } + data, ok := event.Data.(*copilot.AssistantMessageData) + if !ok { + t.Fatalf("Expected AssistantMessageData, got %T", event.Data) + } + return data.Content +} + +func containsString(slice []string, value string) bool { + for _, entry := range slice { + if entry == value { + return true + } + } + return false +} + +func waitForMatchingEvent(session *copilot.Session, eventType copilot.SessionEventType, predicate func(copilot.SessionEvent) bool, description string) func() (*copilot.SessionEvent, error) { + result := make(chan *copilot.SessionEvent, 1) + errCh := make(chan error, 1) + unsubscribe := session.On(func(event copilot.SessionEvent) { + if event.Type() == eventType && predicate(event) { + select { + case result <- &event: + default: + } + } else if event.Type() == copilot.SessionEventTypeSessionError { + msg := "session error" + if data, ok := event.Data.(*copilot.SessionErrorData); ok { + msg = data.Message + } + select { + case errCh <- fmt.Errorf("%s while waiting for %s", msg, description): + default: + } + } + }) + + return func() (*copilot.SessionEvent, error) { + defer unsubscribe() + select { + case event := <-result: + return event, nil + case err := <-errCh: + return nil, err + case <-time.After(30 * time.Second): + return nil, fmt.Errorf("timed out waiting for %s", description) + } + } +} + +func awaitEvent(t *testing.T, await func() (*copilot.SessionEvent, error)) *copilot.SessionEvent { + t.Helper() + event, err := await() + if err != nil { + t.Fatal(err) + } + return event +} + +func randomHex(t *testing.T) string { + t.Helper() + var buf [8]byte + if _, err := rand.Read(buf[:]); err != nil { + t.Fatalf("Failed to generate random bytes: %v", err) + } + return hex.EncodeToString(buf[:]) +} + +func rpcPtr[T any](value T) *T { + return &value +} + +func waitForRPCCondition(t *testing.T, timeout time.Duration, description string, condition func() (bool, error)) { + t.Helper() + deadline := time.Now().Add(timeout) + var lastErr error + for time.Now().Before(deadline) { + ok, err := condition() + if err == nil && ok { + return + } + if err != nil { + lastErr = err + } + time.Sleep(100 * time.Millisecond) + } + if lastErr != nil { + t.Fatalf("Timed out waiting for %s: %v", description, lastErr) + } + t.Fatalf("Timed out waiting for %s", description) +} diff --git a/go/internal/e2e/event_fidelity_e2e_test.go b/go/internal/e2e/event_fidelity_e2e_test.go index 8606da22d9..e2b7226f5b 100644 --- a/go/internal/e2e/event_fidelity_e2e_test.go +++ b/go/internal/e2e/event_fidelity_e2e_test.go @@ -1,11 +1,13 @@ package e2e import ( + "context" "os" "path/filepath" "strings" "sync" "testing" + "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" @@ -134,41 +136,39 @@ func TestEventFidelityE2E(t *testing.T) { } t.Cleanup(func() { _ = session.Disconnect() }) - var mu sync.Mutex - var events []copilot.SessionEvent - session.On(func(event copilot.SessionEvent) { - mu.Lock() - events = append(events, event) - mu.Unlock() - }) + // pending_messages.modified and session.idle are independent notifications. + // Subscribe to both before Send so either delivery order is retained. + pendingModified := testharness.SubscribeToEvent( + session, + copilot.SessionEventTypePendingMessagesModified, + ) + finalAssistant := testharness.SubscribeToFinalAssistantMessage(session) + t.Cleanup(pendingModified.Close) + t.Cleanup(finalAssistant.Close) + + waitCtx, cancel := context.WithTimeout(t.Context(), 60*time.Second) + defer cancel() - // SendAndWait collects everything in one round trip and matches the - // pattern of every other test in this file (and the Rust E2E equivalent), - // avoiding the split fire-and-forget + helper pattern that previously - // made this test prone to flakes. - answer, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + if _, err := session.Send(waitCtx, copilot.MessageOptions{ Prompt: "What is 9+9? Reply with just the number.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) + }); err != nil { + t.Fatalf("Send failed: %v", err) } - snapshot := snapshotEventFidelityEvents(&mu, &events) - - var pendingEvent *copilot.SessionEvent - for i := range snapshot { - if _, ok := snapshot[i].Data.(*copilot.PendingMessagesModifiedData); ok { - pendingEvent = &snapshot[i] - break - } + pendingEvent, err := pendingModified.Wait(waitCtx) + if err != nil { + t.Fatalf("Failed waiting for pending_messages.modified: %v", err) } if pendingEvent == nil { - t.Error("Expected to observe a pending_messages.modified event") + t.Fatal("Expected a pending_messages.modified event") + } + if _, ok := pendingEvent.Data.(*copilot.PendingMessagesModifiedData); !ok { + t.Fatalf("Expected pending_messages.modified data, got %T", pendingEvent.Data) } - if answer == nil { - t.Fatal("Expected SendAndWait to return an assistant message") - return + answer, err := finalAssistant.Wait(waitCtx) + if err != nil { + t.Fatalf("Failed waiting for final assistant message: %v", err) } if ad, ok := answer.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(ad.Content, "18") { t.Errorf("Expected answer to contain '18', got %v", answer.Data) diff --git a/go/internal/e2e/hooks_extended_e2e_test.go b/go/internal/e2e/hooks_extended_e2e_test.go index 5cbba38566..2c804eba99 100644 --- a/go/internal/e2e/hooks_extended_e2e_test.go +++ b/go/internal/e2e/hooks_extended_e2e_test.go @@ -13,13 +13,13 @@ import ( // Mirrors dotnet/test/HookLifecycleAndOutputTests.cs (snapshot category "hooks_extended"). // -// Covers each handler exposed on copilot.SessionHooks: OnPreToolUse, +// Covers the general-purpose handlers on copilot.SessionHooks: OnPreToolUse, // OnPostToolUse, OnPostToolUseFailure, OnUserPromptSubmitted, // OnUserPromptTransformed, OnSessionStart, OnSessionEnd, OnErrorOccurred, -// OnAgentStop. Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt / +// OnAgentStop. Subagent hooks are exercised in subagent_hooks_e2e_test.go. +// Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt / // additionalContext / errorHandling / modifiedArgs / modifiedResult / -// sessionSummary) is asserted alongside hook invocation. If a new handler is -// added to SessionHooks, add a corresponding test here. +// sessionSummary) is asserted alongside hook invocation. func TestHooksExtendedE2E(t *testing.T) { ctx := testharness.NewTestContext(t) client := ctx.NewClient() diff --git a/go/internal/e2e/mcp_oauth_e2e_test.go b/go/internal/e2e/mcp_oauth_e2e_test.go index eb9c72593c..7dfe900319 100644 --- a/go/internal/e2e/mcp_oauth_e2e_test.go +++ b/go/internal/e2e/mcp_oauth_e2e_test.go @@ -296,77 +296,6 @@ func TestMCPOAuthE2E(t *testing.T) { } }) - t.Run("resolve pending MCP OAuth request through RPC", func(t *testing.T) { - testharness.SkipIfInProcess(t, "blocked on github/copilot-agent-runtime#18961 MCP OAuth connection stall") - ctx := testharness.NewTestContext(t) - ctx.ConfigureWithoutSnapshot(t) - client := ctx.NewClient() - defer client.ForceStop() - - baseURL := startOAuthMCPServer(t) - serverName := "oauth-direct-rpc-mcp" - requests := make(chan copilot.MCPAuthRequest, 1) - releaseHandler := make(chan struct{}) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - EnableMCPApps: true, - OnMCPAuthRequest: func(request copilot.MCPAuthRequest, _ copilot.MCPAuthInvocation) (*copilot.MCPAuthResult, error) { - requests <- request - <-releaseHandler - return copilot.MCPAuthResultCancelled(), nil - }, - MCPServers: map[string]copilot.MCPServerConfig{ - serverName: copilot.MCPHTTPServerConfig{ - URL: baseURL + "/mcp", - Tools: []string{"*"}, - }, - }, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { session.Disconnect() }) - - connected := make(chan error, 1) - go func() { - connected <- waitForMCPServerStatusResult(t.Context(), session, serverName, rpc.MCPServerStatusConnected, 60*time.Second) - }() - - var request copilot.MCPAuthRequest - select { - case request = <-requests: - case <-time.After(30 * time.Second): - t.Fatal("Timed out waiting for MCP OAuth request") - } - - tokenType := "Bearer" - expiresIn := int64(3600) - result, err := session.RPC.MCP.Oauth().HandlePendingRequest(t.Context(), &rpc.MCPOauthHandlePendingRequest{ - RequestID: request.RequestID, - Result: rpc.MCPOauthPendingRequestResponseToken{ - AccessToken: expectedMCPOAuthToken, - TokenType: &tokenType, - ExpiresIn: &expiresIn, - }, - }) - if err != nil { - close(releaseHandler) - t.Fatalf("HandlePendingRequest failed: %v", err) - } - close(releaseHandler) - if !result.Success { - t.Fatal("Expected direct MCP OAuth pending request resolution to succeed") - } - - if err := <-connected; err != nil { - t.Fatal(err) - } - requestLog := fetchOAuthMCPRequests(t, baseURL) - if !hasAuthorization(requestLog, "Bearer "+expectedMCPOAuthToken) { - t.Fatal("Expected MCP request with token supplied through direct RPC") - } - }) } type oauthMCPRequest struct { diff --git a/go/internal/e2e/permissions_e2e_test.go b/go/internal/e2e/permissions_e2e_test.go index d46599fa86..ee971b10be 100644 --- a/go/internal/e2e/permissions_e2e_test.go +++ b/go/internal/e2e/permissions_e2e_test.go @@ -826,253 +826,6 @@ func TestPermissionsE2E(t *testing.T) { } }) - t.Run("should configure and update permission paths", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - configuredAllowed := createUniqueRPCWorkDirectory(t, ctx, "configured-allowed") - addedAllowed := createUniqueRPCWorkDirectory(t, ctx, "added-allowed") - newPrimary := createUniqueRPCWorkDirectory(t, ctx, "new-primary") - includeTemp := false - unrestricted := false - configure, err := session.RPC.Permissions.Configure(t.Context(), &rpc.PermissionsConfigureParams{ - ApproveAllToolPermissionRequests: rpcPtr(false), - ApproveAllReadPermissionRequests: rpcPtr(true), - Rules: &rpc.PermissionRulesSet{ - Approved: []rpc.PermissionRule{{Kind: "read", Argument: nil}}, - Denied: []rpc.PermissionRule{{Kind: "write", Argument: nil}}, - }, - Paths: &rpc.PermissionPathsConfig{ - WorkspacePath: &ctx.WorkDir, - AdditionalDirectories: []string{configuredAllowed}, - IncludeTempDirectory: &includeTemp, - Unrestricted: &unrestricted, - }, - URLs: &rpc.PermissionURLsConfig{ - InitialAllowed: []string{"https://example.invalid/permissions-configure"}, - Unrestricted: &unrestricted, - }, - }) - if err != nil { - t.Fatalf("Permissions.Configure failed: %v", err) - } - if !configure.Success { - t.Fatalf("Expected Configure Success=true, got %+v", configure) - } - - configuredList, err := session.RPC.Permissions.Paths().List(t.Context()) - if err != nil { - t.Fatalf("Permissions.Paths.List failed: %v", err) - } - assertRPCPathEqual(t, ctx.WorkDir, configuredList.Primary) - assertRPCContainsPath(t, configuredList.Directories, ctx.WorkDir) - assertRPCContainsPath(t, configuredList.Directories, configuredAllowed) - - add, err := session.RPC.Permissions.Paths().Add(t.Context(), &rpc.PermissionPathsAddParams{Path: addedAllowed}) - if err != nil { - t.Fatalf("Permissions.Paths.Add failed: %v", err) - } - if !add.Success { - t.Fatalf("Expected Paths.Add Success=true, got %+v", add) - } - - allowed, err := session.RPC.Permissions.Paths().IsPathWithinAllowedDirectories(t.Context(), &rpc.PermissionPathsAllowedCheckParams{ - Path: filepath.Join(addedAllowed, "child.txt"), - }) - if err != nil { - t.Fatalf("Permissions.Paths.IsPathWithinAllowedDirectories failed: %v", err) - } - if !allowed.Allowed { - t.Fatalf("Expected path within added allowed directory to be allowed") - } - - updatePrimary, err := session.RPC.Permissions.Paths().UpdatePrimary(t.Context(), &rpc.PermissionPathsUpdatePrimaryParams{Path: newPrimary}) - if err != nil { - t.Fatalf("Permissions.Paths.UpdatePrimary failed: %v", err) - } - if !updatePrimary.Success { - t.Fatalf("Expected UpdatePrimary Success=true, got %+v", updatePrimary) - } - - updatedList, err := session.RPC.Permissions.Paths().List(t.Context()) - if err != nil { - t.Fatalf("Permissions.Paths.List after update failed: %v", err) - } - assertRPCPathEqual(t, newPrimary, updatedList.Primary) - assertRPCContainsPath(t, updatedList.Directories, newPrimary) - - workspaceCheck, err := session.RPC.Permissions.Paths().IsPathWithinWorkspace(t.Context(), &rpc.PermissionPathsWorkspaceCheckParams{ - Path: filepath.Join(newPrimary, "child.txt"), - }) - if err != nil { - t.Fatalf("Permissions.Paths.IsPathWithinWorkspace failed: %v", err) - } - if !workspaceCheck.Allowed { - t.Fatalf("Expected path within new primary workspace to be allowed") - } - }) - - t.Run("should invoke permission state rpc apis", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - pending, err := session.RPC.Permissions.PendingRequests(t.Context()) - if err != nil { - t.Fatalf("Permissions.PendingRequests failed: %v", err) - } - if len(pending.Items) != 0 { - t.Fatalf("Expected no pending permission requests, got %+v", pending.Items) - } - - setRequired, err := session.RPC.Permissions.SetRequired(t.Context(), &rpc.PermissionsSetRequiredRequest{Required: true}) - if err != nil { - t.Fatalf("Permissions.SetRequired(true) failed: %v", err) - } - if !setRequired.Success { - t.Fatalf("Expected SetRequired(true) Success=true") - } - clearRequired, err := session.RPC.Permissions.SetRequired(t.Context(), &rpc.PermissionsSetRequiredRequest{Required: false}) - if err != nil { - t.Fatalf("Permissions.SetRequired(false) failed: %v", err) - } - if !clearRequired.Success { - t.Fatalf("Expected SetRequired(false) Success=true") - } - - promptShown, err := session.RPC.Permissions.NotifyPromptShown(t.Context(), &rpc.PermissionPromptShownNotification{ - Message: "Permission prompt shown from Go SDK E2E", - }) - if err != nil { - t.Fatalf("Permissions.NotifyPromptShown failed: %v", err) - } - if !promptShown.Success { - t.Fatalf("Expected NotifyPromptShown Success=true") - } - - ruleArg := "go-permission-e2e-" + randomHex(t) - rule := rpc.PermissionRule{Kind: "commands", Argument: &ruleArg} - addRule, err := session.RPC.Permissions.ModifyRules(t.Context(), &rpc.PermissionsModifyRulesParams{ - Scope: rpc.PermissionsModifyRulesScopeSession, - Add: []rpc.PermissionRule{rule}, - }) - if err != nil { - t.Fatalf("Permissions.ModifyRules(add) failed: %v", err) - } - if !addRule.Success { - t.Fatalf("Expected ModifyRules(add) Success=true") - } - removeRule, err := session.RPC.Permissions.ModifyRules(t.Context(), &rpc.PermissionsModifyRulesParams{ - Scope: rpc.PermissionsModifyRulesScopeSession, - Remove: []rpc.PermissionRule{rule}, - }) - if err != nil { - t.Fatalf("Permissions.ModifyRules(remove) failed: %v", err) - } - if !removeRule.Success { - t.Fatalf("Expected ModifyRules(remove) Success=true") - } - - enableURLs, err := session.RPC.Permissions.URLs().SetUnrestrictedMode(t.Context(), &rpc.PermissionURLsSetUnrestrictedModeParams{Enabled: true}) - if err != nil { - t.Fatalf("Permissions.URLs.SetUnrestrictedMode(true) failed: %v", err) - } - if !enableURLs.Success { - t.Fatalf("Expected SetUnrestrictedMode(true) Success=true") - } - disableURLs, err := session.RPC.Permissions.URLs().SetUnrestrictedMode(t.Context(), &rpc.PermissionURLsSetUnrestrictedModeParams{Enabled: false}) - if err != nil { - t.Fatalf("Permissions.URLs.SetUnrestrictedMode(false) failed: %v", err) - } - if !disableURLs.Success { - t.Fatalf("Expected SetUnrestrictedMode(false) Success=true") - } - }) - - t.Run("should invoke permission location and folder trust rpc apis", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - locationDirectory := createUniqueRPCWorkDirectory(t, ctx, "permission-location") - trustedDirectory := createUniqueRPCWorkDirectory(t, ctx, "folder-trust") - commandIdentifier := "go-permission-location-" + randomHex(t) - - resolved, err := session.RPC.Permissions.Locations().Resolve(t.Context(), &rpc.PermissionLocationResolveParams{WorkingDirectory: locationDirectory}) - if err != nil { - t.Fatalf("Permissions.Locations.Resolve failed: %v", err) - } - if resolved.LocationType != rpc.PermissionLocationTypeDir { - t.Fatalf("Expected dir location type, got %+v", resolved) - } - assertRPCPathEqual(t, locationDirectory, resolved.LocationKey) - - addToolApproval, err := session.RPC.Permissions.Locations().AddToolApproval(t.Context(), &rpc.PermissionLocationAddToolApprovalParams{ - LocationKey: resolved.LocationKey, - Approval: &rpc.PermissionsLocationsAddToolApprovalDetailsCommands{CommandIdentifiers: []string{commandIdentifier}}, - }) - if err != nil { - t.Fatalf("Permissions.Locations.AddToolApproval failed: %v", err) - } - if !addToolApproval.Success { - t.Fatalf("Expected AddToolApproval Success=true") - } - - applied, err := session.RPC.Permissions.Locations().Apply(t.Context(), &rpc.PermissionLocationApplyParams{WorkingDirectory: locationDirectory}) - if err != nil { - t.Fatalf("Permissions.Locations.Apply failed: %v", err) - } - if applied.LocationType != resolved.LocationType { - t.Fatalf("Expected applied location type %q, got %+v", resolved.LocationType, applied) - } - assertRPCPathEqual(t, resolved.LocationKey, applied.LocationKey) - if applied.AppliedRuleCount < 1 { - t.Fatalf("Expected at least one applied rule, got %+v", applied) - } - var foundRule bool - for _, rule := range applied.AppliedRules { - if rule.Kind == "shell" && rule.Argument != nil && *rule.Argument == commandIdentifier { - foundRule = true - break - } - } - if !foundRule { - t.Fatalf("Expected applied shell rule for %q, got %+v", commandIdentifier, applied.AppliedRules) - } - - initialTrust, err := session.RPC.Permissions.FolderTrust().IsTrusted(t.Context(), &rpc.FolderTrustCheckParams{Path: trustedDirectory}) - if err != nil { - t.Fatalf("Permissions.FolderTrust.IsTrusted(initial) failed: %v", err) - } - if initialTrust.Trusted { - t.Fatalf("Expected new trusted directory to start untrusted") - } - - addTrusted, err := session.RPC.Permissions.FolderTrust().AddTrusted(t.Context(), &rpc.FolderTrustAddParams{Path: trustedDirectory}) - if err != nil { - t.Fatalf("Permissions.FolderTrust.AddTrusted failed: %v", err) - } - if !addTrusted.Success { - t.Fatalf("Expected AddTrusted Success=true") - } - updatedTrust, err := session.RPC.Permissions.FolderTrust().IsTrusted(t.Context(), &rpc.FolderTrustCheckParams{Path: trustedDirectory}) - if err != nil { - t.Fatalf("Permissions.FolderTrust.IsTrusted(updated) failed: %v", err) - } - if !updatedTrust.Trusted { - t.Fatalf("Expected trusted directory to be trusted after AddTrusted") - } - }) } // atomicBool is a tiny helper for concurrent flag updates in handler callbacks. diff --git a/go/internal/e2e/provider_endpoint_e2e_test.go b/go/internal/e2e/provider_endpoint_e2e_test.go deleted file mode 100644 index aad02ca2b5..0000000000 --- a/go/internal/e2e/provider_endpoint_e2e_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package e2e - -import ( - "regexp" - "strings" - "testing" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// 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. -func TestProviderEndpointE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - - client := ctx.NewClient(func(opts *copilot.ClientOptions) { - opts.Env = append(opts.Env, "COPILOT_ALLOW_GET_PROVIDER_ENDPOINT=true") - }) - t.Cleanup(func() { client.ForceStop() }) - - t.Run("returns the BYOK provider endpoint when a custom provider is configured", func(t *testing.T) { - ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Provider: &copilot.ProviderConfig{ - Type: "openai", - WireAPI: "completions", - BaseURL: "https://api.example.test/v1", - APIKey: "byok-secret", - Headers: map[string]string{"X-Custom-Header": "byok-yes"}, - }, - }) - if err != nil { - t.Fatalf("create session: %v", err) - } - // disconnect may fail since the BYOK provider URL is fake. - defer func() { _ = session.Disconnect() }() - - endpoint, err := session.RPC.Provider.GetEndpoint(t.Context()) - if err != nil { - t.Fatalf("getEndpoint: %v", err) - } - - if endpoint.Type != rpc.ProviderEndpointTypeOpenai { - t.Errorf("Type: want %q, got %q", rpc.ProviderEndpointTypeOpenai, endpoint.Type) - } - if endpoint.WireAPI == nil || *endpoint.WireAPI != rpc.ProviderEndpointWireAPICompletions { - t.Errorf("WireAPI: want %q, got %v", rpc.ProviderEndpointWireAPICompletions, endpoint.WireAPI) - } - if endpoint.BaseURL != "https://api.example.test/v1" { - t.Errorf("BaseURL: got %q", endpoint.BaseURL) - } - if endpoint.APIKey == nil || *endpoint.APIKey != "byok-secret" { - t.Errorf("APIKey: got %v", endpoint.APIKey) - } - if got := endpoint.Headers["X-Custom-Header"]; got != "byok-yes" { - t.Errorf("X-Custom-Header: got %q", got) - } - // BYOK sessions never issue a CAPI session token. - if endpoint.SessionToken != nil { - t.Errorf("SessionToken: expected nil, got %+v", endpoint.SessionToken) - } - }) - - t.Run("returns the CAPI provider endpoint for an OAuth-authenticated session", func(t *testing.T) { - ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("create session: %v", err) - } - defer func() { - if err := session.Disconnect(); err != nil { - t.Errorf("disconnect: %v", err) - } - }() - - endpoint, err := session.RPC.Provider.GetEndpoint(t.Context()) - if err != nil { - t.Fatalf("getEndpoint: %v", err) - } - - switch endpoint.Type { - case rpc.ProviderEndpointTypeOpenai, rpc.ProviderEndpointTypeAzure, rpc.ProviderEndpointTypeAnthropic: - default: - t.Errorf("unexpected Type %q", endpoint.Type) - } - // wireApi is omitted for anthropic; otherwise one of the OpenAI shapes. - if endpoint.Type != rpc.ProviderEndpointTypeAnthropic { - if endpoint.WireAPI == nil || - (*endpoint.WireAPI != rpc.ProviderEndpointWireAPICompletions && - *endpoint.WireAPI != rpc.ProviderEndpointWireAPIResponses) { - t.Errorf("unexpected WireAPI %v for type %q", endpoint.WireAPI, endpoint.Type) - } - } - - // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness. - if !strings.HasPrefix(endpoint.BaseURL, "http://") && !strings.HasPrefix(endpoint.BaseURL, "https://") { - t.Errorf("BaseURL not an http(s) URL: %q", endpoint.BaseURL) - } - - // For CAPI OAuth sessions the apiKey is the resolved GitHub bearer. - if endpoint.APIKey == nil || len(*endpoint.APIKey) == 0 { - t.Fatalf("APIKey should be a non-empty string, got %v", endpoint.APIKey) - } - - // Standard CAPI headers must be present, and Authorization is surfaced - // as the runtime sends it (`Bearer `). - if endpoint.Headers["Copilot-Integration-Id"] == "" { - t.Errorf("Copilot-Integration-Id header missing") - } - if ua := endpoint.Headers["User-Agent"]; !regexp.MustCompile(`(?i)Copilot`).MatchString(ua) { - t.Errorf("User-Agent should mention Copilot, got %q", ua) - } - if endpoint.Headers["X-GitHub-Api-Version"] == "" { - t.Errorf("X-GitHub-Api-Version header missing") - } - if !regexp.MustCompile(`[0-9a-f-]{8,}`).MatchString(endpoint.Headers["X-Interaction-Id"]) { - t.Errorf("X-Interaction-Id should match interaction-id format, got %q", endpoint.Headers["X-Interaction-Id"]) - } - if want, got := "Bearer "+*endpoint.APIKey, endpoint.Headers["Authorization"]; want != got { - t.Errorf("Authorization: want %q, got %q", want, got) - } - - // When the omit-modelId 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.SessionToken != nil { - if endpoint.SessionToken.Header != "Copilot-Session-Token" { - t.Errorf("SessionToken.Header: got %q", endpoint.SessionToken.Header) - } - if endpoint.SessionToken.Token == "" { - t.Errorf("SessionToken.Token should be non-empty") - } - if endpoint.SessionToken.ExpiresAt != nil && endpoint.SessionToken.ExpiresAt.IsZero() { - t.Errorf("SessionToken.ExpiresAt should be a valid time when present") - } - } - }) -} diff --git a/go/internal/e2e/rewind_e2e_test.go b/go/internal/e2e/rewind_e2e_test.go deleted file mode 100644 index 701e610775..0000000000 --- a/go/internal/e2e/rewind_e2e_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package e2e - -import ( - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -const ( - rewindFileName = "rewind-sdk.txt" - rewindFileOriginalContent = "Original rewind content" - rewindFilePreparedContent = "Prepared rewind content" - rewindFileContent = "SDK rewind content" -) - -func TestRewindE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should restore tracked file and conversation", func(t *testing.T) { - ctx.ConfigureForTest(t) - filePath := filepath.Join(ctx.WorkDir, rewindFileName) - if err := os.WriteFile(filePath, []byte(rewindFileOriginalContent), 0o600); err != nil { - t.Fatalf("Failed to create original file: %v", err) - } - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-5", - EnableFileChangeTracking: copilot.Bool(true), - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - defer session.Disconnect() - - ready, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ - Prompt: "Use the edit tool to replace the exact contents of " + rewindFileName + " from " + - rewindFileOriginalContent + " to " + rewindFilePreparedContent + - ". After the tool succeeds, reply with exactly SDK_REWIND_READY.", - }) - if err != nil { - t.Fatalf("SendAndWait readiness turn failed: %v", err) - } - readyData, ok := ready.Data.(*copilot.AssistantMessageData) - if !ok || readyData.Content != "SDK_REWIND_READY" { - t.Fatalf("Expected SDK_REWIND_READY response, got %+v", ready) - } - content, err := os.ReadFile(filePath) - if err != nil { - t.Fatalf("Failed to read prepared file: %v", err) - } - if string(content) != rewindFilePreparedContent { - t.Fatalf("Expected file content %q, got %q", rewindFilePreparedContent, content) - } - - response, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ - Prompt: "Use the edit tool to replace the exact contents of " + rewindFileName + " from " + - rewindFilePreparedContent + " to " + rewindFileContent + - ". After the tool succeeds, reply with exactly SDK_REWIND_DONE.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - responseData, ok := response.Data.(*copilot.AssistantMessageData) - if !ok || responseData.Content != "SDK_REWIND_DONE" { - t.Fatalf("Expected SDK_REWIND_DONE response, got %+v", response) - } - content, err = os.ReadFile(filePath) - if err != nil { - t.Fatalf("Failed to read updated file: %v", err) - } - if string(content) != rewindFileContent { - t.Fatalf("Expected file content %q, got %q", rewindFileContent, content) - } - - rewindPoints := waitForRewindPoints(t, session) - if !rewindPoints.FileChangeTrackingEnabled { - t.Fatal("Expected file change tracking to be enabled") - } - if len(rewindPoints.Points) != 2 { - t.Fatalf("Expected two rewind points, got %+v", rewindPoints.Points) - } - rewindPoint := rewindPoints.Points[1] - if !rewindPoint.TurnChangedFiles { - t.Fatalf("Expected the edit turn to report changed files, got %+v", rewindPoint) - } - if !rewindPoint.CanRestoreFiles || rewindPoint.FileCount != 1 { - t.Fatalf("Expected one restorable file, got %+v", rewindPoint) - } - - preview, err := session.RPC.History.PreviewRewind(t.Context(), &rpc.HistoryPreviewRewindRequest{ - EventID: rewindPoint.EventID, - }) - if err != nil { - t.Fatalf("PreviewRewind failed: %v", err) - } - if !preview.Available || len(preview.Files) != 1 { - t.Fatalf("Expected one available preview file, got %+v", preview) - } - assertSameRewindPath(t, filePath, preview.Files[0].Path) - - rewind, err := session.RPC.History.Rewind(t.Context(), &rpc.HistoryRewindRequest{ - EventID: rewindPoint.EventID, - Mode: rpc.HistoryRewindModeConversationAndFiles, - }) - if err != nil { - t.Fatalf("Rewind failed: %v", err) - } - if rewind.Outcome != rpc.HistoryRewindOutcomeSuccess { - t.Fatalf("Expected successful rewind, got %+v", rewind) - } - if rewind.EventsRemoved == nil || *rewind.EventsRemoved < 1 { - t.Fatalf("Expected rewind to remove events, got %+v", rewind) - } - if len(rewind.RestoredFiles) != 1 { - t.Fatalf("Expected one restored file, got %+v", rewind.RestoredFiles) - } - assertSameRewindPath(t, filePath, rewind.RestoredFiles[0]) - content, err = os.ReadFile(filePath) - if err != nil { - t.Fatalf("Failed to read restored file: %v", err) - } - if string(content) != rewindFilePreparedContent { - t.Fatalf("Expected restored file content %q, got %q", rewindFilePreparedContent, content) - } - - events, err := session.GetEvents(t.Context()) - if err != nil { - t.Fatalf("GetEvents failed: %v", err) - } - for _, event := range events { - if event.ID == rewindPoint.EventID { - t.Fatalf("Expected rewound event %q to be removed", rewindPoint.EventID) - } - } - }) -} - -func waitForRewindPoints(t *testing.T, session *copilot.Session) *rpc.HistoryListRewindPointsResult { - t.Helper() - deadline := time.Now().Add(30 * time.Second) - for { - result, err := session.RPC.History.ListRewindPoints(t.Context()) - if err != nil { - t.Fatalf("ListRewindPoints failed: %v", err) - } - if result.UnavailableReason == nil && - len(result.Points) == 2 && - result.Points[1].TurnChangedFiles && - result.Points[1].CanRestoreFiles && - result.Points[1].FileCount == 1 { - return result - } - if time.Now().After(deadline) { - t.Fatalf("Timed out waiting for a restorable rewind point: %+v", result) - } - time.Sleep(100 * time.Millisecond) - } -} - -func assertSameRewindPath(t *testing.T, expected, actual string) { - t.Helper() - expectedPath, err := filepath.Abs(expected) - if err != nil { - t.Fatalf("Failed to resolve expected path: %v", err) - } - actualPath, err := filepath.Abs(actual) - if err != nil { - t.Fatalf("Failed to resolve actual path: %v", err) - } - - expectedPath = filepath.Clean(expectedPath) - actualPath = filepath.Clean(actualPath) - if runtime.GOOS == "windows" { - if !strings.EqualFold(expectedPath, actualPath) { - t.Fatalf("Expected path %q, got %q", expectedPath, actualPath) - } - } else if expectedPath != actualPath { - t.Fatalf("Expected path %q, got %q", expectedPath, actualPath) - } -} diff --git a/go/internal/e2e/rpc_coverage_helpers_test.go b/go/internal/e2e/rpc_coverage_helpers_test.go deleted file mode 100644 index 8c566a6a52..0000000000 --- a/go/internal/e2e/rpc_coverage_helpers_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package e2e - -import ( - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/github/copilot-sdk/go/internal/e2e/testharness" -) - -func rpcPtr[T any](value T) *T { - return &value -} - -func createUniqueRPCWorkDirectory(t *testing.T, ctx *testharness.TestContext, prefix string) string { - t.Helper() - dir := filepath.Join(ctx.WorkDir, prefix+"-"+randomHex(t)) - if err := os.MkdirAll(dir, 0755); err != nil { - t.Fatalf("Failed to create %q: %v", dir, err) - } - return dir -} - -func rpcPathsEqual(expected, actual string) bool { - expected = filepath.Clean(expected) - actual = filepath.Clean(actual) - if runtime.GOOS == "windows" { - return strings.EqualFold(expected, actual) - } - return expected == actual -} - -func assertRPCPathEqual(t *testing.T, expected, actual string) { - t.Helper() - if !rpcPathsEqual(expected, actual) { - t.Fatalf("Expected path %q to equal %q", actual, expected) - } -} - -func assertRPCContainsPath(t *testing.T, paths []string, expected string) { - t.Helper() - for _, path := range paths { - if rpcPathsEqual(expected, path) { - return - } - } - t.Fatalf("Expected paths to contain %q, got %v", expected, paths) -} - -func waitForRPCCondition(t *testing.T, timeout time.Duration, description string, condition func() (bool, error)) { - t.Helper() - deadline := time.Now().Add(timeout) - var lastErr error - for time.Now().Before(deadline) { - ok, err := condition() - if err == nil && ok { - return - } - if err != nil { - lastErr = err - } - time.Sleep(100 * time.Millisecond) - } - if lastErr != nil { - t.Fatalf("Timed out waiting for %s: %v", description, lastErr) - } - t.Fatalf("Timed out waiting for %s", description) -} diff --git a/go/internal/e2e/rpc_e2e_test.go b/go/internal/e2e/rpc_e2e_test.go deleted file mode 100644 index 4380415701..0000000000 --- a/go/internal/e2e/rpc_e2e_test.go +++ /dev/null @@ -1,381 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestRPCE2E(t *testing.T) { - cliPath := testharness.CLIPath() - if cliPath == "" { - t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.") - } - - t.Run("should call RPC.Ping with typed params and result", func(t *testing.T) { - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - result, err := client.RPC.Ping(t.Context(), &rpc.PingRequest{Message: copilot.String("typed rpc test")}) - if err != nil { - t.Fatalf("Failed to call RPC.Ping: %v", err) - } - - if result.Message != "pong: typed rpc test" { - t.Errorf("Expected message 'pong: typed rpc test', got %q", result.Message) - } - - if result.Timestamp.IsZero() { - t.Errorf("Expected non-zero timestamp, got %s", result.Timestamp) - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) - - t.Run("should call RPC.Models.List with typed result", func(t *testing.T) { - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - authStatus, err := client.GetAuthStatus(t.Context()) - if err != nil { - t.Fatalf("Failed to get auth status: %v", err) - } - - if !authStatus.IsAuthenticated { - t.Skip("Not authenticated - skipping models.list test") - } - - result, err := client.RPC.Models.List(t.Context(), nil) - if err != nil { - t.Fatalf("Failed to call RPC.Models.List: %v", err) - } - - if result.Models == nil { - t.Error("Expected models to be defined") - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) - - // account.getQuota is defined in schema but not yet implemented in CLI - t.Run("should call RPC.Account.GetQuota when authenticated", func(t *testing.T) { - t.Skip("account.getQuota not yet implemented in CLI") - - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: cliPath}, - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - authStatus, err := client.GetAuthStatus(t.Context()) - if err != nil { - t.Fatalf("Failed to get auth status: %v", err) - } - - if !authStatus.IsAuthenticated { - t.Skip("Not authenticated - skipping account.getQuota test") - } - - result, err := client.RPC.Account.GetQuota(t.Context(), nil) - if err != nil { - t.Fatalf("Failed to call RPC.Account.GetQuota: %v", err) - } - - if result.QuotaSnapshots == nil { - t.Error("Expected quotaSnapshots to be defined") - } - - if err := client.Stop(); err != nil { - t.Errorf("Expected no errors on stop, got %v", err) - } - }) -} - -func TestSessionRPCE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - // session.model.getCurrent is defined in schema but not yet implemented in CLI - t.Run("should call session.RPC.Model.GetCurrent", func(t *testing.T) { - t.Skip("session.model.getCurrent not yet implemented in CLI") - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-5", - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - result, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Failed to call session.RPC.Model.GetCurrent: %v", err) - } - - if result.ModelID == nil || *result.ModelID == "" { - t.Error("Expected modelId to be defined") - } - }) - - // session.model.switchTo is defined in schema but not yet implemented in CLI - t.Run("should call session.RPC.Model.SwitchTo", func(t *testing.T) { - t.Skip("session.model.switchTo not yet implemented in CLI") - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-5", - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - // Get initial model - before, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Failed to get current model: %v", err) - } - if before.ModelID == nil || *before.ModelID == "" { - t.Error("Expected initial modelId to be defined") - } - - // Switch to a different model with reasoning effort - re := "high" - result, err := session.RPC.Model.SwitchTo(t.Context(), &rpc.ModelSwitchToRequest{ - ModelID: "gpt-4.1", - ReasoningEffort: &re, - }) - if err != nil { - t.Fatalf("Failed to switch model: %v", err) - } - if result.ModelID == nil || *result.ModelID != "gpt-4.1" { - t.Errorf("Expected modelId 'gpt-4.1', got %v", result.ModelID) - } - - // Verify the switch persisted - after, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Failed to get current model after switch: %v", err) - } - if after.ModelID == nil || *after.ModelID != "gpt-4.1" { - t.Errorf("Expected modelId 'gpt-4.1' after switch, got %v", after.ModelID) - } - }) - - // session.model.switchTo is defined in schema but not yet implemented in CLI - t.Run("should call session.SetModel", func(t *testing.T) { - t.Skip("session.model.switchTo not yet implemented in CLI") - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-5", - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - if err := session.SetModel(t.Context(), "gpt-4.1", &copilot.SetModelOptions{ReasoningEffort: copilot.String("high")}); err != nil { - t.Fatalf("SetModel returned error: %v", err) - } - }) - - t.Run("should get and set session mode", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - // Get initial mode (default should be interactive) - initial, err := session.RPC.Mode.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get mode: %v", err) - } - if *initial != rpc.SessionModeInteractive { - t.Errorf("Expected initial mode 'interactive', got %q", *initial) - } - - // Switch to plan mode - _, err = session.RPC.Mode.Set(t.Context(), &rpc.ModeSetRequest{Mode: rpc.SessionModePlan}) - if err != nil { - t.Fatalf("Failed to set mode to plan: %v", err) - } - - // Verify mode persisted - afterPlan, err := session.RPC.Mode.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get mode after plan: %v", err) - } - if *afterPlan != rpc.SessionModePlan { - t.Errorf("Expected mode 'plan' after set, got %q", *afterPlan) - } - - // Switch back to interactive - _, err = session.RPC.Mode.Set(t.Context(), &rpc.ModeSetRequest{Mode: rpc.SessionModeInteractive}) - if err != nil { - t.Fatalf("Failed to set mode to interactive: %v", err) - } - }) - - t.Run("should read, update, and delete plan", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - // Initially plan should not exist - initial, err := session.RPC.Plan.Read(t.Context()) - if err != nil { - t.Fatalf("Failed to read plan: %v", err) - } - if initial.Exists { - t.Error("Expected plan to not exist initially") - } - if initial.Content != nil { - t.Error("Expected content to be nil initially") - } - - // Create/update plan - planContent := "# Test Plan\n\n- Step 1\n- Step 2" - _, err = session.RPC.Plan.Update(t.Context(), &rpc.PlanUpdateRequest{Content: planContent}) - if err != nil { - t.Fatalf("Failed to update plan: %v", err) - } - - // Verify plan exists and has correct content - afterUpdate, err := session.RPC.Plan.Read(t.Context()) - if err != nil { - t.Fatalf("Failed to read plan after update: %v", err) - } - if !afterUpdate.Exists { - t.Error("Expected plan to exist after update") - } - if afterUpdate.Content == nil || *afterUpdate.Content != planContent { - t.Errorf("Expected content %q, got %v", planContent, afterUpdate.Content) - } - - // Delete plan - _, err = session.RPC.Plan.Delete(t.Context()) - if err != nil { - t.Fatalf("Failed to delete plan: %v", err) - } - - // Verify plan is deleted - afterDelete, err := session.RPC.Plan.Read(t.Context()) - if err != nil { - t.Fatalf("Failed to read plan after delete: %v", err) - } - if afterDelete.Exists { - t.Error("Expected plan to not exist after delete") - } - if afterDelete.Content != nil { - t.Error("Expected content to be nil after delete") - } - }) - - t.Run("should create, list, and read workspace files", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - // Initially no files - initialFiles, err := session.RPC.Workspaces.ListFiles(t.Context()) - if err != nil { - t.Fatalf("Failed to list files: %v", err) - } - if len(initialFiles.Files) != 0 { - t.Errorf("Expected no files initially, got %v", initialFiles.Files) - } - - // Create a file - fileContent := "Hello, workspace!" - _, err = session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{ - Path: "test.txt", - Content: fileContent, - }) - if err != nil { - t.Fatalf("Failed to create file: %v", err) - } - - // List files - afterCreate, err := session.RPC.Workspaces.ListFiles(t.Context()) - if err != nil { - t.Fatalf("Failed to list files after create: %v", err) - } - if !containsString(afterCreate.Files, "test.txt") { - t.Errorf("Expected files to contain 'test.txt', got %v", afterCreate.Files) - } - - // Read file - readResult, err := session.RPC.Workspaces.ReadFile(t.Context(), &rpc.WorkspacesReadFileRequest{ - Path: "test.txt", - }) - if err != nil { - t.Fatalf("Failed to read file: %v", err) - } - if readResult.Content != fileContent { - t.Errorf("Expected content %q, got %q", fileContent, readResult.Content) - } - - // Create nested file - _, err = session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{ - Path: "subdir/nested.txt", - Content: "Nested content", - }) - if err != nil { - t.Fatalf("Failed to create nested file: %v", err) - } - - afterNested, err := session.RPC.Workspaces.ListFiles(t.Context()) - if err != nil { - t.Fatalf("Failed to list files after nested: %v", err) - } - if !containsString(afterNested.Files, "test.txt") { - t.Errorf("Expected files to contain 'test.txt', got %v", afterNested.Files) - } - hasNested := false - for _, f := range afterNested.Files { - if strings.Contains(f, "nested.txt") { - hasNested = true - break - } - } - if !hasNested { - t.Errorf("Expected files to contain 'nested.txt', got %v", afterNested.Files) - } - }) -} - -func containsString(slice []string, str string) bool { - for _, s := range slice { - if s == str { - return true - } - } - return false -} diff --git a/go/internal/e2e/rpc_event_log_e2e_test.go b/go/internal/e2e/rpc_event_log_e2e_test.go deleted file mode 100644 index 4e491026c8..0000000000 --- a/go/internal/e2e/rpc_event_log_e2e_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package e2e - -import ( - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -const rpcEventLogTimeout = 30 * time.Second - -// Mirrors dotnet/test/E2E/RpcEventLogE2ETests.cs (snapshot category "rpc_event_log"). -func TestRPCEventLogE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should read persisted events from beginning", func(t *testing.T) { - session := createEventLogSession(t, client) - defer session.Disconnect() - - if _, err := session.RPC.Plan.Update(t.Context(), &rpc.PlanUpdateRequest{Content: "# Event log E2E plan\n- persisted event"}); err != nil { - t.Fatalf("Plan.Update failed: %v", err) - } - - var read *rpc.EventsReadResult - waitForRPCCondition(t, rpcEventLogTimeout, "persisted session.plan_changed event", func() (bool, error) { - var err error - read, err = session.RPC.EventLog.Read(t.Context(), &rpc.EventLogReadRequest{ - Max: rpcPtr(int64(100)), - WaitMs: rpcPtr(int32(0)), - }) - if err != nil { - return false, err - } - for _, event := range read.Events { - if data, ok := event.Data.(*copilot.SessionPlanChangedData); ok && - data.Operation == copilot.PlanChangedOperationCreate && - (event.Ephemeral == nil || !*event.Ephemeral) { - return true, nil - } - } - return false, nil - }) - - if read.CursorStatus != rpc.EventsCursorStatusOk { - t.Fatalf("Expected cursor status ok, got %q", read.CursorStatus) - } - if read.Cursor == "" { - t.Fatal("Expected non-empty cursor") - } - }) - - t.Run("should return tail cursor and read empty when no new events", func(t *testing.T) { - session := createEventLogSession(t, client) - defer session.Disconnect() - - var tail *rpc.EventLogTailResult - var read *rpc.EventsReadResult - waitForRPCCondition(t, rpcEventLogTimeout, "stable empty event log tail", func() (bool, error) { - var err error - tail, err = session.RPC.EventLog.Tail(t.Context()) - if err != nil { - return false, err - } - read, err = session.RPC.EventLog.Read(t.Context(), &rpc.EventLogReadRequest{ - Cursor: &tail.Cursor, - Max: rpcPtr(int64(10)), - WaitMs: rpcPtr(int32(0)), - }) - return err == nil && read.CursorStatus == rpc.EventsCursorStatusOk && len(read.Events) == 0, err - }) - - if tail.Cursor == "" { - t.Fatal("Expected non-empty tail cursor") - } - if len(read.Events) != 0 { - t.Fatalf("Expected no events after tail cursor, got %d", len(read.Events)) - } - if read.HasMore { - t.Fatal("Expected HasMore=false for empty read") - } - }) - - t.Run("should register and release event interest idempotently", func(t *testing.T) { - session := createEventLogSession(t, client) - defer session.Disconnect() - - registered, err := session.RPC.EventLog.RegisterInterest(t.Context(), &rpc.RegisterEventInterestParams{ - EventType: string(copilot.SessionEventTypeSessionTitleChanged), - }) - if err != nil { - t.Fatalf("EventLog.RegisterInterest failed: %v", err) - } - if registered.Handle == "" { - t.Fatal("Expected non-empty event interest handle") - } - - released, err := session.RPC.EventLog.ReleaseInterest(t.Context(), &rpc.ReleaseEventInterestParams{Handle: registered.Handle}) - if err != nil { - t.Fatalf("EventLog.ReleaseInterest failed: %v", err) - } - if !released.Success { - t.Fatal("Expected first ReleaseInterest to succeed") - } - releasedAgain, err := session.RPC.EventLog.ReleaseInterest(t.Context(), &rpc.ReleaseEventInterestParams{Handle: registered.Handle}) - if err != nil { - t.Fatalf("EventLog.ReleaseInterest second call failed: %v", err) - } - if !releasedAgain.Success { - t.Fatal("Expected second ReleaseInterest to be idempotent") - } - }) - - t.Run("should long poll with types filter for title changed event", func(t *testing.T) { - session := createEventLogSession(t, client) - defer session.Disconnect() - - var read *rpc.EventsReadResult - var expectedTitle string - waitForRPCCondition(t, rpcEventLogTimeout, "filtered session.title_changed event", func() (bool, error) { - expectedTitle = "EventLogTitle-" + randomHex(t) - tail, err := session.RPC.EventLog.Tail(t.Context()) - if err != nil { - return false, err - } - resultCh := make(chan *rpc.EventsReadResult, 1) - errCh := make(chan error, 1) - go func() { - result, err := session.RPC.EventLog.Read(t.Context(), &rpc.EventLogReadRequest{ - Cursor: &tail.Cursor, - Max: rpcPtr(int64(10)), - WaitMs: rpcPtr(int32(5000)), - Types: &rpc.EventLogTypes{StringArray: []string{string(copilot.SessionEventTypeSessionTitleChanged)}}, - }) - if err != nil { - errCh <- err - return - } - resultCh <- result - }() - time.Sleep(100 * time.Millisecond) - if _, err := session.RPC.Name.Set(t.Context(), &rpc.NameSetRequest{Name: expectedTitle}); err != nil { - return false, err - } - select { - case err := <-errCh: - return false, err - case read = <-resultCh: - case <-time.After(6 * time.Second): - return false, nil - } - for _, event := range read.Events { - if event.Type() != copilot.SessionEventTypeSessionTitleChanged { - return false, nil - } - if data, ok := event.Data.(*copilot.SessionTitleChangedData); ok && data.Title == expectedTitle { - return true, nil - } - } - return false, nil - }) - - if read.CursorStatus != rpc.EventsCursorStatusOk { - t.Fatalf("Expected cursor status ok, got %q", read.CursorStatus) - } - }) -} - -func createEventLogSession(t *testing.T, client *copilot.Client) *copilot.Session { - t.Helper() - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - return session -} diff --git a/go/internal/e2e/rpc_event_side_effects_e2e_test.go b/go/internal/e2e/rpc_event_side_effects_e2e_test.go deleted file mode 100644 index ef66ec83e5..0000000000 --- a/go/internal/e2e/rpc_event_side_effects_e2e_test.go +++ /dev/null @@ -1,323 +0,0 @@ -package e2e - -import ( - "fmt" - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -const rpcEventSideEffectsTimeout = 30 * time.Second - -// Mirrors dotnet/test/RpcEventSideEffectsE2ETests.cs (snapshot category "rpc_event_side_effects"). -func TestRPCEventSideEffectsE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - t.Run("should emit mode changed event when mode set", func(t *testing.T) { - session := createEventSideEffectsSession(t, client) - defer session.Disconnect() - - awaitModeChanged := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionModeChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionModeChangedData) - return ok && data.NewMode == "plan" && data.PreviousMode == "interactive" - }, - "session.mode_changed event for interactive to plan", - ) - - if _, err := session.RPC.Mode.Set(t.Context(), &rpc.ModeSetRequest{Mode: rpc.SessionModePlan}); err != nil { - t.Fatalf("Failed to set mode to plan: %v", err) - } - - evt := awaitEvent(t, awaitModeChanged) - data := evt.Data.(*copilot.SessionModeChangedData) - if data.NewMode != "plan" || data.PreviousMode != "interactive" { - t.Fatalf("Unexpected mode change: %+v", data) - } - }) - - t.Run("should emit plan changed event for update and delete", func(t *testing.T) { - session := createEventSideEffectsSession(t, client) - defer session.Disconnect() - - awaitCreate := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionPlanChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionPlanChangedData) - return ok && data.Operation == copilot.PlanChangedOperationCreate - }, - "session.plan_changed create event", - ) - if _, err := session.RPC.Plan.Update(t.Context(), &rpc.PlanUpdateRequest{Content: "# Test plan\n- item"}); err != nil { - t.Fatalf("Failed to update plan: %v", err) - } - if data := awaitEvent(t, awaitCreate).Data.(*copilot.SessionPlanChangedData); data.Operation != copilot.PlanChangedOperationCreate { - t.Fatalf("Expected create operation, got %+v", data) - } - - awaitDelete := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionPlanChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionPlanChangedData) - return ok && data.Operation == copilot.PlanChangedOperationDelete - }, - "session.plan_changed delete event", - ) - if _, err := session.RPC.Plan.Delete(t.Context()); err != nil { - t.Fatalf("Failed to delete plan: %v", err) - } - if data := awaitEvent(t, awaitDelete).Data.(*copilot.SessionPlanChangedData); data.Operation != copilot.PlanChangedOperationDelete { - t.Fatalf("Expected delete operation, got %+v", data) - } - }) - - t.Run("should emit plan changed update operation on second update", func(t *testing.T) { - session := createEventSideEffectsSession(t, client) - defer session.Disconnect() - - if _, err := session.RPC.Plan.Update(t.Context(), &rpc.PlanUpdateRequest{Content: "# initial"}); err != nil { - t.Fatalf("Failed to create plan: %v", err) - } - - awaitUpdate := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionPlanChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionPlanChangedData) - return ok && data.Operation == copilot.PlanChangedOperationUpdate - }, - "session.plan_changed update event", - ) - if _, err := session.RPC.Plan.Update(t.Context(), &rpc.PlanUpdateRequest{Content: "# updated content"}); err != nil { - t.Fatalf("Failed to update plan: %v", err) - } - if data := awaitEvent(t, awaitUpdate).Data.(*copilot.SessionPlanChangedData); data.Operation != copilot.PlanChangedOperationUpdate { - t.Fatalf("Expected update operation, got %+v", data) - } - }) - - t.Run("should emit workspace file changed event when file created", func(t *testing.T) { - session := createEventSideEffectsSession(t, client) - defer session.Disconnect() - - path := fmt.Sprintf("side-effect-%d.txt", time.Now().UnixNano()) - awaitChanged := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionWorkspaceFileChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionWorkspaceFileChangedData) - return ok && data.Path == path - }, - "session.workspace_file_changed event", - ) - if _, err := session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{Path: path, Content: "hello"}); err != nil { - t.Fatalf("Failed to create workspace file: %v", err) - } - data := awaitEvent(t, awaitChanged).Data.(*copilot.SessionWorkspaceFileChangedData) - if data.Path != path { - t.Fatalf("Expected path %q, got %+v", path, data) - } - if data.Operation != copilot.WorkspaceFileChangedOperationCreate && data.Operation != copilot.WorkspaceFileChangedOperationUpdate { - t.Fatalf("Unexpected workspace file operation: %+v", data) - } - }) - - t.Run("should emit title changed event when name set", func(t *testing.T) { - session := createEventSideEffectsSession(t, client) - defer session.Disconnect() - - title := fmt.Sprintf("Renamed-%d", time.Now().UnixNano()) - awaitTitleChanged := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionTitleChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionTitleChangedData) - return ok && data.Title == title - }, - "session.title_changed event", - ) - if _, err := session.RPC.Name.Set(t.Context(), &rpc.NameSetRequest{Name: title}); err != nil { - t.Fatalf("Failed to set session name: %v", err) - } - if data := awaitEvent(t, awaitTitleChanged).Data.(*copilot.SessionTitleChangedData); data.Title != title { - t.Fatalf("Expected title %q, got %+v", title, data) - } - }) - - t.Run("should emit snapshot rewind event and remove events on truncate", func(t *testing.T) { - ctx.ConfigureForTest(t) - - session := createEventSideEffectsSession(t, client) - defer session.Disconnect() - - if _, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Say SNAPSHOT_REWIND_TARGET exactly."}); err != nil { - t.Fatalf("Failed to create persisted message: %v", err) - } - - messages, err := session.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read messages: %v", err) - } - userEvent := firstUserMessageEvent(messages) - if userEvent == nil { - t.Fatal("Expected at least one user.message in persisted history") - return - } - targetEventID := userEvent.ID - - awaitRewind := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionSnapshotRewind, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionSnapshotRewindData) - return ok && strings.EqualFold(data.UpToEventID, targetEventID) - }, - "session.snapshot_rewind event", - ) - truncateResult, err := session.RPC.History.Truncate(t.Context(), &rpc.HistoryTruncateRequest{EventID: targetEventID}) - if err != nil { - t.Fatalf("Failed to truncate history: %v", err) - } - if truncateResult.EventsRemoved < 1 { - t.Fatalf("Expected truncate to remove at least one event, got %+v", truncateResult) - } - rewindData := awaitEvent(t, awaitRewind).Data.(*copilot.SessionSnapshotRewindData) - if !strings.EqualFold(rewindData.UpToEventID, targetEventID) { - t.Fatalf("Expected rewind to target %q, got %+v", targetEventID, rewindData) - } - if rewindData.EventsRemoved != truncateResult.EventsRemoved { - t.Fatalf("Expected rewind count %d, got %+v", truncateResult.EventsRemoved, rewindData) - } - - messagesAfter, err := session.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read messages after truncate: %v", err) - } - for _, event := range messagesAfter { - if event.ID == targetEventID { - t.Fatalf("Expected truncated event %q to be removed", targetEventID) - } - } - }) - - t.Run("should allow session use after truncate", func(t *testing.T) { - ctx.ConfigureForTest(t) - - session := createEventSideEffectsSession(t, client) - defer session.Disconnect() - - if _, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Say SNAPSHOT_REWIND_TARGET exactly."}); err != nil { - t.Fatalf("Failed to create persisted message: %v", err) - } - - messages, err := session.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read messages: %v", err) - } - userEvent := firstUserMessageEvent(messages) - if userEvent == nil { - t.Fatal("Expected at least one user.message in persisted history") - return - } - - truncateResult, err := session.RPC.History.Truncate(t.Context(), &rpc.HistoryTruncateRequest{EventID: userEvent.ID}) - if err != nil { - t.Fatalf("Failed to truncate history: %v", err) - } - if truncateResult.EventsRemoved < 1 { - t.Fatalf("Expected truncate to remove at least one event, got %+v", truncateResult) - } - - mode, err := session.RPC.Mode.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get mode after truncate: %v", err) - } - if mode == nil || (*mode != rpc.SessionModeInteractive && *mode != rpc.SessionModePlan && *mode != rpc.SessionModeAutopilot) { - t.Fatalf("Unexpected mode after truncate: %v", mode) - } - workspace, err := session.RPC.Workspaces.GetWorkspace(t.Context()) - if err != nil { - t.Fatalf("Failed to get workspace after truncate: %v", err) - } - if workspace.Workspace == nil { - t.Fatal("Expected workspace metadata after truncate") - } - }) -} - -func createEventSideEffectsSession(t *testing.T, client *copilot.Client) *copilot.Session { - t.Helper() - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - return session -} - -func waitForMatchingEvent(session *copilot.Session, eventType copilot.SessionEventType, predicate func(copilot.SessionEvent) bool, description string) func() (*copilot.SessionEvent, error) { - result := make(chan *copilot.SessionEvent, 1) - errCh := make(chan error, 1) - unsubscribe := session.On(func(event copilot.SessionEvent) { - if event.Type() == eventType && predicate(event) { - select { - case result <- &event: - default: - } - } else if event.Type() == copilot.SessionEventTypeSessionError { - msg := "session error" - if data, ok := event.Data.(*copilot.SessionErrorData); ok { - msg = data.Message - } - select { - case errCh <- fmt.Errorf("%s while waiting for %s", msg, description): - default: - } - } - }) - - return func() (*copilot.SessionEvent, error) { - defer unsubscribe() - select { - case event := <-result: - return event, nil - case err := <-errCh: - return nil, err - case <-time.After(rpcEventSideEffectsTimeout): - return nil, fmt.Errorf("timed out waiting for %s", description) - } - } -} - -func awaitEvent(t *testing.T, await func() (*copilot.SessionEvent, error)) *copilot.SessionEvent { - t.Helper() - event, err := await() - if err != nil { - t.Fatal(err) - } - return event -} - -func firstUserMessageEvent(events []copilot.SessionEvent) *copilot.SessionEvent { - for i := range events { - if _, ok := events[i].Data.(*copilot.UserMessageData); ok { - return &events[i] - } - } - return nil -} diff --git a/go/internal/e2e/rpc_generated_surface_gaps_e2e_test.go b/go/internal/e2e/rpc_generated_surface_gaps_e2e_test.go deleted file mode 100644 index 99d9c0a4b1..0000000000 --- a/go/internal/e2e/rpc_generated_surface_gaps_e2e_test.go +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -package e2e - -import ( - "context" - "encoding/json" - "net" - "reflect" - "runtime" - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/jsonrpc2" - "github.com/github/copilot-sdk/go/rpc" -) - -type generatedRPCGapCase struct { - name string - wire string - signature any - receiver func(*generatedRPCFixture) any - rpcError bool -} - -func TestGeneratedRPCSurfaceGapsE2E(t *testing.T) { - ctx, cancel := testContextWithTimeout(t, 20*time.Second) - defer cancel() - f := newGeneratedRPCFixture(t, ctx) - - server := func(get func(*rpc.ServerRPC) any) func(*generatedRPCFixture) any { - return func(f *generatedRPCFixture) any { return get(f.client.RPC) } - } - session := func(get func(*rpc.SessionRPC) any) func(*generatedRPCFixture) any { - return func(f *generatedRPCFixture) any { return get(f.session.RPC) } - } - - cases := []generatedRPCGapCase{ - {"server catalog search", "catalog.search", (*rpc.ServerCatalogAPI).Search, server(func(r *rpc.ServerRPC) any { return r.Catalog }), true}, - {"server extensions discover", "extensions.discover", (*rpc.ServerExtensionsAPI).Discover, server(func(r *rpc.ServerRPC) any { return r.Extensions }), false}, - {"server hooks discover", "hooks.discover", (*rpc.ServerHooksAPI).Discover, server(func(r *rpc.ServerRPC) any { return r.Hooks }), false}, - {"server llm inference set provider", "llmInference.setProvider", (*rpc.ServerLlmInferenceAPI).SetProvider, server(func(r *rpc.ServerRPC) any { return r.LlmInference }), false}, - {"server managed settings read", "managedSettings.read", (*rpc.ServerManagedSettingsAPI).Read, server(func(r *rpc.ServerRPC) any { return r.ManagedSettings }), false}, - {"server mcp plan install", "mcp.planInstall", (*rpc.ServerMCPAPI).PlanInstall, server(func(r *rpc.ServerRPC) any { return r.MCP }), false}, - {"server models built in catalog", "models.getBuiltInCatalog", (*rpc.ServerModelsAPI).GetBuiltInCatalog, server(func(r *rpc.ServerRPC) any { return r.Models }), false}, - {"server plugins builtin set", "plugins.builtin.set", (*rpc.ServerPluginsBuiltinAPI).Set, server(func(r *rpc.ServerRPC) any { return r.Plugins.Builtin() }), false}, - {"server sessions client metadata", "sessions.getClientMetadata", (*rpc.ServerSessionsAPI).GetClientMetadata, server(func(r *rpc.ServerRPC) any { return r.Sessions }), false}, - {"server sessions persisted events", "sessions.readPersistedEvents", (*rpc.ServerSessionsAPI).ReadPersistedEvents, server(func(r *rpc.ServerRPC) any { return r.Sessions }), false}, - {"server skills disabled", "skills.config.setSkillDisabled", (*rpc.ServerSkillsConfigAPI).SetSkillDisabled, server(func(r *rpc.ServerRPC) any { return r.Skills.Config() }), false}, - {"server extension launch provider", "registerExtensionLaunchProvider", (*rpc.ServerRPC).RegisterExtensionLaunchProvider, server(func(r *rpc.ServerRPC) any { return r }), false}, - {"session agent prompt", "session.agent.setPrompt", (*rpc.AgentAPI).SetPrompt, session(func(r *rpc.SessionRPC) any { return r.Agent }), false}, - {"session autopilot objective state", "session.autopilotObjective.getState", (*rpc.AutopilotObjectiveAPI).GetState, session(func(r *rpc.SessionRPC) any { return r.AutopilotObjective }), false}, - {"session canvas list open", "session.canvas.listOpen", (*rpc.CanvasAPI).ListOpen, session(func(r *rpc.SessionRPC) any { return r.Canvas }), false}, - {"session completion triggers", "session.completions.getTriggerCharacters", (*rpc.CompletionsAPI).GetTriggerCharacters, session(func(r *rpc.SessionRPC) any { return r.Completions }), false}, - {"session content exclusion paths", "session.contentExclusion.checkPaths", (*rpc.ContentExclusionAPI).CheckPaths, session(func(r *rpc.SessionRPC) any { return r.ContentExclusion }), false}, - {"session debug logs", "session.debug.collectLogs", (*rpc.DebugAPI).CollectLogs, session(func(r *rpc.SessionRPC) any { return r.Debug }), false}, - {"session factory agent", "session.factory.agent", (*rpc.FactoryAPI).Agent, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory cancel", "session.factory.cancel", (*rpc.FactoryAPI).Cancel, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory get run", "session.factory.getRun", (*rpc.FactoryAPI).GetRun, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory detail", "session.factory.getRunDetail", (*rpc.FactoryAPI).GetRunDetail, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory progress", "session.factory.getRunProgress", (*rpc.FactoryAPI).GetRunProgress, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory list runs", "session.factory.listRuns", (*rpc.FactoryAPI).ListRuns, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory log", "session.factory.log", (*rpc.FactoryAPI).Log, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory pause", "session.factory.pause", (*rpc.FactoryAPI).Pause, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory resume", "session.factory.resume", (*rpc.FactoryAPI).Resume, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory run", "session.factory.run", (*rpc.FactoryAPI).Run, session(func(r *rpc.SessionRPC) any { return r.Factory }), false}, - {"session factory journal get", "session.factory.journal.get", (*rpc.FactoryJournalAPI).Get, session(func(r *rpc.SessionRPC) any { return r.Factory.Journal() }), false}, - {"session factory journal put", "session.factory.journal.put", (*rpc.FactoryJournalAPI).Put, session(func(r *rpc.SessionRPC) any { return r.Factory.Journal() }), false}, - {"session history clear context", "session.history.clearContext", (*rpc.HistoryAPI).ClearContext, session(func(r *rpc.SessionRPC) any { return r.History }), false}, - {"session limit prediction", "session.limitPrediction.predict", (*rpc.LimitPredictionAPI).Predict, session(func(r *rpc.SessionRPC) any { return r.LimitPrediction }), false}, - {"session mcp loading background", "session.mcp.moveLoadingToBackground", (*rpc.MCPAPI).MoveLoadingToBackground, session(func(r *rpc.SessionRPC) any { return r.MCP }), false}, - {"session mcp restart", "session.mcp.restartServer", (*rpc.MCPAPI).RestartServer, session(func(r *rpc.SessionRPC) any { return r.MCP }), false}, - {"session mcp start", "session.mcp.startServer", (*rpc.MCPAPI).StartServer, session(func(r *rpc.SessionRPC) any { return r.MCP }), false}, - {"session mcp oauth state changed", "session.mcp.oauth.authenticationStateChanged", (*rpc.MCPOauthAPI).AuthenticationStateChanged, session(func(r *rpc.SessionRPC) any { return r.MCP.Oauth() }), false}, - {"session mcp oauth probe", "session.mcp.oauth.probe", (*rpc.MCPOauthAPI).Probe, session(func(r *rpc.SessionRPC) any { return r.MCP.Oauth() }), true}, - {"session mcp oauth respond", "session.mcp.oauth.respond", (*rpc.MCPOauthAPI).Respond, session(func(r *rpc.SessionRPC) any { return r.MCP.Oauth() }), false}, - {"session mcp resources list", "session.mcp.resources.list", (*rpc.MCPResourcesAPI).List, session(func(r *rpc.SessionRPC) any { return r.MCP.Resources() }), false}, - {"session mcp resource templates", "session.mcp.resources.listTemplates", (*rpc.MCPResourcesAPI).ListTemplates, session(func(r *rpc.SessionRPC) any { return r.MCP.Resources() }), false}, - {"session mcp resource read", "session.mcp.resources.read", (*rpc.MCPResourcesAPI).Read, session(func(r *rpc.SessionRPC) any { return r.MCP.Resources() }), false}, - {"session metadata get", "session.metadata.getClientMetadata", (*rpc.MetadataAPI).GetClientMetadata, session(func(r *rpc.SessionRPC) any { return r.Metadata }), false}, - {"session metadata update", "session.metadata.updateClientMetadata", (*rpc.MetadataAPI).UpdateClientMetadata, session(func(r *rpc.SessionRPC) any { return r.Metadata }), false}, - {"session allowed models", "session.model.setAllowedModels", (*rpc.ModelAPI).SetAllowedModels, session(func(r *rpc.SessionRPC) any { return r.Model }), false}, - {"session auto tier", "session.model.switchAutoTier", (*rpc.ModelAPI).SwitchAutoTier, session(func(r *rpc.SessionRPC) any { return r.Model }), false}, - {"session queue duplicate", "session.queue.duplicateAt", (*rpc.QueueAPI).DuplicateAt, session(func(r *rpc.SessionRPC) any { return r.Queue }), false}, - {"session queue insert", "session.queue.insertAt", (*rpc.QueueAPI).InsertAt, session(func(r *rpc.SessionRPC) any { return r.Queue }), false}, - {"session queue move", "session.queue.moveItem", (*rpc.QueueAPI).MoveItem, session(func(r *rpc.SessionRPC) any { return r.Queue }), false}, - {"session queue remove", "session.queue.removeAt", (*rpc.QueueAPI).RemoveAt, session(func(r *rpc.SessionRPC) any { return r.Queue }), false}, - {"session queue send now", "session.queue.sendNow", (*rpc.QueueAPI).SendNow, session(func(r *rpc.SessionRPC) any { return r.Queue }), false}, - {"session queue drain pause", "session.queue.setDrainPaused", (*rpc.QueueAPI).SetDrainPaused, session(func(r *rpc.SessionRPC) any { return r.Queue }), false}, - {"session queue update", "session.queue.updateText", (*rpc.QueueAPI).UpdateText, session(func(r *rpc.SessionRPC) any { return r.Queue }), false}, - {"session sandbox disable", "session.sandbox.disableForSession", (*rpc.SandboxAPI).DisableForSession, session(func(r *rpc.SessionRPC) any { return r.Sandbox }), false}, - {"session sandbox status", "session.sandbox.getEnforcementStatus", (*rpc.SandboxAPI).GetEnforcementStatus, session(func(r *rpc.SessionRPC) any { return r.Sandbox }), false}, - {"session tasks register", "session.tasks.register", (*rpc.TasksAPI).Register, session(func(r *rpc.SessionRPC) any { return r.Tasks }), false}, - {"session tasks update", "session.tasks.update", (*rpc.TasksAPI).Update, session(func(r *rpc.SessionRPC) any { return r.Tasks }), false}, - {"session tools execute", "session.tools.execute", (*rpc.ToolsAPI).Execute, session(func(r *rpc.SessionRPC) any { return r.Tools }), false}, - {"session builtin tool descriptors", "session.tools.getBuiltinDescriptors", (*rpc.ToolsAPI).GetBuiltinDescriptors, session(func(r *rpc.SessionRPC) any { return r.Tools }), false}, - {"session tools set", "session.tools.set", (*rpc.ToolsAPI).Set, session(func(r *rpc.SessionRPC) any { return r.Tools }), false}, - {"session task complete event data", "session.tools.taskCompleteEventData", (*rpc.ToolsAPI).TaskCompleteEventData, session(func(r *rpc.SessionRPC) any { return r.Tools }), false}, - {"session workspace summary", "session.workspaces.addSummary", (*rpc.WorkspacesAPI).AddSummary, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace objective exists", "session.workspaces.autopilotObjectiveExists", (*rpc.WorkspacesAPI).AutopilotObjectiveExists, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace directory", "session.workspaces.createDirectory", (*rpc.WorkspacesAPI).CreateDirectory, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace objective delete", "session.workspaces.deleteAutopilotObjective", (*rpc.WorkspacesAPI).DeleteAutopilotObjective, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace ensure", "session.workspaces.ensure", (*rpc.WorkspacesAPI).Ensure, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace objective read", "session.workspaces.readAutopilotObjective", (*rpc.WorkspacesAPI).ReadAutopilotObjective, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace remove", "session.workspaces.removePath", (*rpc.WorkspacesAPI).RemovePath, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace rename", "session.workspaces.renamePath", (*rpc.WorkspacesAPI).RenamePath, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace stat", "session.workspaces.statFile", (*rpc.WorkspacesAPI).StatFile, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace truncate summaries", "session.workspaces.truncateSummaries", (*rpc.WorkspacesAPI).TruncateSummaries, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace metadata", "session.workspaces.updateMetadata", (*rpc.WorkspacesAPI).UpdateMetadata, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session workspace objective write", "session.workspaces.writeAutopilotObjective", (*rpc.WorkspacesAPI).WriteAutopilotObjective, session(func(r *rpc.SessionRPC) any { return r.Workspaces }), false}, - {"session abort", "session.abort", (*rpc.SessionRPC).Abort, session(func(r *rpc.SessionRPC) any { return r }), false}, - {"session cancel background agents", "session.cancelAllBackgroundAgents", (*rpc.SessionRPC).CancelAllBackgroundAgents, session(func(r *rpc.SessionRPC) any { return r }), false}, - {"session interrupt main turn", "session.interruptMainTurn", (*rpc.SessionRPC).InterruptMainTurn, session(func(r *rpc.SessionRPC) any { return r }), false}, - {"session log", "session.log", (*rpc.SessionRPC).Log, session(func(r *rpc.SessionRPC) any { return r }), false}, - {"session send", "session.send", (*rpc.SessionRPC).Send, session(func(r *rpc.SessionRPC) any { return r }), false}, - {"session send messages", "session.sendMessages", (*rpc.SessionRPC).SendMessages, session(func(r *rpc.SessionRPC) any { return r }), false}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - var captured json.RawMessage - f.server.SetRequestHandler(tc.wire, func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - captured = append(captured[:0], params...) - if tc.rpcError { - return nil, &jsonrpc2.Error{Code: -32042, Message: "synthetic " + tc.wire} - } - return json.RawMessage(generatedRPCResponse(tc.wire)), nil - }) - - method := reflect.ValueOf(tc.receiver(f)).MethodByName(methodName(tc.signature)) - if !method.IsValid() { - t.Fatalf("Method expression %T did not resolve on receiver %T", tc.signature, tc.receiver(f)) - } - args := []reflect.Value{reflect.ValueOf(ctx)} - requestJSON := generatedRPCRequest(tc.wire) - if method.Type().NumIn() == 2 { - if requestJSON == "" { - if !method.Type().IsVariadic() { - args = append(args, reflect.Zero(method.Type().In(1))) - } - } else { - requestType := method.Type().In(1) - if method.Type().IsVariadic() { - requestType = requestType.Elem() - } - request := reflect.New(requestType.Elem()) - if err := json.Unmarshal([]byte(requestJSON), request.Interface()); err != nil { - t.Fatalf("Decode %s typed request: %v", tc.wire, err) - } - args = append(args, request) - } - } - results := method.Call(args) - if len(results) != 2 { - t.Fatalf("Expected two return values, got %d", len(results)) - } - err, _ := results[1].Interface().(error) - if tc.rpcError { - if err == nil || !strings.Contains(err.Error(), "synthetic "+tc.wire) { - t.Fatalf("Expected synthetic RPC error, got %v", err) - } - } else { - if err != nil { - t.Fatalf("%s returned error: %v", tc.wire, err) - } - if results[0].Kind() == reflect.Pointer && results[0].IsNil() { - t.Fatalf("%s returned a nil typed result", tc.wire) - } - assertGeneratedRPCResult(t, tc.wire, results[0].Interface()) - } - - var request map[string]any - if len(captured) != 0 && string(captured) != "null" { - if err := json.Unmarshal(captured, &request); err != nil { - t.Fatalf("Decode %s request %s: %v", tc.wire, captured, err) - } - } - if strings.HasPrefix(tc.wire, "session.") { - if request["sessionId"] != f.session.SessionID { - t.Fatalf("%s sessionId = %#v, want %q; request=%s", tc.wire, request["sessionId"], f.session.SessionID, captured) - } - } else if _, exists := request["sessionId"]; exists { - t.Fatalf("%s unexpectedly serialized sessionId; request=%s", tc.wire, captured) - } - if requestJSON != "" { - var expected map[string]any - if err := json.Unmarshal([]byte(requestJSON), &expected); err != nil { - t.Fatal(err) - } - assertJSONSubset(t, tc.wire+" request", expected, request) - } - }) - } -} - -func generatedRPCRequest(wire string) string { - switch wire { - case "hooks.discover": - return `{"projectPaths":["Q:\\rpc-project"],"excludeHostHooks":true}` - case "mcp.planInstall": - return `{"contract":{"protocolVersion":3,"requiredCapabilities":["mcp-install-planning"]},"source":{"kind":"candidate","candidateHandle":"candidate-1","searchId":"search-1"},"scope":"user"}` - case "plugins.builtin.set": - return `{"paths":["Q:\\rpc-plugins"]}` - case "sessions.getClientMetadata": - return `{"sessionIds":["persisted-session"],"keys":["rpc/key"]}` - case "skills.config.setSkillDisabled": - return `{"name":"skill-one","disabled":true}` - case "session.agent.setPrompt": - return `{"id":"agent-1","prompt":"Use the RPC prompt."}` - case "session.contentExclusion.checkPaths": - return `{"paths":["/tmp/rpc-workspace/file.txt"]}` - case "session.debug.collectLogs": - return `{"destination":{"kind":"directory","outputDirectory":"/tmp/rpc-debug"},"include":{"events":true,"processLogs":false,"shellLogs":true},"additionalEntries":[{"bundlePath":"host/diagnostic.txt","kind":"file","path":"/tmp/diagnostic.txt","required":true}]}` - case "session.factory.run": - return `{"name":"rpc-factory","args":{"input":42},"options":{"limits":{"maxAiCredits":2.5,"maxConcurrentSubagents":2,"maxTotalSubagents":4,"timeoutSeconds":30},"logPhaseNames":true,"notifyOnComplete":false}}` - case "session.factory.resume": - return `{"runId":"factory-run-1","limits":{"maxTotalSubagents":8},"notifyOnComplete":true,"logPhaseNames":false}` - case "session.factory.getRun", "session.factory.pause": - return `{"runId":"factory-run-1"}` - case "session.factory.log": - return `{"runId":"factory-run-1","executionToken":"execution-token-1","lines":[{"kind":"log","seq":7,"text":"Factory progress"}]}` - case "session.factory.agent": - return `{"factoryRunId":"factory-run-1","executionToken":"execution-token-1","prompt":"Complete the RPC task.","opts":{"agent":"explore","label":"rpc-agent","model":"model-a","reasoningEffort":"high"}}` - case "session.factory.journal.get": - return `{"runId":"factory-run-1","executionToken":"execution-token-1","key":"checkpoint"}` - case "session.factory.journal.put": - return `{"runId":"factory-run-1","executionToken":"execution-token-1","key":"checkpoint","resultJson":{"checkpoint":8}}` - case "session.history.clearContext": - return `{"prompt":"Reset context."}` - case "session.limitPrediction.predict": - return `{"clientType":"sdk","modelId":"model-a"}` - case "session.mcp.startServer": - return `{"serverName":"rpc-server","config":{"type":"stdio","command":"node","args":["server.js"]}}` - case "session.mcp.oauth.authenticationStateChanged": - return `{"serverName":"rpc-server","refreshSessionToken":true}` - case "session.mcp.oauth.respond": - return `{"requestId":"oauth-request-1"}` - case "session.mcp.resources.list": - return `{"serverName":"rpc-server","cursor":"resource-cursor"}` - case "session.mcp.resources.listTemplates": - return `{"serverName":"rpc-server","cursor":"template-cursor"}` - case "session.mcp.resources.read": - return `{"serverName":"rpc-server","uri":"file://rpc/resource.txt"}` - case "session.model.setAllowedModels": - return `{"allowedModels":["model-a","model-b"]}` - case "session.model.switchAutoTier": - return `{"autoTier":"intelligence"}` - case "session.sandbox.disableForSession": - return `{"requestId":"sandbox-request-1"}` - case "session.abort": - return `{"reason":"user"}` - case "session.interruptMainTurn": - return `{"flushQueued":true}` - case "session.log": - return `{"message":"RPC log","level":"warning","type":"rpc","ephemeral":true,"url":"https://example.test/rpc","tip":"Inspect the RPC."}` - case "session.tasks.register": - return `{"type":"client","clientTaskId":"client-task-1","description":"RPC task","cancellable":true,"displayName":"RPC Task"}` - case "session.tasks.update": - return `{"id":"task-1","sequence":1,"update":{"kind":"progress","message":"Halfway","percentage":50,"phase":"work","status":"running"}}` - case "session.tools.execute": - return `{"name":"rpc_tool","arguments":{"value":"input"},"toolCallId":"tool-call-1"}` - case "session.tools.getBuiltinDescriptors": - return `{"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}` - case "session.tools.set": - return `{"tools":[{"name":"rpc_external","title":"RPC External","description":"External RPC tool","parameters":{"type":"object"},"isTerminal":false,"overridesBuiltInTool":false,"skipPermission":true}]}` - case "session.tools.taskCompleteEventData": - return `{"toolArgs":{"objectiveId":17},"finalResult":{"resultType":"success","textResultForLlm":"RPC task complete","sessionLog":"Completion logged."}}` - case "session.workspaces.updateMetadata": - return `{"context":{"owner":"rpc-test"},"name":"Updated RPC workspace"}` - case "session.workspaces.ensure": - return `{"context":{"owner":"rpc-test"}}` - case "session.workspaces.statFile": - return `{"path":"folder/file.txt"}` - case "session.workspaces.createDirectory": - return `{"path":"folder/nested","recursive":true}` - case "session.workspaces.renamePath": - return `{"source":"folder/file.txt","destination":"folder/renamed.txt"}` - case "session.workspaces.removePath": - return `{"path":"folder","recursive":true,"force":true}` - case "session.workspaces.addSummary": - return `{"title":"RPC summary","content":"Summary content"}` - case "session.workspaces.truncateSummaries": - return `{"keepCount":2}` - default: - return "" - } -} - -func generatedRPCResponse(wire string) string { - switch wire { - case "hooks.discover": - return `{"hooks":[],"warnings":["rpc-warning"],"errors":[]}` - case "llmInference.setProvider": - return `{"success":true}` - case "managedSettings.read": - return `{"settingsJson":{"policy":"strict"}}` - case "mcp.planInstall": - return `{"kind":"unavailable","message":"The host does not provide installation.","reason":"host-not-available"}` - case "models.getBuiltInCatalog": - return `{"models":[{"id":"built-in-model"}]}` - case "sessions.getClientMetadata": - return `[{"status":"ok","sessionId":"persisted-session","metadata":{"rpc/key":"rpc-value"}}]` - case "session.contentExclusion.checkPaths": - return `{"available":true,"checks":[{"path":"/tmp/rpc-workspace/file.txt","excluded":false}]}` - case "session.debug.collectLogs": - return `{"kind":"directory","path":"/tmp/rpc-debug","entries":[{"bundlePath":"host/diagnostic.txt","sizeBytes":123,"source":"additional"}],"skippedEntries":[{"bundlePath":"host/missing.txt","path":"/tmp/missing.txt","reason":"not found"}]}` - case "session.factory.run", "session.factory.getRun": - return `{"runId":"factory-run-1","status":"running","attempt":1,"result":{"value":"running"},"snapshot":{"step":1}}` - case "session.factory.pause": - return `{"runId":"factory-run-1","status":"paused","attempt":1,"reason":"caller requested pause","snapshot":{"step":2}}` - case "session.factory.resume": - return `{"factoryName":"rpc-factory","run":{"runId":"factory-run-1","status":"running","attempt":2,"snapshot":{"step":3}}}` - case "session.factory.agent": - return `{"result":{"answer":"agent-result"}}` - case "session.factory.journal.get": - return `{"hit":true,"resultJson":{"checkpoint":7}}` - case "session.history.clearContext": - return `{"messagesCleared":4}` - case "session.limitPrediction.predict": - return `{"kind":"unavailable","reason":"insufficient-data"}` - case "session.mcp.moveLoadingToBackground": - return `{"movedToBackground":true}` - case "session.mcp.oauth.respond": - return `{"success":true}` - case "session.mcp.resources.list": - return `{"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": - return `{"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": - return `{"contents":[{"uri":"file://rpc/resource.txt","mimeType":"text/plain","text":"resource-content","_meta":{"audience":"assistant"}}]}` - case "session.metadata.getClientMetadata": - return `{"rpc/key":"rpc-value","rpc/other":"other-value"}` - case "session.model.setAllowedModels": - return `{"allowedModels":["model-a","model-b"],"effectiveAllowedModels":["model-a"],"fallbackModel":"model-a","modelId":"model-a"}` - case "session.model.switchAutoTier": - return `{"status":"applied","activatingAutoTier":"intelligence","effectiveAutoTier":"intelligence","supersededAutoTier":"balance"}` - case "session.sandbox.getEnforcementStatus": - return `{"required":true,"blocked":false,"reason":"managed-policy"}` - case "session.sandbox.disableForSession": - return `{"success":true,"enabled":false}` - case "session.abort": - return `{"success":true}` - case "session.interruptMainTurn": - return `{"interrupted":true}` - case "session.cancelAllBackgroundAgents": - return `3` - case "session.log": - return `{"eventId":"11111111-2222-3333-4444-555555555555"}` - case "session.tasks.register": - return `{"created":true,"reclaimed":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":0,"status":"running"}}` - case "session.tasks.update": - return `{"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"}}` - case "session.tools.execute": - return `{"resultType":"success","textResultForLlm":"executed"}` - case "session.tools.getBuiltinDescriptors": - return `{"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": - return `{"objectiveId":17,"outcome":"completed","reason":"completed","success":true,"summary":"RPC task complete"}` - case "session.workspaces.updateMetadata": - return `{"path":"/tmp/rpc-workspace","workspace":{"id":"workspace-1","cwd":"/tmp/rpc-workspace","name":"Updated RPC workspace","branch":"rpc-branch","client_name":"rpc-client","created_at":"2026-09-18T11:00:00Z","git_root":"/tmp/rpc-workspace","remote_steerable":true}}` - case "session.workspaces.ensure": - return `{"path":"/tmp/rpc-workspace","workspace":{"id":"workspace-1","cwd":"/tmp/rpc-workspace","name":"RPC workspace"}}` - case "session.workspaces.statFile": - return `{"birthtimeMs":1000,"isDirectory":false,"isFile":true,"mtimeMs":2000,"size":42}` - case "session.workspaces.addSummary": - return `{"summary":{"number":3,"title":"RPC summary","content":"Summary content"},"workspace":{"id":"workspace-1","cwd":"/tmp/rpc-workspace","name":"RPC workspace"}}` - case "session.workspaces.truncateSummaries": - return `{"path":"/tmp/rpc-workspace","workspace":{"id":"workspace-1","cwd":"/tmp/rpc-workspace","name":"Truncated RPC workspace"}}` - default: - return `{}` - } -} - -func assertGeneratedRPCResult(t *testing.T, wire string, result any) { - t.Helper() - if wire == "sessions.getClientMetadata" { - entries, ok := result.(*rpc.SessionsGetClientMetadataResult) - if !ok || len(*entries) != 1 { - t.Fatalf("%s result = %#v, want one metadata entry", wire, result) - } - entry, ok := (*entries)[0].(*rpc.SessionsClientMetadataEntryOk) - if !ok || entry.SessionID != "persisted-session" || entry.Metadata["rpc/key"] != "rpc-value" { - t.Fatalf("%s result entry = %#v", wire, (*entries)[0]) - } - return - } - expectedJSON := generatedRPCResponse(wire) - if expectedJSON == "{}" { - return - } - var expected any - if err := json.Unmarshal([]byte(expectedJSON), &expected); err != nil { - t.Fatal(err) - } - actualJSON, err := json.Marshal(result) - if err != nil { - t.Fatalf("Marshal %s typed result: %v", wire, err) - } - var actual any - if err := json.Unmarshal(actualJSON, &actual); err != nil { - t.Fatalf("Decode %s typed result %s: %v", wire, actualJSON, err) - } - assertJSONSubset(t, wire+" result", expected, actual) -} - -func assertJSONSubset(t *testing.T, label string, expected, actual any) { - t.Helper() - switch expected := expected.(type) { - case map[string]any: - actual, ok := actual.(map[string]any) - if !ok { - t.Fatalf("%s = %#v, want object", label, actual) - } - for key, value := range expected { - actualValue, exists := actual[key] - if !exists { - t.Fatalf("%s missing %q in %#v", label, key, actual) - } - assertJSONSubset(t, label+"."+key, value, actualValue) - } - case []any: - actual, ok := actual.([]any) - if !ok || len(actual) != len(expected) { - t.Fatalf("%s = %#v, want %d items", label, actual, len(expected)) - } - for i := range expected { - assertJSONSubset(t, label, expected[i], actual[i]) - } - default: - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("%s = %#v, want %#v", label, actual, expected) - } - } -} - -func methodName(signature any) string { - value := reflect.ValueOf(signature) - function := runtime.FuncForPC(value.Pointer()) - if function == nil { - panic("method expression has no runtime function") - } - name := function.Name() - if dot := strings.LastIndexByte(name, '.'); dot >= 0 { - return strings.TrimSuffix(name[dot+1:], "-fm") - } - panic("unexpected method expression name " + name) -} - -func testContextWithTimeout(t *testing.T, timeout time.Duration) (context.Context, context.CancelFunc) { - t.Helper() - return context.WithTimeout(t.Context(), timeout) -} - -type generatedRPCFixture struct { - client *copilot.Client - session *copilot.Session - server *jsonrpc2.Client - conn net.Conn -} - -func newGeneratedRPCFixture(t *testing.T, ctx context.Context) *generatedRPCFixture { - t.Helper() - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = listener.Close() }) - - type serverConnection struct { - server *jsonrpc2.Client - conn net.Conn - } - ready := make(chan serverConnection, 1) - go func() { - conn, err := listener.Accept() - if err != nil { - return - } - server := jsonrpc2.NewClient(conn, conn) - t.Cleanup(server.Stop) - for method, result := range map[string]string{ - "connect": `{"ok":true,"protocolVersion":3,"version":"test"}`, - "plugins.builtin.set": `{}`, - "session.create": `{"sessionId":"generated-rpc-surface"}`, - "session.options.update": `{"success":true}`, - "session.detach": `{"success":true}`, - } { - server.SetRequestHandler(method, func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return json.RawMessage(result), nil - }) - } - server.Start() - ready <- serverConnection{server: server, conn: conn} - }() - - client := copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.URIConnection{URL: listener.Addr().String()}, - }) - t.Cleanup(client.ForceStop) - session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - SessionID: "generated-rpc-surface", - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatal(err) - } - - select { - case connection := <-ready: - return &generatedRPCFixture{client: client, session: session, server: connection.server, conn: connection.conn} - case <-ctx.Done(): - t.Fatal(ctx.Err()) - return nil - } -} diff --git a/go/internal/e2e/rpc_mcp_and_skills_e2e_test.go b/go/internal/e2e/rpc_mcp_and_skills_e2e_test.go deleted file mode 100644 index 22f53c48a5..0000000000 --- a/go/internal/e2e/rpc_mcp_and_skills_e2e_test.go +++ /dev/null @@ -1,616 +0,0 @@ -package e2e - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/RpcMcpAndSkillsTests.cs (snapshot category "rpc_mcp_and_skills"). -// Tests session-scoped MCP, skills, plugins, and extensions RPCs. -func TestRPCMCPAndSkillsE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - // --yolo auto-approves extension permission gates at the CLI level, - // preventing breakage from new gates (e.g., extension-permission-access). - client := ctx.NewClient(func(o *copilot.ClientOptions) { - stdio := o.Connection.(copilot.StdioConnection) - stdio.Args = []string{"--yolo"} - o.Connection = stdio - }) - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should list and toggle session skills", func(t *testing.T) { - skillName := fmt.Sprintf("session-rpc-skill-%s", randomHex(t)) - skillsDir := createMCPSkillsRPCDirectory(t, ctx.WorkDir, "session-rpc-skills", skillName, "Session skill controlled by RPC.") - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - SkillDirectories: []string{skillsDir}, - DisabledSkills: []string{skillName}, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - disabled, err := session.RPC.Skills.List(t.Context()) - if err != nil { - t.Fatalf("Skills.List (initial) failed: %v", err) - } - assertSkillState(t, disabled, skillName, false) - - if _, err := session.RPC.Skills.Enable(t.Context(), &rpc.SkillsEnableRequest{Name: skillName}); err != nil { - t.Fatalf("Skills.Enable failed: %v", err) - } - enabled, err := session.RPC.Skills.List(t.Context()) - if err != nil { - t.Fatalf("Skills.List (after enable) failed: %v", err) - } - assertSkillState(t, enabled, skillName, true) - - if _, err := session.RPC.Skills.Disable(t.Context(), &rpc.SkillsDisableRequest{Name: skillName}); err != nil { - t.Fatalf("Skills.Disable failed: %v", err) - } - disabledAgain, err := session.RPC.Skills.List(t.Context()) - if err != nil { - t.Fatalf("Skills.List (after disable) failed: %v", err) - } - assertSkillState(t, disabledAgain, skillName, false) - }) - - t.Run("should ensure skills are loaded and list invoked skills", func(t *testing.T) { - skillName := fmt.Sprintf("ensure-rpc-skill-%s", randomHex(t)) - skillsDir := createMCPSkillsRPCDirectory(t, ctx.WorkDir, "session-rpc-skills", skillName, "Skill loaded explicitly by RPC.") - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - SkillDirectories: []string{skillsDir}, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - if _, err := session.RPC.Skills.EnsureLoaded(t.Context()); err != nil { - t.Fatalf("Skills.EnsureLoaded failed: %v", err) - } - loaded, err := session.RPC.Skills.List(t.Context()) - if err != nil { - t.Fatalf("Skills.List failed: %v", err) - } - skill := assertSkillState(t, loaded, skillName, true) - if skill.Description != "Skill loaded explicitly by RPC." { - t.Errorf("Expected description to match, got %q", skill.Description) - } - - invoked, err := session.RPC.Skills.GetInvoked(t.Context()) - if err != nil { - t.Fatalf("Skills.GetInvoked failed: %v", err) - } - if invoked.Skills == nil { - t.Fatal("Expected non-nil invoked skills list") - } - if len(invoked.Skills) != 0 { - t.Fatalf("Expected no invoked skills in fresh session, got %+v", invoked.Skills) - } - }) - - t.Run("should reload session skills", func(t *testing.T) { - skillsDir := filepath.Join(ctx.WorkDir, "reloadable-rpc-skills", randomHex(t)) - if err := os.MkdirAll(skillsDir, 0755); err != nil { - t.Fatalf("Failed to create skills directory: %v", err) - } - skillName := fmt.Sprintf("reload-rpc-skill-%s", randomHex(t)) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - SkillDirectories: []string{skillsDir}, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - before, err := session.RPC.Skills.List(t.Context()) - if err != nil { - t.Fatalf("Skills.List (before) failed: %v", err) - } - for _, skill := range before.Skills { - if skill.Name == skillName { - t.Fatalf("Did not expect %q to be present before creation", skillName) - } - } - - writeSkillFile(t, skillsDir, skillName, "Skill added after session creation.") - - if _, err := session.RPC.Skills.Reload(t.Context()); err != nil { - t.Fatalf("Skills.Reload failed: %v", err) - } - - after, err := session.RPC.Skills.List(t.Context()) - if err != nil { - t.Fatalf("Skills.List (after) failed: %v", err) - } - reloaded := assertSkillState(t, after, skillName, true) - if reloaded != nil && reloaded.Description != "Skill added after session creation." { - t.Errorf("Expected description %q, got %q", "Skill added after session creation.", reloaded.Description) - } - }) - - t.Run("should list mcp servers with configured server", func(t *testing.T) { - const serverName = "rpc-list-mcp-server" - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: testMCPServers(t, serverName), - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - result, err := session.RPC.MCP.List(t.Context()) - if err != nil { - t.Fatalf("MCP.List failed: %v", err) - } - var found bool - for _, server := range result.Servers { - if server.Name == serverName { - found = true - if string(server.Status) == "" { - t.Errorf("Expected non-empty MCP server status, got empty") - } - break - } - } - if !found { - t.Errorf("Expected MCP server %q in result, got %+v", serverName, result.Servers) - } - }) - - t.Run("should set mcp env value mode and remove github server", func(t *testing.T) { - const serverName = "github" - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: testMCPServers(t, serverName), - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - direct, err := session.RPC.MCP.SetEnvValueMode(t.Context(), &rpc.MCPSetEnvValueModeParams{Mode: rpc.MCPSetEnvValueModeDetailsDirect}) - if err != nil { - t.Fatalf("MCP.SetEnvValueMode(direct) failed: %v", err) - } - if direct.Mode != rpc.MCPSetEnvValueModeDetailsDirect { - t.Fatalf("Expected direct env value mode, got %+v", direct) - } - indirect, err := session.RPC.MCP.SetEnvValueMode(t.Context(), &rpc.MCPSetEnvValueModeParams{Mode: rpc.MCPSetEnvValueModeDetailsIndirect}) - if err != nil { - t.Fatalf("MCP.SetEnvValueMode(indirect) failed: %v", err) - } - if indirect.Mode != rpc.MCPSetEnvValueModeDetailsIndirect { - t.Fatalf("Expected indirect env value mode, got %+v", indirect) - } - - removeGitHub, err := session.RPC.MCP.RemoveGitHub(t.Context()) - if err != nil { - t.Fatalf("MCP.RemoveGitHub failed: %v", err) - } - if removeGitHub.Removed { - t.Fatalf("Expected RemoveGitHub=false for explicitly configured server, got %+v", removeGitHub) - } - servers, err := session.RPC.MCP.List(t.Context()) - if err != nil { - t.Fatalf("MCP.List failed: %v", err) - } - var stillConnected bool - for _, server := range servers.Servers { - if server.Name == serverName && server.Status == rpc.MCPServerStatusConnected { - stillConnected = true - break - } - } - if !stillConnected { - t.Fatalf("Expected %q MCP server to remain connected after RemoveGitHub, got %+v", serverName, servers.Servers) - } - }) - - t.Run("should report mcp sampling failure and cancel missing sampling", func(t *testing.T) { - const serverName = "rpc-sampling-server" - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: testMCPServers(t, serverName), - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - - cancelMissing, err := session.RPC.MCP.CancelSamplingExecution(t.Context(), &rpc.MCPCancelSamplingExecutionParams{RequestID: "missing-" + randomHex(t)}) - if err != nil { - t.Fatalf("MCP.CancelSamplingExecution failed: %v", err) - } - if cancelMissing.Cancelled { - t.Fatal("Expected cancelling missing sampling execution to report Cancelled=false") - } - - result, err := session.RPC.MCP.ExecuteSampling(t.Context(), &rpc.MCPExecuteSamplingParams{ - RequestID: "sampling-" + randomHex(t), - ServerName: "missing-sampling-server", - MCPRequestID: "mcp-request-" + randomHex(t), - Request: rpc.MCPExecuteSamplingRequest{}, - }) - if err != nil { - assertRPCError(t, "MCP.ExecuteSampling", func() error { return err }, "sampling") - return - } - if result.Action != rpc.MCPSamplingExecutionActionFailure { - t.Fatalf("Expected sampling failure action, got %+v", result) - } - if result.Result != nil || result.Error == nil || strings.TrimSpace(*result.Error) == "" { - t.Fatalf("Expected failure error without result, got %+v", result) - } - if strings.Contains(strings.ToLower(*result.Error), "unhandled method") { - t.Fatalf("Expected implemented sampling error, got %+v", result) - } - }) - - t.Run("should list plugins", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - result, err := session.RPC.Plugins.List(t.Context()) - if err != nil { - t.Fatalf("Plugins.List failed: %v", err) - } - if result.Plugins == nil { - t.Error("Expected non-nil Plugins list") - } - for i, plugin := range result.Plugins { - if strings.TrimSpace(plugin.Name) == "" { - t.Errorf("Plugin[%d] has empty Name", i) - } - } - }) - - t.Run("should list extensions", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - result, err := session.RPC.Extensions.List(t.Context()) - if err != nil { - t.Fatalf("Extensions.List failed: %v", err) - } - if result.Extensions == nil { - t.Error("Expected non-nil Extensions list") - } - for i, ext := range result.Extensions { - if strings.TrimSpace(ext.ID) == "" { - t.Errorf("Extension[%d] has empty ID", i) - } - if strings.TrimSpace(ext.Name) == "" { - t.Errorf("Extension[%d] has empty Name", i) - } - } - }) - - t.Run("should round trip MCP app host context", func(t *testing.T) { - mcpAppsClient := createMCPAppsClient(ctx) - t.Cleanup(func() { mcpAppsClient.ForceStop() }) - session, err := mcpAppsClient.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - displayMode := rpc.MCPAppsSetHostContextDetailsDisplayModeInline - platform := rpc.MCPAppsSetHostContextDetailsPlatformDesktop - theme := rpc.MCPAppsSetHostContextDetailsThemeDark - if _, err := session.RPC.MCP.Apps().SetHostContext(t.Context(), &rpc.MCPAppsSetHostContextRequest{ - Context: rpc.MCPAppsSetHostContextDetails{ - AvailableDisplayModes: []rpc.MCPAppsSetHostContextDetailsAvailableDisplayMode{ - rpc.MCPAppsSetHostContextDetailsAvailableDisplayModeInline, - rpc.MCPAppsSetHostContextDetailsAvailableDisplayModeFullscreen, - }, - DisplayMode: &displayMode, - Locale: rpcPtr("en-GB"), - Platform: &platform, - Theme: &theme, - TimeZone: rpcPtr("Etc/UTC"), - UserAgent: rpcPtr("go-sdk-e2e"), - }, - }); err != nil { - t.Fatalf("MCP.Apps.SetHostContext failed: %v", err) - } - - result, err := session.RPC.MCP.Apps().GetHostContext(t.Context()) - if err != nil { - t.Fatalf("MCP.Apps.GetHostContext failed: %v", err) - } - if result.Context.DisplayMode == nil || string(*result.Context.DisplayMode) != "inline" || - result.Context.Locale == nil || *result.Context.Locale != "en-GB" || - result.Context.Platform == nil || string(*result.Context.Platform) != "desktop" || - result.Context.Theme == nil || string(*result.Context.Theme) != "dark" || - result.Context.TimeZone == nil || *result.Context.TimeZone != "Etc/UTC" || - result.Context.UserAgent == nil || *result.Context.UserAgent != "go-sdk-e2e" { - t.Fatalf("Unexpected MCP app host context: %+v", result.Context) - } - if len(result.Context.AvailableDisplayModes) != 2 { - t.Fatalf("Expected two available display modes, got %+v", result.Context.AvailableDisplayModes) - } - }) - - t.Run("should diagnose and report mcp app capability errors", func(t *testing.T) { - const serverName = "rpc-apps-server" - const otherServerName = "rpc-apps-other-server" - servers := testMCPServers(t, serverName, otherServerName) - if stdio, ok := servers[serverName].(copilot.MCPStdioServerConfig); ok { - stdio.Env = map[string]string{"MCP_APP_RPC_VALUE": "from-app-rpc"} - servers[serverName] = stdio - } - - mcpAppsClient := createMCPAppsClient(ctx) - t.Cleanup(func() { mcpAppsClient.ForceStop() }) - session, err := mcpAppsClient.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: servers, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - waitForMCPServerStatus(t, session, otherServerName, rpc.MCPServerStatusConnected) - - diagnose, err := session.RPC.MCP.Apps().Diagnose(t.Context(), &rpc.MCPAppsDiagnoseRequest{ServerName: serverName}) - if err != nil { - t.Fatalf("MCP.Apps.Diagnose failed: %v", err) - } - if !diagnose.Server.Connected || diagnose.Server.ToolCount < 1 { - t.Fatalf("Expected connected MCP app diagnose result with tools, got %+v", diagnose) - } - - assertMCPAppsResultOrImplementedError(t, "MCP.Apps.ListTools(self)", func() (any, error) { - return session.RPC.MCP.Apps().ListTools(t.Context(), &rpc.MCPAppsListToolsRequest{ - ServerName: serverName, - OriginServerName: serverName, - }) - }) - assertMCPAppsResultOrImplementedError(t, "MCP.Apps.ListTools(other)", func() (any, error) { - return session.RPC.MCP.Apps().ListTools(t.Context(), &rpc.MCPAppsListToolsRequest{ - ServerName: serverName, - OriginServerName: otherServerName, - }) - }) - assertMCPAppsResultOrImplementedError(t, "MCP.Apps.CallTool", func() (any, error) { - return session.RPC.MCP.Apps().CallTool(t.Context(), &rpc.MCPAppsCallToolRequest{ - ServerName: serverName, - OriginServerName: serverName, - ToolName: "get_env", - Arguments: map[string]any{"name": "MCP_APP_RPC_VALUE"}, - }) - }) - }) - - t.Run("should report error when mcp app resource is not available", func(t *testing.T) { - const serverName = "rpc-apps-resource-server" - mcpAppsClient := createMCPAppsClient(ctx) - t.Cleanup(func() { mcpAppsClient.ForceStop() }) - session, err := mcpAppsClient.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: testMCPServers(t, serverName), - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - - _, err = session.RPC.MCP.Apps().ReadResource(t.Context(), &rpc.MCPAppsReadResourceRequest{ - ServerName: serverName, - URI: "ui://missing-resource", - }) - if err == nil { - t.Fatal("Expected missing MCP app resource to fail") - } - text := strings.ToLower(err.Error()) - if strings.Contains(text, "unhandled method") || - (!strings.Contains(text, "resource") && !strings.Contains(text, "not found") && !strings.Contains(text, "method not found")) { - t.Fatalf("Expected implemented missing-resource error, got %v", err) - } - }) - - t.Run("should report error when mcp host is not initialized", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - assertRPCError(t, "MCP.Enable", func() error { - _, e := session.RPC.MCP.Enable(t.Context(), &rpc.MCPEnableRequest{ServerName: "missing-server"}) - return e - }, "no mcp host initialized") - assertRPCError(t, "MCP.Disable", func() error { - _, e := session.RPC.MCP.Disable(t.Context(), &rpc.MCPDisableRequest{ServerName: "missing-server"}) - return e - }, "no mcp host initialized") - assertRPCError(t, "MCP.Reload", func() error { - _, e := session.RPC.MCP.Reload(t.Context()) - return e - }, "mcp config reload not available") - assertRPCError(t, "MCP.Oauth.Login", func() error { - _, e := session.RPC.MCP.Oauth().Login(t.Context(), &rpc.MCPOauthLoginRequest{ServerName: "missing-server"}) - return e - }, "mcp host is not available") - }) - - t.Run("should report error when mcp oauth server is not configured", func(t *testing.T) { - const serverName = "configured-stdio-server" - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: testMCPServers(t, serverName), - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - - assertRPCError(t, "MCP.Oauth.Login", func() error { - _, e := session.RPC.MCP.Oauth().Login(t.Context(), &rpc.MCPOauthLoginRequest{ServerName: "missing-server"}) - return e - }, "is not configured") - }) - - t.Run("should report error when mcp oauth server is not remote", func(t *testing.T) { - const serverName = "configured-stdio-server" - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: testMCPServers(t, serverName), - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - - force := true - clientName := "SDK E2E" - callback := "Done" - assertRPCError(t, "MCP.Oauth.Login", func() error { - _, e := session.RPC.MCP.Oauth().Login(t.Context(), &rpc.MCPOauthLoginRequest{ - ServerName: serverName, - ForceReauth: &force, - ClientName: &clientName, - CallbackSuccessMessage: &callback, - }) - return e - }, "not a remote server") - }) - - t.Run("should report error when extensions are not available", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - assertRPCError(t, "Extensions.Enable", func() error { - _, e := session.RPC.Extensions.Enable(t.Context(), &rpc.ExtensionsEnableRequest{ID: "missing-extension"}) - return e - }, "extensions not available") - assertRPCError(t, "Extensions.Disable", func() error { - _, e := session.RPC.Extensions.Disable(t.Context(), &rpc.ExtensionsDisableRequest{ID: "missing-extension"}) - return e - }, "extensions not available") - assertRPCError(t, "Extensions.Reload", func() error { - _, e := session.RPC.Extensions.Reload(t.Context()) - return e - }, "extensions not available") - }) -} - -// createMCPSkillsRPCDirectory creates a unique skills directory containing a single -// SKILL.md and returns the parent directory suitable for SkillDirectories. -func createMCPSkillsRPCDirectory(t *testing.T, workDir, baseName, skillName, description string) string { - t.Helper() - skillsDir := filepath.Join(workDir, baseName, randomHex(t)) - if err := os.MkdirAll(skillsDir, 0755); err != nil { - t.Fatalf("Failed to create skills directory: %v", err) - } - writeSkillFile(t, skillsDir, skillName, description) - return skillsDir -} - -func writeSkillFile(t *testing.T, skillsDir, skillName, description string) { - t.Helper() - skillSubdir := filepath.Join(skillsDir, skillName) - if err := os.MkdirAll(skillSubdir, 0755); err != nil { - t.Fatalf("Failed to create skill subdirectory: %v", err) - } - content := fmt.Sprintf("---\nname: %s\ndescription: %s\n---\n\n# %s\n\nThis skill is used by RPC E2E tests.\n", skillName, description, skillName) - if err := os.WriteFile(filepath.Join(skillSubdir, "SKILL.md"), []byte(content), 0644); err != nil { - t.Fatalf("Failed to write SKILL.md: %v", err) - } -} - -// assertSkillState finds a skill by name in the list and asserts it has the -// expected enabled state, returning the matched skill (or nil if not found). -func assertSkillState(t *testing.T, list *rpc.SkillList, name string, enabled bool) *rpc.Skill { - t.Helper() - var matched *rpc.Skill - count := 0 - for i, skill := range list.Skills { - if skill.Name == name { - count++ - matched = &list.Skills[i] - } - } - if count != 1 { - t.Fatalf("Expected exactly 1 skill named %q, found %d", name, count) - } - if matched.Enabled != enabled { - t.Errorf("Expected skill %q Enabled=%t, got %t", name, enabled, matched.Enabled) - } - if matched.Path == nil || !strings.HasSuffix(strings.ReplaceAll(*matched.Path, "\\", "/"), strings.Join([]string{name, "SKILL.md"}, "/")) { - t.Errorf("Expected skill path to end with %s/SKILL.md, got %v", name, matched.Path) - } - return matched -} - -func createMCPAppsClient(ctx *testharness.TestContext) *copilot.Client { - return ctx.NewClient(func(opts *copilot.ClientOptions) { - opts.Env = append(opts.Env, "COPILOT_MCP_APPS=true", "MCP_APPS=true") - }) -} - -func assertMCPAppsResultOrImplementedError(t *testing.T, name string, action func() (any, error)) { - t.Helper() - result, err := action() - if err == nil { - if result == nil { - t.Fatalf("%s returned nil result", name) - } - switch value := result.(type) { - case *rpc.MCPAppsListToolsResult: - if value.Tools == nil { - t.Fatalf("%s returned nil Tools", name) - } - case *rpc.SessionMCPAppsCallToolResult: - if value == nil { - t.Fatalf("%s returned nil CallTool result", name) - } - } - return - } - - text := strings.ToLower(err.Error()) - if strings.Contains(text, "unhandled method") || - (!strings.Contains(text, "mcp-apps") && !strings.Contains(text, "capability") && !strings.Contains(text, "visibility")) { - t.Fatalf("Expected %s to return an implemented MCP apps error, got %v", name, err) - } -} - -func assertRPCError(t *testing.T, name string, action func() error, expectedSubstring string) { - t.Helper() - err := action() - if err == nil { - t.Errorf("Expected %s to fail with error containing %q, got nil", name, expectedSubstring) - return - } - if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(expectedSubstring)) { - t.Errorf("Expected %s error to contain %q, got %v", name, expectedSubstring, err) - } -} diff --git a/go/internal/e2e/rpc_mcp_config_e2e_test.go b/go/internal/e2e/rpc_mcp_config_e2e_test.go deleted file mode 100644 index b761c8ee0f..0000000000 --- a/go/internal/e2e/rpc_mcp_config_e2e_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package e2e - -import ( - "fmt" - "testing" - - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/RpcMcpConfigTests.cs (snapshot category "rpc_mcp_config"). -// Tests server-scoped MCP configuration management via MCP.Config.* RPCs. -func TestRPCMCPConfigE2E(t *testing.T) { - t.Run("should call server MCP config rpcs", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - serverName := fmt.Sprintf("sdk-test-%s", randomHex(t)) - - baseConfig := &rpc.MCPServerConfigStdio{ - Command: "node", - Args: []string{"-v"}, - } - updatedConfig := &rpc.MCPServerConfigStdio{ - Command: "node", - Args: []string{"--version"}, - } - - initial, err := client.RPC.MCP.Config().List(t.Context()) - if err != nil { - t.Fatalf("MCP.Config.List (initial) failed: %v", err) - } - if _, present := initial.Servers[serverName]; present { - t.Fatalf("Did not expect %q to be present initially", serverName) - } - - // Best-effort cleanup if a subtest assertion fails mid-flight. - t.Cleanup(func() { - _, _ = client.RPC.MCP.Config().Remove(t.Context(), &rpc.MCPConfigRemoveRequest{Name: serverName}) - }) - - if _, err := client.RPC.MCP.Config().Add(t.Context(), &rpc.MCPConfigAddRequest{ - Name: serverName, - Config: baseConfig, - }); err != nil { - t.Fatalf("MCP.Config.Add failed: %v", err) - } - - afterAdd, err := client.RPC.MCP.Config().List(t.Context()) - if err != nil { - t.Fatalf("MCP.Config.List (after add) failed: %v", err) - } - if _, present := afterAdd.Servers[serverName]; !present { - t.Fatalf("Expected %q to be present after Add", serverName) - } - - if _, err := client.RPC.MCP.Config().Update(t.Context(), &rpc.MCPConfigUpdateRequest{ - Name: serverName, - Config: updatedConfig, - }); err != nil { - t.Fatalf("MCP.Config.Update failed: %v", err) - } - - afterUpdate, err := client.RPC.MCP.Config().List(t.Context()) - if err != nil { - t.Fatalf("MCP.Config.List (after update) failed: %v", err) - } - updated, present := afterUpdate.Servers[serverName] - if !present { - t.Fatalf("Expected %q to still be present after Update", serverName) - } - updatedLocal, ok := updated.(*rpc.MCPServerConfigStdio) - if !ok { - t.Fatalf("Expected local MCP config, got %T", updated) - } - if updatedLocal.Command != "node" { - t.Errorf("Expected command='node', got %q", updatedLocal.Command) - } - if len(updatedLocal.Args) == 0 || updatedLocal.Args[0] != "--version" { - t.Errorf("Expected args[0]='--version', got %v", updatedLocal.Args) - } - - if _, err := client.RPC.MCP.Config().Disable(t.Context(), &rpc.MCPConfigDisableRequest{Names: []string{serverName}}); err != nil { - t.Fatalf("MCP.Config.Disable failed: %v", err) - } - if _, err := client.RPC.MCP.Config().Enable(t.Context(), &rpc.MCPConfigEnableRequest{Names: []string{serverName}}); err != nil { - t.Fatalf("MCP.Config.Enable failed: %v", err) - } - - if _, err := client.RPC.MCP.Config().Remove(t.Context(), &rpc.MCPConfigRemoveRequest{Name: serverName}); err != nil { - t.Fatalf("MCP.Config.Remove failed: %v", err) - } - - afterRemove, err := client.RPC.MCP.Config().List(t.Context()) - if err != nil { - t.Fatalf("MCP.Config.List (after remove) failed: %v", err) - } - if _, present := afterRemove.Servers[serverName]; present { - t.Errorf("Expected %q to be removed", serverName) - } - }) - - t.Run("should round trip http MCP oauth config rpc", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - serverName := fmt.Sprintf("sdk-http-oauth-%s", randomHex(t)) - - httpType := rpc.MCPServerConfigHTTPTypeHTTP - urlBase := "https://example.com/mcp" - urlUpdated := "https://example.com/updated-mcp" - clientID := "client-id" - clientIDUpdated := "updated-client-id" - grantClientCreds := rpc.MCPServerConfigHTTPOauthGrantTypeClientCredentials - grantAuthCode := rpc.MCPServerConfigHTTPOauthGrantTypeAuthorizationCode - var publicFalse = false - var publicTrue = true - var timeoutBase int64 = 3000 - var timeoutUpdated int64 = 4000 - - baseConfig := &rpc.MCPServerConfigHTTP{ - Type: &httpType, - URL: urlBase, - Headers: map[string]string{"Authorization": "Bearer token"}, - OauthClientID: &clientID, - OauthPublicClient: &publicFalse, - OauthGrantType: &grantClientCreds, - Tools: []string{"*"}, - Timeout: &timeoutBase, - } - updatedConfig := &rpc.MCPServerConfigHTTP{ - Type: &httpType, - URL: urlUpdated, - OauthClientID: &clientIDUpdated, - OauthPublicClient: &publicTrue, - OauthGrantType: &grantAuthCode, - Tools: []string{"updated-tool"}, - Timeout: &timeoutUpdated, - } - - t.Cleanup(func() { - _, _ = client.RPC.MCP.Config().Remove(t.Context(), &rpc.MCPConfigRemoveRequest{Name: serverName}) - }) - - if _, err := client.RPC.MCP.Config().Add(t.Context(), &rpc.MCPConfigAddRequest{ - Name: serverName, - Config: baseConfig, - }); err != nil { - t.Fatalf("MCP.Config.Add failed: %v", err) - } - - afterAdd, err := client.RPC.MCP.Config().List(t.Context()) - if err != nil { - t.Fatalf("MCP.Config.List (after add) failed: %v", err) - } - added, present := afterAdd.Servers[serverName] - if !present { - t.Fatalf("Expected %q to be present after Add", serverName) - } - addedHTTP, ok := added.(*rpc.MCPServerConfigHTTP) - if !ok { - t.Fatalf("Expected HTTP MCP config, got %T", added) - } - if addedHTTP.Type == nil || *addedHTTP.Type != "http" { - t.Errorf("Expected type='http', got %v", addedHTTP.Type) - } - if addedHTTP.URL != "https://example.com/mcp" { - t.Errorf("Expected url='https://example.com/mcp', got %q", addedHTTP.URL) - } - if got := addedHTTP.Headers["Authorization"]; got != "Bearer token" { - t.Errorf("Expected Authorization='Bearer token', got %q", got) - } - if addedHTTP.OauthClientID == nil || *addedHTTP.OauthClientID != "client-id" { - t.Errorf("Expected oauthClientId='client-id', got %v", addedHTTP.OauthClientID) - } - if addedHTTP.OauthPublicClient == nil || *addedHTTP.OauthPublicClient { - t.Errorf("Expected oauthPublicClient=false, got %v", addedHTTP.OauthPublicClient) - } - if addedHTTP.OauthGrantType == nil || *addedHTTP.OauthGrantType != "client_credentials" { - t.Errorf("Expected oauthGrantType='client_credentials', got %v", addedHTTP.OauthGrantType) - } - - if _, err := client.RPC.MCP.Config().Update(t.Context(), &rpc.MCPConfigUpdateRequest{ - Name: serverName, - Config: updatedConfig, - }); err != nil { - t.Fatalf("MCP.Config.Update failed: %v", err) - } - afterUpdate, err := client.RPC.MCP.Config().List(t.Context()) - if err != nil { - t.Fatalf("MCP.Config.List (after update) failed: %v", err) - } - updated, present := afterUpdate.Servers[serverName] - if !present { - t.Fatalf("Expected %q to still be present after Update", serverName) - } - updatedHTTP, ok := updated.(*rpc.MCPServerConfigHTTP) - if !ok { - t.Fatalf("Expected HTTP MCP config, got %T", updated) - } - if updatedHTTP.URL != "https://example.com/updated-mcp" { - t.Errorf("Expected url='https://example.com/updated-mcp', got %q", updatedHTTP.URL) - } - if updatedHTTP.OauthClientID == nil || *updatedHTTP.OauthClientID != "updated-client-id" { - t.Errorf("Expected oauthClientId='updated-client-id', got %v", updatedHTTP.OauthClientID) - } - if updatedHTTP.OauthPublicClient == nil || !*updatedHTTP.OauthPublicClient { - t.Errorf("Expected oauthPublicClient=true, got %v", updatedHTTP.OauthPublicClient) - } - if updatedHTTP.OauthGrantType == nil || *updatedHTTP.OauthGrantType != "authorization_code" { - t.Errorf("Expected oauthGrantType='authorization_code', got %v", updatedHTTP.OauthGrantType) - } - if len(updatedHTTP.Tools) == 0 || updatedHTTP.Tools[0] != "updated-tool" { - t.Errorf("Expected tools[0]='updated-tool', got %v", updatedHTTP.Tools) - } - if updatedHTTP.Timeout == nil || *updatedHTTP.Timeout != 4000 { - t.Errorf("Expected timeout=4000, got %v", updatedHTTP.Timeout) - } - - if _, err := client.RPC.MCP.Config().Remove(t.Context(), &rpc.MCPConfigRemoveRequest{Name: serverName}); err != nil { - t.Fatalf("MCP.Config.Remove failed: %v", err) - } - - afterRemove, err := client.RPC.MCP.Config().List(t.Context()) - if err != nil { - t.Fatalf("MCP.Config.List (after remove) failed: %v", err) - } - if _, present := afterRemove.Servers[serverName]; present { - t.Errorf("Expected %q to be removed", serverName) - } - }) -} diff --git a/go/internal/e2e/rpc_mcp_lifecycle_e2e_test.go b/go/internal/e2e/rpc_mcp_lifecycle_e2e_test.go deleted file mode 100644 index cfe1123fd1..0000000000 --- a/go/internal/e2e/rpc_mcp_lifecycle_e2e_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestRpcMcpLifecycle(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should_list_tools_and_report_running_status_for_connected_server", func(t *testing.T) { - ctx.ConfigureForTest(t) - const serverName = "rpc-lifecycle-list-server" - session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, serverName)}) - defer session.Disconnect() - waitForPortedMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - - tools, err := session.RPC.MCP.ListTools(t.Context(), &rpc.MCPListToolsRequest{ServerName: serverName}) - if err != nil { - t.Fatalf("MCP.ListTools failed: %v", err) - } - if len(tools.Tools) == 0 { - t.Fatal("Expected connected MCP server to expose at least one tool") - } - for _, tool := range tools.Tools { - if strings.TrimSpace(tool.Name) == "" { - t.Fatalf("Expected non-empty MCP tool name, got %+v", tool) - } - } - - running, err := session.RPC.MCP.IsServerRunning(t.Context(), &rpc.MCPIsServerRunningRequest{ServerName: serverName}) - if err != nil { - t.Fatalf("MCP.IsServerRunning(%s) failed: %v", serverName, err) - } - if !running.Running { - t.Fatalf("Expected %s to be running", serverName) - } - missing, err := session.RPC.MCP.IsServerRunning(t.Context(), &rpc.MCPIsServerRunningRequest{ServerName: "missing-" + randomHex(t)}) - if err != nil { - t.Fatalf("MCP.IsServerRunning(missing) failed: %v", err) - } - if missing.Running { - t.Fatal("Expected missing MCP server not to be running") - } - }) - - t.Run("should_throw_when_listing_tools_for_unconnected_server", func(t *testing.T) { - ctx.ConfigureForTest(t) - const serverName = "rpc-lifecycle-unconnected-host" - session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, serverName)}) - defer session.Disconnect() - waitForPortedMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - - _, err := session.RPC.MCP.ListTools(t.Context(), &rpc.MCPListToolsRequest{ServerName: "missing-" + randomHex(t)}) - if err == nil { - t.Fatal("Expected MCP.ListTools for an unconnected server to fail") - } - message := err.Error() - assertPortedNoUnhandledMethod(t, message) - assertPortedContainsFold(t, message, "not connected") - }) - - t.Run("should_stop_running_mcp_server", func(t *testing.T) { - ctx.ConfigureForTest(t) - const serverName = "rpc-lifecycle-stop-server" - session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, serverName)}) - defer session.Disconnect() - waitForPortedMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) - waitForPortedMCPRunning(t, session, serverName, true) - - if _, err := session.RPC.MCP.StopServer(t.Context(), &rpc.MCPStopServerRequest{ServerName: serverName}); err != nil { - t.Fatalf("MCP.StopServer failed: %v", err) - } - waitForPortedMCPRunning(t, session, serverName, false) - }) -} - -func waitForPortedMCPServerStatus(t *testing.T, session *copilot.Session, serverName string, expectedStatus rpc.MCPServerStatus) { - t.Helper() - waitForRPCCondition(t, 60*time.Second, serverName+" reaching "+string(expectedStatus), func() (bool, error) { - result, err := session.RPC.MCP.List(t.Context()) - if err != nil { - return false, err - } - for _, server := range result.Servers { - if server.Name == serverName { - return server.Status == expectedStatus, nil - } - } - return false, nil - }) -} - -func waitForPortedMCPRunning(t *testing.T, session *copilot.Session, serverName string, expectedRunning bool) { - t.Helper() - waitForRPCCondition(t, 60*time.Second, serverName+" running state", func() (bool, error) { - result, err := session.RPC.MCP.IsServerRunning(t.Context(), &rpc.MCPIsServerRunningRequest{ServerName: serverName}) - if err != nil { - return false, err - } - return result.Running == expectedRunning, nil - }) -} diff --git a/go/internal/e2e/rpc_queue_e2e_test.go b/go/internal/e2e/rpc_queue_e2e_test.go deleted file mode 100644 index 3b79398219..0000000000 --- a/go/internal/e2e/rpc_queue_e2e_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/E2E/RpcQueueE2ETests.cs (snapshot category "rpc_queue"). -func TestRPCQueueE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("fresh queue is empty and empty mutations are noops", func(t *testing.T) { - session := createQueueSession(t, client) - defer session.Disconnect() - - assertQueueEmpty(t, session) - - remove, err := session.RPC.Queue.RemoveMostRecent(t.Context()) - if err != nil { - t.Fatalf("Queue.RemoveMostRecent failed: %v", err) - } - if remove.Removed { - t.Fatal("Expected RemoveMostRecent Removed=false on empty queue") - } - assertQueueEmpty(t, session) - - if _, err := session.RPC.Queue.Clear(t.Context()); err != nil { - t.Fatalf("Queue.Clear failed: %v", err) - } - assertQueueEmpty(t, session) - }) - - t.Run("pending items reports queued command and remove and clear update queue", func(t *testing.T) { - session := createQueueSession(t, client) - defer session.Disconnect() - - interest, err := session.RPC.EventLog.RegisterInterest(t.Context(), &rpc.RegisterEventInterestParams{EventType: string(copilot.SessionEventTypeCommandQueued)}) - if err != nil { - t.Fatalf("EventLog.RegisterInterest failed: %v", err) - } - defer func() { - _, _ = session.RPC.EventLog.ReleaseInterest(t.Context(), &rpc.ReleaseEventInterestParams{Handle: interest.Handle}) - _, _ = session.RPC.Queue.Clear(t.Context()) - }() - - firstCommand := "/sdk-queue-first-" + randomHex(t) - secondCommand := "/sdk-queue-second-" + randomHex(t) - thirdCommand := "/sdk-queue-third-" + randomHex(t) - firstQueued := make(chan *copilot.CommandQueuedData, 1) - unsubscribe := session.On(func(event copilot.SessionEvent) { - data, ok := event.Data.(*copilot.CommandQueuedData) - if ok && data.Command == firstCommand { - select { - case firstQueued <- data: - default: - } - } - }) - defer unsubscribe() - - first, err := session.RPC.Commands.Enqueue(t.Context(), &rpc.EnqueueCommandParams{Command: firstCommand}) - if err != nil { - t.Fatalf("Commands.Enqueue(first) failed: %v", err) - } - if !first.Queued() { - t.Fatal("Expected first command to be queued") - } - - var firstEvent *copilot.CommandQueuedData - select { - case firstEvent = <-firstQueued: - case <-time.After(30 * time.Second): - t.Fatalf("Timed out waiting for first command.queued event") - } - - second, err := session.RPC.Commands.Enqueue(t.Context(), &rpc.EnqueueCommandParams{Command: secondCommand}) - if err != nil { - t.Fatalf("Commands.Enqueue(second) failed: %v", err) - } - if !second.Queued() { - t.Fatal("Expected second command to be queued") - } - waitForCommandInPendingItems(t, session, secondCommand) - - remove, err := session.RPC.Queue.RemoveMostRecent(t.Context()) - if err != nil { - t.Fatalf("Queue.RemoveMostRecent failed: %v", err) - } - if !remove.Removed { - t.Fatal("Expected RemoveMostRecent to remove second queued command") - } - waitForCommandNotInPendingItems(t, session, secondCommand) - - third, err := session.RPC.Commands.Enqueue(t.Context(), &rpc.EnqueueCommandParams{Command: thirdCommand}) - if err != nil { - t.Fatalf("Commands.Enqueue(third) failed: %v", err) - } - if !third.Queued() { - t.Fatal("Expected third command to be queued") - } - waitForCommandInPendingItems(t, session, thirdCommand) - - if _, err := session.RPC.Queue.Clear(t.Context()); err != nil { - t.Fatalf("Queue.Clear failed: %v", err) - } - waitForCommandNotInPendingItems(t, session, thirdCommand) - - stop := true - completed, err := session.RPC.Commands.RespondToQueuedCommand(t.Context(), &rpc.CommandsRespondToQueuedCommandRequest{ - RequestID: firstEvent.RequestID, - Result: rpc.QueuedCommandHandled{StopProcessingQueue: &stop}, - }) - if err != nil { - t.Fatalf("Commands.RespondToQueuedCommand failed: %v", err) - } - if !completed.Success { - t.Fatal("Expected response to first queued command to succeed") - } - waitForQueueEmpty(t, session) - }) -} - -func createQueueSession(t *testing.T, client *copilot.Client) *copilot.Session { - t.Helper() - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - return session -} - -func assertQueueEmpty(t *testing.T, session *copilot.Session) { - t.Helper() - pending, err := session.RPC.Queue.PendingItems(t.Context()) - if err != nil { - t.Fatalf("Queue.PendingItems failed: %v", err) - } - if len(pending.Items) != 0 || len(pending.SteeringMessages) != 0 { - t.Fatalf("Expected empty queue, got %+v", pending) - } -} - -func waitForCommandInPendingItems(t *testing.T, session *copilot.Session, command string) { - t.Helper() - var matched *rpc.QueuePendingItems - waitForRPCCondition(t, 30*time.Second, "queued command "+command+" to appear", func() (bool, error) { - pending, err := session.RPC.Queue.PendingItems(t.Context()) - if err != nil { - return false, err - } - for i := range pending.Items { - if isPendingCommand(pending.Items[i], command) { - matched = &pending.Items[i] - return true, nil - } - } - return false, nil - }) - if matched.Kind != rpc.QueuePendingItemsKindCommand { - t.Fatalf("Expected command pending item, got %+v", matched) - } - if !strings.Contains(matched.DisplayText, strings.TrimPrefix(command, "/")) && matched.DisplayText != command { - t.Fatalf("Expected pending item display text to include %q, got %q", command, matched.DisplayText) - } -} - -func waitForCommandNotInPendingItems(t *testing.T, session *copilot.Session, command string) { - t.Helper() - waitForRPCCondition(t, 30*time.Second, "queued command "+command+" to leave queue", func() (bool, error) { - pending, err := session.RPC.Queue.PendingItems(t.Context()) - if err != nil { - return false, err - } - for _, item := range pending.Items { - if isPendingCommand(item, command) { - return false, nil - } - } - return true, nil - }) -} - -func waitForQueueEmpty(t *testing.T, session *copilot.Session) { - t.Helper() - waitForRPCCondition(t, 30*time.Second, "queue to empty", func() (bool, error) { - pending, err := session.RPC.Queue.PendingItems(t.Context()) - return err == nil && len(pending.Items) == 0 && len(pending.SteeringMessages) == 0, err - }) - assertQueueEmpty(t, session) -} - -func isPendingCommand(item rpc.QueuePendingItems, command string) bool { - return item.Kind == rpc.QueuePendingItemsKindCommand && - (item.DisplayText == command || strings.Contains(item.DisplayText, strings.TrimPrefix(command, "/"))) -} diff --git a/go/internal/e2e/rpc_remote_e2e_test.go b/go/internal/e2e/rpc_remote_e2e_test.go deleted file mode 100644 index fa4392b030..0000000000 --- a/go/internal/e2e/rpc_remote_e2e_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/E2E/RpcRemoteE2ETests.cs (snapshot category "rpc_remote"). -func TestRPCRemoteE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should treat remote off as no op or implemented error", func(t *testing.T) { - session := createRemoteSession(t, client) - defer session.Disconnect() - - mode := rpc.RemoteSessionModeOff - result, err := session.RPC.Remote.Enable(t.Context(), &rpc.RemoteEnableRequest{Mode: &mode}) - if err != nil { - assertImplementedRPCError(t, err, "session.remote.enable") - return - } - if result.RemoteSteerable { - t.Fatalf("Expected remote off to report RemoteSteerable=false, got %+v", result) - } - if result.URL != nil && *result.URL != "" { - t.Fatalf("Expected remote off to return empty URL, got %q", *result.URL) - } - }) - - t.Run("should treat remote disable as no op or implemented error", func(t *testing.T) { - session := createRemoteSession(t, client) - defer session.Disconnect() - - if _, err := session.RPC.Remote.Disable(t.Context()); err != nil { - assertImplementedRPCError(t, err, "session.remote.disable") - } - }) - - t.Run("should notify steerable changed event and persist flag", func(t *testing.T) { - session := createRemoteSession(t, client) - defer session.Disconnect() - - if _, err := session.RPC.Remote.NotifySteerableChanged(t.Context(), &rpc.RemoteNotifySteerableChangedRequest{RemoteSteerable: true}); err != nil { - t.Fatalf("Remote.NotifySteerableChanged(true) failed: %v", err) - } - waitForRemoteSteerableEvent(t, session, true) - - if _, err := session.RPC.Remote.NotifySteerableChanged(t.Context(), &rpc.RemoteNotifySteerableChangedRequest{RemoteSteerable: false}); err != nil { - t.Fatalf("Remote.NotifySteerableChanged(false) failed: %v", err) - } - waitForRemoteSteerableEvent(t, session, false) - }) -} - -func createRemoteSession(t *testing.T, client *copilot.Client) *copilot.Session { - t.Helper() - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - return session -} - -func waitForRemoteSteerableEvent(t *testing.T, session *copilot.Session, expected bool) { - t.Helper() - waitForRPCCondition(t, 30*time.Second, "session.remote_steerable_changed event", func() (bool, error) { - events, err := session.GetEvents(t.Context()) - if err != nil { - return false, err - } - for _, event := range events { - if data, ok := event.Data.(*copilot.SessionRemoteSteerableChangedData); ok && data.RemoteSteerable == expected { - return true, nil - } - } - return false, nil - }) -} - -func assertImplementedRPCError(t *testing.T, err error, method string) { - t.Helper() - if strings.Contains(strings.ToLower(err.Error()), "unhandled method "+strings.ToLower(method)) { - t.Fatalf("Expected implemented error for %s, got %v", method, err) - } -} diff --git a/go/internal/e2e/rpc_schedule_e2e_test.go b/go/internal/e2e/rpc_schedule_e2e_test.go deleted file mode 100644 index a20d481742..0000000000 --- a/go/internal/e2e/rpc_schedule_e2e_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package e2e - -import ( - "math" - "testing" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/E2E/RpcScheduleE2ETests.cs (snapshot category "rpc_schedule"). -func TestRPCScheduleE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should list no schedules for fresh session", func(t *testing.T) { - session := createScheduleSession(t, client) - defer session.Disconnect() - - result, err := session.RPC.Schedule.List(t.Context()) - if err != nil { - t.Fatalf("Schedule.List failed: %v", err) - } - if result.Entries == nil { - t.Fatal("Expected non-nil schedule Entries") - } - if len(result.Entries) != 0 { - t.Fatalf("Expected no schedules for a fresh session, got %+v", result.Entries) - } - }) - - t.Run("should return nil entry when stopping unknown schedule", func(t *testing.T) { - session := createScheduleSession(t, client) - defer session.Disconnect() - - result, err := session.RPC.Schedule.Stop(t.Context(), &rpc.ScheduleStopRequest{ID: math.MaxInt64}) - if err != nil { - t.Fatalf("Schedule.Stop failed: %v", err) - } - if result.Entry != nil { - t.Fatalf("Expected nil entry for unknown schedule, got %+v", result.Entry) - } - list, err := session.RPC.Schedule.List(t.Context()) - if err != nil { - t.Fatalf("Schedule.List after Stop failed: %v", err) - } - if len(list.Entries) != 0 { - t.Fatalf("Expected no schedules after stopping unknown schedule, got %+v", list.Entries) - } - }) -} - -func createScheduleSession(t *testing.T, client *copilot.Client) *copilot.Session { - t.Helper() - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - return session -} diff --git a/go/internal/e2e/rpc_server_e2e_test.go b/go/internal/e2e/rpc_server_e2e_test.go deleted file mode 100644 index 7cf7f4b74b..0000000000 --- a/go/internal/e2e/rpc_server_e2e_test.go +++ /dev/null @@ -1,837 +0,0 @@ -package e2e - -import ( - "fmt" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" - "github.com/google/uuid" -) - -// Mirrors dotnet/test/RpcServerTests.cs (snapshot category "rpc_server"). -// Tests server-scoped (non-session) RPCs. -func TestRPCServerE2E(t *testing.T) { - t.Run("should clear the managed settings cache", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - ctx.ConfigureForTest(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - if _, err := client.RPC.ManagedSettings.ClearCache(t.Context()); err != nil { - t.Fatalf("ManagedSettings.ClearCache failed: %v", err) - } - }) - - t.Run("should call rpc ping with typed params and result", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - ctx.ConfigureForTest(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - message := "typed rpc test" - result, err := client.RPC.Ping(t.Context(), &rpc.PingRequest{Message: &message}) - if err != nil { - t.Fatalf("RPC.Ping failed: %v", err) - } - if !strings.Contains(result.Message, "typed rpc test") { - t.Errorf("Expected ping response to contain 'typed rpc test', got %q", result.Message) - } - if result.Timestamp.IsZero() { - t.Errorf("Expected non-zero Timestamp, got %s", result.Timestamp) - } - }) - - t.Run("should call rpc models list with typed result", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - ctx.ConfigureForTest(t) - const token = "rpc-models-token" - registerProxyUser(t, ctx, token, "rpc-user", nil) - client := newAuthenticatedClient(ctx, token) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - result, err := client.RPC.Models.List(t.Context(), &rpc.ModelsListRequest{}) - if err != nil { - t.Fatalf("Models.List failed: %v", err) - } - if result.Models == nil { - t.Fatal("Expected non-nil Models list") - } - var hasClaude bool - for _, model := range result.Models { - if strings.TrimSpace(model.Name) == "" { - t.Errorf("Model %q has empty Name", model.ID) - } - if model.ID == "claude-sonnet-5" { - hasClaude = true - } - } - if !hasClaude { - t.Errorf("Expected models list to contain 'claude-sonnet-5'") - } - }) - - t.Run("should call rpc account get quota when authenticated", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - ctx.ConfigureForTest(t) - const token = "rpc-quota-token" - registerProxyUser(t, ctx, token, "rpc-user", map[string]any{ - "chat": map[string]any{ - "entitlement": 100, - "overage_count": 2, - "overage_permitted": true, - "percent_remaining": 75, - "timestamp_utc": "2026-04-30T00:00:00Z", - }, - }) - client := newAuthenticatedClient(ctx, token) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - tokenCopy := token - result, err := client.RPC.Account.GetQuota(t.Context(), &rpc.AccountGetQuotaRequest{GitHubToken: &tokenCopy}) - if err != nil { - t.Fatalf("Account.GetQuota failed: %v", err) - } - chat, present := result.QuotaSnapshots["chat"] - if !present { - t.Fatalf("Expected 'chat' quota in snapshots, got %+v", result.QuotaSnapshots) - } - if chat.EntitlementRequests != 100 { - t.Errorf("Expected EntitlementRequests=100, got %d", chat.EntitlementRequests) - } - if chat.UsedRequests != 25 { - t.Errorf("Expected UsedRequests=25, got %d", chat.UsedRequests) - } - if chat.RemainingPercentage != 75 { - t.Errorf("Expected RemainingPercentage=75, got %v", chat.RemainingPercentage) - } - if chat.Overage != 2 { - t.Errorf("Expected Overage=2, got %v", chat.Overage) - } - if !chat.UsageAllowedWithExhaustedQuota { - t.Errorf("Expected UsageAllowedWithExhaustedQuota=true") - } - if !chat.OverageAllowedWithExhaustedQuota { - t.Errorf("Expected OverageAllowedWithExhaustedQuota=true") - } - expectedResetDate, err := time.Parse(time.RFC3339, "2026-04-30T00:00:00Z") - if err != nil { - t.Fatalf("Parse expected reset date: %v", err) - } - if chat.ResetDate == nil || !chat.ResetDate.Equal(expectedResetDate) { - t.Errorf("Expected ResetDate='2026-04-30T00:00:00Z', got %v", chat.ResetDate) - } - }) - - t.Run("should call rpc tools list with typed result", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - ctx.ConfigureForTest(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - result, err := client.RPC.Tools.List(t.Context(), &rpc.ToolsListRequest{}) - if err != nil { - t.Fatalf("Tools.List failed: %v", err) - } - if len(result.Tools) == 0 { - t.Fatal("Expected non-empty Tools list") - } - for i, tool := range result.Tools { - if strings.TrimSpace(tool.Name) == "" { - t.Errorf("Tool[%d] has empty Name", i) - } - } - }) - - t.Run("should call rpc session fs set provider with typed result", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - result, err := client.RPC.SessionFS.SetProvider(t.Context(), &rpc.SessionFSSetProviderRequest{ - InitialCwd: "/", - SessionStatePath: "/session-state", - Conventions: rpc.SessionFSSetProviderConventionsPosix, - Capabilities: &rpc.SessionFSSetProviderCapabilities{Sqlite: rpcPtr(true)}, - }) - if err != nil { - t.Fatalf("SessionFS.SetProvider failed: %v", err) - } - if !result.Success { - t.Fatalf("Expected SessionFS.SetProvider Success=true, got %+v", result) - } - }) - - t.Run("should add secret filter values", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - client := ctx.NewClient(func(opts *copilot.ClientOptions) { - opts.Env = append(opts.Env, "COPILOT_ENABLE_SECRET_FILTERING=true") - }) - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - secret := "rpc-secret-" + randomHex(t) - result, err := client.RPC.Secrets.AddFilterValues(t.Context(), &rpc.SecretsAddFilterValuesRequest{Values: []string{secret}}) - if err != nil { - t.Fatalf("Secrets.AddFilterValues failed: %v", err) - } - if !result.Ok { - t.Fatalf("Expected AddFilterValues Ok=true, got %+v", result) - } - }) - - t.Run("should return false for missing LLM response frames", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - start, err := client.RPC.LlmInference.HttpResponseStart(t.Context(), &rpc.LlmInferenceHTTPResponseStartRequest{ - RequestID: "missing-response-start-request", - Status: 200, - StatusText: rpcPtr("OK"), - Headers: map[string][]string{ - "content-type": {"application/json"}, - }, - }) - if err != nil { - t.Fatalf("LlmInference.HttpResponseStart failed: %v", err) - } - if start.Accepted { - t.Fatal("Expected Accepted=false for missing LLM response start request id") - } - - end := true - chunk, err := client.RPC.LlmInference.HttpResponseChunk(t.Context(), &rpc.LlmInferenceHTTPResponseChunkRequest{ - RequestID: "missing-response-chunk-request", - Data: "{}", - End: &end, - }) - if err != nil { - t.Fatalf("LlmInference.HttpResponseChunk failed: %v", err) - } - if chunk.Accepted { - t.Fatal("Expected Accepted=false for missing LLM response chunk request id") - } - }) - - t.Run("should list find and inspect persisted session state", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - token := "rpc-server-list-token-" + randomHex(t) - registerProxyUser(t, ctx, token, "rpc-user", nil) - client := newAuthenticatedClient(ctx, token) - t.Cleanup(func() { client.ForceStop() }) - - sessionID := uuid.NewString() - workingDirectory := createUniqueRPCWorkDirectory(t, ctx, "server-rpc-list") - missingSessionID := uuid.NewString() - missingTaskID := "missing-task-" + randomHex(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - SessionID: sessionID, - WorkingDirectory: workingDirectory, - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - if err := session.Log(t.Context(), "SERVER_RPC_LIST_READY", nil); err != nil { - t.Fatalf("Log failed: %v", err) - } - - saveSession(t, client, sessionID) - - metadataLimit := int64(0) - filter := &rpc.SessionListFilter{Cwd: &workingDirectory} - listed, err := client.RPC.Sessions.List(t.Context(), &rpc.SessionsListRequest{ - MetadataLimit: &metadataLimit, - Filter: filter, - }) - if err != nil { - t.Fatalf("Sessions.List failed: %v", err) - } - if listed.Sessions == nil { - t.Fatal("Expected non-nil sessions list") - } - for _, metadata := range listed.Sessions { - local, ok := metadata.(*rpc.LocalSessionMetadataValue) - if ok && local.Context != nil { - assertRPCPathEqual(t, workingDirectory, local.Context.Cwd) - } - } - - byPrefix, err := client.RPC.Sessions.FindByPrefix(t.Context(), &rpc.SessionsFindByPrefixRequest{Prefix: sessionID[:8]}) - if err != nil { - t.Fatalf("Sessions.FindByPrefix failed: %v", err) - } - if byPrefix.SessionID != nil && *byPrefix.SessionID != sessionID { - t.Fatalf("Expected prefix lookup to return %q or nil, got %q", sessionID, *byPrefix.SessionID) - } - - byTask, err := client.RPC.Sessions.FindByTaskId(t.Context(), &rpc.SessionsFindByTaskIDRequest{TaskID: missingTaskID}) - if err != nil { - t.Fatalf("Sessions.FindByTaskId failed: %v", err) - } - if byTask.SessionID != nil { - t.Fatalf("Expected missing task ID lookup to return nil, got %q", *byTask.SessionID) - } - - lastForContext, err := client.RPC.Sessions.GetLastForContext(t.Context(), &rpc.SessionsGetLastForContextRequest{ - Context: &rpc.SessionContext{Cwd: workingDirectory}, - }) - if err != nil { - t.Fatalf("Sessions.GetLastForContext failed: %v", err) - } - if lastForContext.SessionID != nil && *lastForContext.SessionID != sessionID { - t.Fatalf("Expected last session for context to be %q or nil, got %q", sessionID, *lastForContext.SessionID) - } - - sizes, err := client.RPC.Sessions.GetSizes(t.Context()) - if err != nil { - t.Fatalf("Sessions.GetSizes failed: %v", err) - } - if sizes.Sizes == nil { - t.Fatal("Expected non-nil session sizes map") - } - if size, present := sizes.Sizes[sessionID]; present && size < 0 { - t.Fatalf("Expected non-negative size for %q, got %d", sessionID, size) - } - - inUse, err := client.RPC.Sessions.CheckInUse(t.Context(), &rpc.SessionsCheckInUseRequest{SessionIDs: []string{sessionID, missingSessionID}}) - if err != nil { - t.Fatalf("Sessions.CheckInUse failed: %v", err) - } - if containsString(inUse.InUse, missingSessionID) { - t.Fatalf("Did not expect missing session %q to be in use: %+v", missingSessionID, inUse.InUse) - } - - }) - - t.Run("should enrich basic session metadata", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - token := "rpc-server-enrich-token-" + randomHex(t) - registerProxyUser(t, ctx, token, "rpc-user", nil) - client := newAuthenticatedClient(ctx, token) - t.Cleanup(func() { client.ForceStop() }) - - sessionID := uuid.NewString() - workingDirectory := createUniqueRPCWorkDirectory(t, ctx, "server-rpc-enrich") - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - SessionID: sessionID, - WorkingDirectory: workingDirectory, - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - if err := session.Log(t.Context(), "SERVER_RPC_ENRICH_READY", nil); err != nil { - t.Fatalf("Log failed: %v", err) - } - saveSession(t, client, sessionID) - - now := time.Now().UTC().Format(time.RFC3339Nano) - result, err := client.RPC.Sessions.EnrichMetadata(t.Context(), &rpc.SessionsEnrichMetadataRequest{ - Sessions: []rpc.LocalSessionMetadataValue{{ - SessionID: sessionID, - StartTime: now, - ModifiedTime: now, - IsRemote: false, - Name: rpcPtr("Basic metadata"), - Context: &rpc.SessionContext{Cwd: workingDirectory}, - }}, - }) - if err != nil { - t.Fatalf("Sessions.EnrichMetadata failed: %v", err) - } - if len(result.Sessions) != 1 { - t.Fatalf("Expected one enriched session, got %+v", result.Sessions) - } - enriched := result.Sessions[0] - if enriched.SessionID != sessionID { - t.Fatalf("Expected enriched session ID %q, got %q", sessionID, enriched.SessionID) - } - if enriched.Context == nil { - t.Fatal("Expected enriched context") - } - assertRPCPathEqual(t, workingDirectory, enriched.Context.Cwd) - if enriched.IsRemote { - t.Fatal("Expected local enriched session") - } - }) - - t.Run("should close active session and release lock", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - token := "rpc-server-close-token-" + randomHex(t) - registerProxyUser(t, ctx, token, "rpc-user", nil) - client := newAuthenticatedClient(ctx, token) - t.Cleanup(func() { client.ForceStop() }) - - sessionID := uuid.NewString() - workingDirectory := createUniqueRPCWorkDirectory(t, ctx, "server-rpc-close") - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - SessionID: sessionID, - WorkingDirectory: workingDirectory, - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - if err := session.Log(t.Context(), "SERVER_RPC_CLOSE_READY", nil); err != nil { - t.Fatalf("Log failed: %v", err) - } - saveSession(t, client, sessionID) - - if _, err := client.RPC.Sessions.Close(t.Context(), &rpc.SessionsCloseRequest{SessionID: sessionID}); err != nil { - t.Fatalf("Sessions.Close failed: %v", err) - } - if _, err := client.RPC.Sessions.ReleaseLock(t.Context(), &rpc.SessionsReleaseLockRequest{SessionID: sessionID}); err != nil { - t.Fatalf("Sessions.ReleaseLock failed: %v", err) - } - inUse, err := client.RPC.Sessions.CheckInUse(t.Context(), &rpc.SessionsCheckInUseRequest{SessionIDs: []string{sessionID}}) - if err != nil { - t.Fatalf("Sessions.CheckInUse failed: %v", err) - } - if containsString(inUse.InUse, sessionID) { - t.Fatalf("Expected %q not to be in use after close/release", sessionID) - } - }) - - t.Run("should prune dry run and bulk delete persisted session", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - token := "rpc-server-delete-token-" + randomHex(t) - registerProxyUser(t, ctx, token, "rpc-user", nil) - client := newAuthenticatedClient(ctx, token) - t.Cleanup(func() { client.ForceStop() }) - - sessionID := uuid.NewString() - missingSessionID := uuid.NewString() - workingDirectory := createUniqueRPCWorkDirectory(t, ctx, "server-rpc-delete") - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - SessionID: sessionID, - WorkingDirectory: workingDirectory, - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - if err := session.Log(t.Context(), "SERVER_RPC_DELETE_READY", nil); err != nil { - t.Fatalf("Log failed: %v", err) - } - - saveSession(t, client, sessionID) - if _, err := client.RPC.Sessions.Close(t.Context(), &rpc.SessionsCloseRequest{SessionID: sessionID}); err != nil { - t.Fatalf("Sessions.Close failed: %v", err) - } - - prune, err := client.RPC.Sessions.PruneOld(t.Context(), &rpc.SessionsPruneOldRequest{ - OlderThanDays: 0, - DryRun: rpcPtr(true), - IncludeNamed: rpcPtr(true), - ExcludeSessionIDs: []string{}, - }) - if err != nil { - t.Fatalf("Sessions.PruneOld failed: %v", err) - } - if !prune.DryRun { - t.Fatalf("Expected prune DryRun=true, got %+v", prune) - } - if containsString(prune.Deleted, sessionID) { - t.Fatalf("Dry run should not delete %q", sessionID) - } - if prune.FreedBytes < 0 { - t.Fatalf("Expected non-negative freed bytes, got %d", prune.FreedBytes) - } - - deleted, err := client.RPC.Sessions.BulkDelete(t.Context(), &rpc.SessionsBulkDeleteRequest{ - SessionIDs: []string{sessionID, missingSessionID}, - }) - if err != nil { - t.Fatalf("Sessions.BulkDelete failed: %v", err) - } - freed, present := deleted.FreedBytes[sessionID] - if !present { - t.Fatalf("Expected BulkDelete to include %q in freedBytes, got %+v", sessionID, deleted.FreedBytes) - } - if freed < 0 { - t.Fatalf("Expected non-negative freed bytes for %q, got %d", sessionID, freed) - } - if missingFreed, present := deleted.FreedBytes[missingSessionID]; present && missingFreed != 0 { - t.Fatalf("Expected missing session freed bytes to be 0 when present, got %d", missingFreed) - } - - _ = session - }) - - t.Run("should set additional plugins and reload deferred hooks", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - if _, err := client.RPC.Sessions.SetAdditionalPlugins(t.Context(), &rpc.SessionsSetAdditionalPluginsRequest{Plugins: []rpc.InstalledPlugin{}}); err != nil { - t.Fatalf("Sessions.SetAdditionalPlugins(clear) failed: %v", err) - } - t.Cleanup(func() { - _, _ = client.RPC.Sessions.SetAdditionalPlugins(t.Context(), &rpc.SessionsSetAdditionalPluginsRequest{Plugins: []rpc.InstalledPlugin{}}) - }) - - sessionID := uuid.NewString() - workingDirectory := createUniqueRPCWorkDirectory(t, ctx, "server-rpc-hooks") - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - SessionID: sessionID, - WorkingDirectory: workingDirectory, - EnableConfigDiscovery: copilot.Bool(false), - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - if _, err := client.RPC.Sessions.ReloadPluginHooks(t.Context(), &rpc.SessionsReloadPluginHooksRequest{ - SessionID: sessionID, - DeferRepoHooks: rpcPtr(true), - }); err != nil { - t.Fatalf("Sessions.ReloadPluginHooks failed: %v", err) - } - loaded, err := client.RPC.Sessions.LoadDeferredRepoHooks(t.Context(), &rpc.SessionsLoadDeferredRepoHooksRequest{SessionID: sessionID}) - if err != nil { - t.Fatalf("Sessions.LoadDeferredRepoHooks failed: %v", err) - } - if loaded.StartupPrompts == nil { - t.Fatal("Expected non-nil StartupPrompts") - } - if loaded.HookCount != 0 || len(loaded.StartupPrompts) != 0 { - t.Fatalf("Expected no deferred hooks for isolated directory, got %+v", loaded) - } - }) - - t.Run("should report implemented error when connecting unknown remote session", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - ctx.ConfigureWithoutSnapshot(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - _, err := client.RPC.Sessions.Connect(t.Context(), &rpc.ConnectRemoteSessionParams{SessionID: "remote-" + randomHex(t)}) - if err == nil { - t.Fatal("Expected Sessions.Connect to fail for an unknown remote session") - } - text := strings.ToLower(err.Error()) - if strings.Contains(text, "unhandled method sessions.connect") { - t.Fatalf("Expected implemented error for sessions.connect, got %v", err) - } - if !strings.Contains(text, "session") { - t.Fatalf("Expected remote connect error to mention session, got %v", err) - } - }) - - t.Run("should discover server mcp and skills", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx := testharness.NewTestContext(t) - ctx.ConfigureForTest(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - skillName := fmt.Sprintf("server-rpc-skill-%s", randomHex(t)) - skillsDir := createMCPSkillsRPCDirectory(t, ctx.WorkDir, "server-rpc-skills", skillName, "Skill discovered by server-scoped RPC tests.") - - workingDir := ctx.WorkDir - mcp, err := client.RPC.MCP.Discover(t.Context(), &rpc.MCPDiscoverRequest{WorkingDirectory: &workingDir}) - if err != nil { - t.Fatalf("MCP.Discover failed: %v", err) - } - if mcp.Servers == nil { - t.Errorf("Expected non-nil Servers") - } - - skills, err := client.RPC.Skills.Discover(t.Context(), &rpc.SkillsDiscoverRequest{SkillDirectories: []string{skillsDir}}) - if err != nil { - t.Fatalf("Skills.Discover failed: %v", err) - } - discovered := findServerSkill(skills.Skills, skillName) - if discovered == nil { - t.Fatalf("Expected to discover skill %q", skillName) - return - } - if discovered.Description != "Skill discovered by server-scoped RPC tests." { - t.Errorf("Expected description to match, got %q", discovered.Description) - } - if !discovered.Enabled { - t.Errorf("Expected discovered skill to be Enabled") - } - expectedSuffix := filepath.Join(skillName, "SKILL.md") - if discovered.Path == nil || !strings.HasSuffix(filepath.ToSlash(*discovered.Path), filepath.ToSlash(expectedSuffix)) { - t.Errorf("Expected skill path to end with %q, got %v", expectedSuffix, discovered.Path) - } - - excludeHost := true - skillPaths, err := client.RPC.Skills.GetDiscoveryPaths(t.Context(), &rpc.SkillsGetDiscoveryPathsRequest{ - ProjectPaths: []string{ctx.WorkDir}, - ExcludeHostSkills: &excludeHost, - }) - if err != nil { - t.Fatalf("Skills.GetDiscoveryPaths failed: %v", err) - } - projectSkillPath := findSkillDiscoveryPath(skillPaths.Paths, ctx.WorkDir) - if projectSkillPath == nil { - t.Fatalf("Expected skill discovery paths to include %q", ctx.WorkDir) - return - } - if strings.TrimSpace(projectSkillPath.Path) == "" { - t.Fatal("Expected non-empty skill discovery path") - } - - agents, err := client.RPC.Agents.Discover(t.Context(), &rpc.AgentsDiscoverRequest{ - ProjectPaths: []string{ctx.WorkDir}, - ExcludeHostAgents: &excludeHost, - }) - if err != nil { - t.Fatalf("Agents.Discover failed: %v", err) - } - for _, agent := range agents.Agents { - if strings.TrimSpace(agent.Name) == "" { - t.Fatalf("Expected discovered agent to have a name: %+v", agent) - } - } - - agentPaths, err := client.RPC.Agents.GetDiscoveryPaths(t.Context(), &rpc.AgentsGetDiscoveryPathsRequest{ - ProjectPaths: []string{ctx.WorkDir}, - ExcludeHostAgents: &excludeHost, - }) - if err != nil { - t.Fatalf("Agents.GetDiscoveryPaths failed: %v", err) - } - projectAgentPath := findAgentDiscoveryPath(agentPaths.Paths, ctx.WorkDir) - if projectAgentPath == nil { - t.Fatalf("Expected agent discovery paths to include %q", ctx.WorkDir) - return - } - if strings.TrimSpace(projectAgentPath.Path) == "" { - t.Fatal("Expected non-empty agent discovery path") - } - - instructions, err := client.RPC.Instructions.Discover(t.Context(), &rpc.InstructionsDiscoverRequest{ - ProjectPaths: []string{ctx.WorkDir}, - ExcludeHostInstructions: &excludeHost, - }) - if err != nil { - t.Fatalf("Instructions.Discover failed: %v", err) - } - for _, source := range instructions.Sources { - if strings.TrimSpace(source.ID) == "" || strings.TrimSpace(source.Label) == "" || strings.TrimSpace(source.SourcePath) == "" { - t.Fatalf("Expected discovered instruction source fields to be populated: %+v", source) - } - } - - instructionPaths, err := client.RPC.Instructions.GetDiscoveryPaths(t.Context(), &rpc.InstructionsGetDiscoveryPathsRequest{ - ProjectPaths: []string{ctx.WorkDir}, - ExcludeHostInstructions: &excludeHost, - }) - if err != nil { - t.Fatalf("Instructions.GetDiscoveryPaths failed: %v", err) - } - if len(instructionPaths.Paths) == 0 { - t.Fatal("Expected instruction discovery paths") - } - if !hasInstructionDiscoveryPath(instructionPaths.Paths, ctx.WorkDir) { - t.Fatalf("Expected instruction discovery paths to include %q", ctx.WorkDir) - } - for _, path := range instructionPaths.Paths { - if strings.TrimSpace(path.Path) == "" { - t.Fatalf("Expected non-empty instruction discovery path: %+v", path) - } - } - - // Disable the skill globally and re-discover. - if _, err := client.RPC.Skills.Config().SetDisabledSkills(t.Context(), &rpc.SkillsConfigSetDisabledSkillsRequest{ - DisabledSkills: []string{skillName}, - }); err != nil { - t.Fatalf("Skills.Config.SetDisabledSkills failed: %v", err) - } - t.Cleanup(func() { - _, _ = client.RPC.Skills.Config().SetDisabledSkills(t.Context(), &rpc.SkillsConfigSetDisabledSkillsRequest{ - DisabledSkills: []string{}, - }) - }) - - disabled, err := client.RPC.Skills.Discover(t.Context(), &rpc.SkillsDiscoverRequest{SkillDirectories: []string{skillsDir}}) - if err != nil { - t.Fatalf("Skills.Discover (after disable) failed: %v", err) - } - disabledSkill := findServerSkill(disabled.Skills, skillName) - if disabledSkill == nil { - t.Fatalf("Expected to find skill %q after disable", skillName) - return - } - if disabledSkill.Enabled { - t.Errorf("Expected skill %q to be Enabled=false after global disable", skillName) - } - }) -} - -// newAuthenticatedClient builds a client that resolves auth through the test proxy. -func newAuthenticatedClient(ctx *testharness.TestContext, token string) *copilot.Client { - return ctx.NewClient(func(opts *copilot.ClientOptions) { - opts.Env = append(opts.Env, "COPILOT_DEBUG_GITHUB_API_URL="+ctx.ProxyURL) - opts.GitHubToken = token - }) -} - -// registerProxyUser configures the proxy with a fake CopilotUser response for the given token. -func registerProxyUser(t *testing.T, ctx *testharness.TestContext, token, login string, quotaSnapshots map[string]any) { - t.Helper() - user := map[string]any{ - "login": login, - "copilot_plan": "individual_pro", - "endpoints": map[string]any{"api": ctx.ProxyURL, "telemetry": "https://localhost:1/telemetry"}, - "analytics_tracking_id": login + "-tracking-id", - } - if quotaSnapshots != nil { - user["quota_snapshots"] = quotaSnapshots - } - if err := ctx.SetCopilotUserByToken(token, user); err != nil { - t.Fatalf("SetCopilotUserByToken failed: %v", err) - } -} - -func findServerSkill(skills []rpc.ServerSkill, name string) *rpc.ServerSkill { - for i, skill := range skills { - if skill.Name == name { - return &skills[i] - } - } - return nil -} - -func findSkillDiscoveryPath(paths []rpc.SkillDiscoveryPath, projectPath string) *rpc.SkillDiscoveryPath { - for i, path := range paths { - if path.ProjectPath != nil && path.PreferredForCreation && pathsEqual(*path.ProjectPath, projectPath) { - return &paths[i] - } - } - return nil -} - -func findAgentDiscoveryPath(paths []rpc.AgentDiscoveryPath, projectPath string) *rpc.AgentDiscoveryPath { - for i, path := range paths { - if path.ProjectPath != nil && path.PreferredForCreation && pathsEqual(*path.ProjectPath, projectPath) { - return &paths[i] - } - } - return nil -} - -func hasInstructionDiscoveryPath(paths []rpc.InstructionDiscoveryPath, projectPath string) bool { - for _, path := range paths { - if path.ProjectPath != nil && pathsEqual(*path.ProjectPath, projectPath) { - return true - } - } - return false -} - -func pathsEqual(left, right string) bool { - left = filepath.Clean(left) - right = filepath.Clean(right) - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func saveSession(t *testing.T, client *copilot.Client, sessionID string) { - t.Helper() - if _, err := client.RPC.Sessions.Save(t.Context(), &rpc.SessionsSaveRequest{SessionID: sessionID}); err != nil { - t.Fatalf("Sessions.Save failed: %v", err) - } -} diff --git a/go/internal/e2e/rpc_server_misc_e2e_test.go b/go/internal/e2e/rpc_server_misc_e2e_test.go deleted file mode 100644 index 8565975550..0000000000 --- a/go/internal/e2e/rpc_server_misc_e2e_test.go +++ /dev/null @@ -1,299 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestRpcServerMisc(t *testing.T) { - ctx := testharness.NewTestContext(t) - - t.Run("should_reload_user_settings", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - if _, err := client.RPC.User.Settings().Reload(t.Context()); err != nil { - t.Fatalf("User.Settings.Reload failed: %v", err) - } - }) - - t.Run("should_get_set_and_clear_user_settings", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - initial, err := client.RPC.User.Settings().Get(t.Context()) - if err != nil { - t.Fatalf("User.Settings.Get initial failed: %v", err) - } - if initial.Settings == nil { - t.Fatal("Expected settings map") - } - var key string - var value bool - for candidateKey, setting := range initial.Settings { - if candidateValue, ok := setting.Value.(bool); ok { - key = candidateKey - value = candidateValue - break - } - } - if key == "" { - t.Fatalf("Expected at least one boolean setting, got %+v", initial.Settings) - } - toggledValue := !value - - set, err := client.RPC.User.Settings().Set(t.Context(), &rpc.UserSettingsSetRequest{ - Settings: map[string]any{key: toggledValue}, - }) - if err != nil { - t.Fatalf("User.Settings.Set(toggle) failed: %v", err) - } - if len(set.ShadowedKeys) != 0 { - t.Fatalf("Expected no shadowed settings keys, got %+v", set.ShadowedKeys) - } - if _, err := client.RPC.User.Settings().Reload(t.Context()); err != nil { - t.Fatalf("User.Settings.Reload after set failed: %v", err) - } - afterSet, err := client.RPC.User.Settings().Get(t.Context()) - if err != nil { - t.Fatalf("User.Settings.Get after set failed: %v", err) - } - metadata, ok := afterSet.Settings[key] - if !ok { - t.Fatalf("Expected setting %q in %+v", key, afterSet.Settings) - } - if metadata.Value != toggledValue || metadata.IsDefault { - t.Fatalf("Expected explicit true setting, got %+v", metadata) - } - - clear, err := client.RPC.User.Settings().Set(t.Context(), &rpc.UserSettingsSetRequest{ - Settings: map[string]any{key: nil}, - }) - if err != nil { - t.Fatalf("User.Settings.Set(null) failed: %v", err) - } - if len(clear.ShadowedKeys) != 0 { - t.Fatalf("Expected no shadowed settings keys from clear, got %+v", clear.ShadowedKeys) - } - if _, err := client.RPC.User.Settings().Reload(t.Context()); err != nil { - t.Fatalf("User.Settings.Reload after clear failed: %v", err) - } - afterClear, err := client.RPC.User.Settings().Get(t.Context()) - if err != nil { - t.Fatalf("User.Settings.Get after clear failed: %v", err) - } - metadata, ok = afterClear.Settings[key] - if !ok { - t.Fatalf("Expected setting %q after clear in %+v", key, afterClear.Settings) - } - if !metadata.IsDefault { - t.Fatalf("Expected cleared setting to be default, got %+v", metadata) - } - }) - - t.Run("should_login_list_getcurrentauth_and_logout_account", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - if err := ctx.SetCopilotUserByToken("go-account-token", map[string]interface{}{ - "login": "go-account-user", - "copilot_plan": "individual_pro", - "endpoints": map[string]interface{}{ - "api": ctx.ProxyURL, - "telemetry": "https://localhost:1/telemetry", - }, - "analytics_tracking_id": "go-account-user-tracking-id", - }); err != nil { - t.Fatalf("SetCopilotUserByToken failed: %v", err) - } - client := newNoTokenClient(t, ctx) - defer client.ForceStop() - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - - initial, err := client.RPC.Account.GetCurrentAuth(t.Context()) - if err != nil { - t.Fatalf("Account.GetCurrentAuth initial failed: %v", err) - } - if initial.AuthInfo != nil { - t.Fatalf("Expected no initial auth info, got %+v", initial.AuthInfo) - } - - login, err := client.RPC.Account.Login(t.Context(), &rpc.AccountLoginRequest{ - Host: "https://github.com", - Login: rpcPtr("go-account-user"), - Token: "go-account-token", - }) - if err != nil { - t.Fatalf("Account.Login failed: %v", err) - } - if login == nil { - t.Fatal("Expected login result") - } - - current, err := client.RPC.Account.GetCurrentAuth(t.Context()) - if err != nil { - t.Fatalf("Account.GetCurrentAuth after login failed: %v", err) - } - authInfo, ok := current.AuthInfo.(*rpc.UserAuthInfo) - if !ok { - t.Fatalf("Expected user auth info after login, got %#v", current.AuthInfo) - } - if authInfo.Login != "go-account-user" || authInfo.Host != "https://github.com" { - t.Fatalf("Unexpected current auth info: %+v", authInfo) - } - - users, err := client.RPC.Account.GetAllUsers(t.Context()) - if err != nil { - t.Fatalf("Account.GetAllUsers failed: %v", err) - } - if users == nil { - t.Fatal("Expected non-nil users result") - return - } - for _, user := range *users { - userInfo, ok := user.AuthInfo.(*rpc.UserAuthInfo) - if !ok { - t.Fatalf("Expected user auth info in all users, got %#v", user.AuthInfo) - } - if userInfo.Login == "go-account-user" && (user.Token == nil || *user.Token != "go-account-token") { - t.Fatalf("Expected logged-in user's token to round trip, got %+v", user) - } - } - - logout, err := client.RPC.Account.Logout(t.Context(), &rpc.AccountLogoutRequest{ - AuthInfo: authInfo, - }) - if err != nil { - t.Fatalf("Account.Logout failed: %v", err) - } - if logout.HasMoreUsers { - t.Fatalf("Expected no users after isolated logout, got %+v", logout) - } - afterLogout, err := client.RPC.Account.GetCurrentAuth(t.Context()) - if err != nil { - t.Fatalf("Account.GetCurrentAuth after logout failed: %v", err) - } - if afterLogout.AuthInfo != nil { - t.Fatalf("Expected no auth after logout, got %+v", afterLogout.AuthInfo) - } - }) - - t.Run("should_report_agent_registry_spawn_gate_closed", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - _, err := client.RPC.AgentRegistry.Spawn(t.Context(), &rpc.AgentRegistrySpawnRequest{Cwd: ctx.WorkDir}) - if err == nil { - t.Fatal("Expected AgentRegistry.Spawn to be rejected by the closed spawn gate") - } - message := err.Error() - assertPortedNoUnhandledMethod(t, message) - assertPortedContainsFold(t, message, "agentRegistry.spawn") - if !strings.Contains(strings.ToLower(message), "not enabled") && !strings.Contains(strings.ToLower(message), "no delegate") { - t.Fatalf("Expected agentRegistry.spawn gate error, got %s", message) - } - }) - - t.Run("should_shut_down_owned_runtime", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - client := newStartedPortedClient(t, ctx) - defer client.ForceStop() - - if _, err := client.RPC.User.Settings().Reload(t.Context()); err != nil { - t.Fatalf("User.Settings.Reload before shutdown failed: %v", err) - } - if _, err := client.RPC.Runtime.Shutdown(t.Context()); err != nil { - t.Fatalf("Runtime.Shutdown failed: %v", err) - } - - waitForRPCCondition(t, 15*time.Second, "runtime to stop serving RPCs after shutdown", func() (bool, error) { - _, err := client.RPC.User.Settings().Reload(t.Context()) - return err != nil, nil - }) - }) - - t.Run("should_report_not_found_when_opening_session_without_context", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - result, err := client.RPC.Sessions.Open(t.Context(), nil) - if err != nil { - t.Fatalf("Sessions.Open failed: %v", err) - } - if result.Status != rpc.SessionsOpenStatusNotFound { - t.Fatalf("Expected Sessions.Open status not_found, got %+v", result) - } - if result.SessionID != nil { - t.Fatalf("Expected nil session ID for not_found, got %q", *result.SessionID) - } - }) - - t.Run("should_reject_send_attachments_from_non_extension_connection", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - _, err := session.RPC.Extensions.SendAttachmentsToMessage(t.Context(), &rpc.SendAttachmentsToMessageParams{Attachments: []rpc.PushAttachment{}}) - if err == nil { - t.Fatal("Expected SendAttachmentsToMessage from a normal SDK connection to fail") - } - message := err.Error() - assertPortedNoUnhandledMethod(t, message) - assertPortedContainsFold(t, message, "extension") - }) -} - -func newNoTokenClient(t *testing.T, ctx *testharness.TestContext) *copilot.Client { - t.Helper() - env := append([]string{}, ctx.Env()...) - env = append(env, - "COPILOT_HOME="+t.TempDir(), - "GH_CONFIG_DIR="+t.TempDir(), - "GH_TOKEN=", - "GITHUB_TOKEN=", - "COPILOT_SDK_AUTH_TOKEN=", - ) - useLoggedInUser := false - return copilot.NewClient(&copilot.ClientOptions{ - Connection: copilot.StdioConnection{Path: ctx.CLIPath}, - WorkingDirectory: ctx.WorkDir, - Env: env, - UseLoggedInUser: &useLoggedInUser, - }) -} diff --git a/go/internal/e2e/rpc_server_plugins_e2e_test.go b/go/internal/e2e/rpc_server_plugins_e2e_test.go deleted file mode 100644 index 2bea21cc3c..0000000000 --- a/go/internal/e2e/rpc_server_plugins_e2e_test.go +++ /dev/null @@ -1,489 +0,0 @@ -package e2e - -import ( - "os" - "path/filepath" - "strings" - "testing" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -const ( - portedMarketplaceName = "go-e2e-marketplace" - portedPluginName = "go-e2e-plugin" - portedDirectPluginName = "go-e2e-direct" -) - -func TestRpcServerPlugins(t *testing.T) { - ctx := testharness.NewTestContext(t) - - t.Run("should_install_and_list_plugin_from_local_marketplace", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - marketplaceDir := createPortedLocalMarketplaceFixture(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - if _, err := client.RPC.Plugins.Marketplaces().Add(t.Context(), &rpc.PluginsMarketplacesAddRequest{Source: marketplaceDir}); err != nil { - t.Fatalf("Plugins.Marketplaces.Add failed: %v", err) - } - - spec := portedPluginName + "@" + portedMarketplaceName - install, err := client.RPC.Plugins.Install(t.Context(), &rpc.PluginsInstallRequest{Source: spec}) - if err != nil { - t.Fatalf("Plugins.Install failed: %v", err) - } - if install.Plugin.Name != portedPluginName { - t.Fatalf("Expected installed plugin name %q, got %q", portedPluginName, install.Plugin.Name) - } - if install.Plugin.Marketplace != portedMarketplaceName { - t.Fatalf("Expected marketplace %q, got %q", portedMarketplaceName, install.Plugin.Marketplace) - } - if !install.Plugin.Enabled { - t.Fatal("Expected installed marketplace plugin to be enabled") - } - if install.SkillsInstalled < 1 { - t.Fatalf("Expected at least one skill, got %d", install.SkillsInstalled) - } - if install.DeprecationWarning != nil { - t.Fatalf("Marketplace install should not return deprecation warning, got %q", *install.DeprecationWarning) - } - - afterInstall, err := client.RPC.Plugins.List(t.Context()) - if err != nil { - t.Fatalf("Plugins.List after install failed: %v", err) - } - listed := findPortedInstalledPlugin(afterInstall.Plugins, portedPluginName, portedMarketplaceName) - if listed == nil { - t.Fatalf("Expected installed plugin %q in marketplace %q", portedPluginName, portedMarketplaceName) - return - } - if !listed.Enabled { - t.Fatal("Expected listed marketplace plugin to be enabled") - } - - }) - - t.Run("should_enable_and_disable_marketplace_plugin", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - marketplaceDir := createPortedLocalMarketplaceFixture(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - spec := portedPluginName + "@" + portedMarketplaceName - if _, err := client.RPC.Plugins.Marketplaces().Add(t.Context(), &rpc.PluginsMarketplacesAddRequest{Source: marketplaceDir}); err != nil { - t.Fatalf("Plugins.Marketplaces.Add failed: %v", err) - } - if _, err := client.RPC.Plugins.Install(t.Context(), &rpc.PluginsInstallRequest{Source: spec}); err != nil { - t.Fatalf("Plugins.Install failed: %v", err) - } - - if _, err := client.RPC.Plugins.Disable(t.Context(), &rpc.PluginsDisableRequest{Names: []string{spec}}); err != nil { - t.Fatalf("Plugins.Disable failed: %v", err) - } - if plugin := getPortedInstalledPlugin(t, client, portedPluginName, portedMarketplaceName); plugin.Enabled { - t.Fatal("Expected plugin to be disabled") - } - - if _, err := client.RPC.Plugins.Enable(t.Context(), &rpc.PluginsEnableRequest{Names: []string{spec}}); err != nil { - t.Fatalf("Plugins.Enable failed: %v", err) - } - if plugin := getPortedInstalledPlugin(t, client, portedPluginName, portedMarketplaceName); !plugin.Enabled { - t.Fatal("Expected plugin to be enabled") - } - }) - - t.Run("should_update_single_marketplace_plugin", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - marketplaceDir := createPortedLocalMarketplaceFixture(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - spec := portedPluginName + "@" + portedMarketplaceName - if _, err := client.RPC.Plugins.Marketplaces().Add(t.Context(), &rpc.PluginsMarketplacesAddRequest{Source: marketplaceDir}); err != nil { - t.Fatalf("Plugins.Marketplaces.Add failed: %v", err) - } - if _, err := client.RPC.Plugins.Install(t.Context(), &rpc.PluginsInstallRequest{Source: spec}); err != nil { - t.Fatalf("Plugins.Install failed: %v", err) - } - - update, err := client.RPC.Plugins.Update(t.Context(), &rpc.PluginsUpdateRequest{Name: spec}) - if err != nil { - t.Fatalf("Plugins.Update failed: %v", err) - } - if update.SkillsInstalled < 1 { - t.Fatalf("Expected at least one skill, got %d", update.SkillsInstalled) - } - if update.PreviousVersion == nil || *update.PreviousVersion != "1.0.0" { - t.Fatalf("Expected previous version 1.0.0, got %v", update.PreviousVersion) - } - if update.NewVersion == nil || *update.NewVersion != "1.0.0" { - t.Fatalf("Expected new version 1.0.0, got %v", update.NewVersion) - } - }) - - t.Run("should_update_all_installed_plugins", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - marketplaceDir := createPortedLocalMarketplaceFixture(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - spec := portedPluginName + "@" + portedMarketplaceName - if _, err := client.RPC.Plugins.Marketplaces().Add(t.Context(), &rpc.PluginsMarketplacesAddRequest{Source: marketplaceDir}); err != nil { - t.Fatalf("Plugins.Marketplaces.Add failed: %v", err) - } - if _, err := client.RPC.Plugins.Install(t.Context(), &rpc.PluginsInstallRequest{Source: spec}); err != nil { - t.Fatalf("Plugins.Install failed: %v", err) - } - - result, err := client.RPC.Plugins.UpdateAll(t.Context()) - if err != nil { - t.Fatalf("Plugins.UpdateAll failed: %v", err) - } - var matches []rpc.PluginUpdateAllEntry - for _, entry := range result.Results { - if entry.Name == portedPluginName && entry.Marketplace == portedMarketplaceName { - matches = append(matches, entry) - } - } - if len(matches) != 1 { - t.Fatalf("Expected exactly one update result for %q, got %d in %+v", spec, len(matches), result.Results) - } - entry := matches[0] - if !entry.Success { - t.Fatalf("Expected update all entry to succeed, got error %v", entry.Error) - } - if entry.SkillsInstalled == nil || *entry.SkillsInstalled < 1 { - t.Fatalf("Expected at least one skill installed, got %v", entry.SkillsInstalled) - } - }) - - t.Run("should_install_direct_local_plugin_with_deprecation_warning", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - pluginDir := createPortedDirectPluginFixture(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - install, err := client.RPC.Plugins.Install(t.Context(), &rpc.PluginsInstallRequest{Source: pluginDir}) - if err != nil { - t.Fatalf("Plugins.Install direct failed: %v", err) - } - if install.Plugin.Name != portedDirectPluginName { - t.Fatalf("Expected installed plugin name %q, got %q", portedDirectPluginName, install.Plugin.Name) - } - if install.Plugin.Marketplace != "" { - t.Fatalf("Expected direct plugin marketplace to be empty, got %q", install.Plugin.Marketplace) - } - if install.DeprecationWarning == nil || !strings.Contains(strings.ToLower(*install.DeprecationWarning), "deprecated") { - t.Fatalf("Expected deprecation warning containing deprecated, got %v", install.DeprecationWarning) - } - if install.SkillsInstalled < 1 { - t.Fatalf("Expected at least one skill, got %d", install.SkillsInstalled) - } - - afterInstall, err := client.RPC.Plugins.List(t.Context()) - if err != nil { - t.Fatalf("Plugins.List after direct install failed: %v", err) - } - if countPortedInstalledPluginByName(afterInstall.Plugins, portedDirectPluginName) != 1 { - t.Fatalf("Expected exactly one direct plugin named %q, got %+v", portedDirectPluginName, afterInstall.Plugins) - } - if install.Plugin.DirectSourceID == nil { - t.Fatal("Expected direct plugin install to include directSourceId") - } - - if _, err := client.RPC.Plugins.Uninstall(t.Context(), &rpc.PluginsUninstallRequest{ - DirectSourceID: install.Plugin.DirectSourceID, - Name: portedDirectPluginName, - }); err != nil { - t.Fatalf("Plugins.Uninstall direct failed: %v", err) - } - afterUninstall, err := client.RPC.Plugins.List(t.Context()) - if err != nil { - t.Fatalf("Plugins.List after direct uninstall failed: %v", err) - } - if countPortedInstalledPluginByName(afterUninstall.Plugins, portedDirectPluginName) != 0 { - t.Fatalf("Expected direct plugin %q to be removed, got %+v", portedDirectPluginName, afterUninstall.Plugins) - } - }) - - t.Run("should_list_browse_refresh_and_remove_local_marketplace", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - marketplaceDir := createPortedLocalMarketplaceFixture(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - add, err := client.RPC.Plugins.Marketplaces().Add(t.Context(), &rpc.PluginsMarketplacesAddRequest{Source: marketplaceDir}) - if err != nil { - t.Fatalf("Plugins.Marketplaces.Add failed: %v", err) - } - if add.Name != portedMarketplaceName { - t.Fatalf("Expected marketplace name %q, got %q", portedMarketplaceName, add.Name) - } - - list, err := client.RPC.Plugins.Marketplaces().List(t.Context()) - if err != nil { - t.Fatalf("Plugins.Marketplaces.List failed: %v", err) - } - mine := findPortedMarketplace(list.Marketplaces, portedMarketplaceName) - if mine == nil { - t.Fatalf("Expected marketplace %q in list %+v", portedMarketplaceName, list.Marketplaces) - return - } - if mine.IsDefault != nil && *mine.IsDefault { - t.Fatal("Expected local marketplace not to be marked default") - } - if !containsPortedDefaultMarketplace(list.Marketplaces) { - t.Fatalf("Expected built-in default marketplace in %+v", list.Marketplaces) - } - - browse, err := client.RPC.Plugins.Marketplaces().Browse(t.Context(), &rpc.PluginsMarketplacesBrowseRequest{Name: portedMarketplaceName}) - if err != nil { - t.Fatalf("Plugins.Marketplaces.Browse failed: %v", err) - } - var advertised []rpc.MarketplacePluginInfo - for _, plugin := range browse.Plugins { - if plugin.Name == portedPluginName { - advertised = append(advertised, plugin) - } - } - if len(advertised) != 1 { - t.Fatalf("Expected one advertised plugin %q, got %+v", portedPluginName, browse.Plugins) - } - if advertised[0].Description == nil || strings.TrimSpace(*advertised[0].Description) == "" { - t.Fatalf("Expected advertised plugin description, got %+v", advertised[0]) - } - - refreshName := portedMarketplaceName - refresh, err := client.RPC.Plugins.Marketplaces().Refresh(t.Context(), &rpc.PluginsMarketplacesRefreshRequest{Name: &refreshName}) - if err != nil { - t.Fatalf("Plugins.Marketplaces.Refresh failed: %v", err) - } - var refreshMatches []rpc.MarketplaceRefreshEntry - for _, entry := range refresh.Results { - if entry.Name == portedMarketplaceName { - refreshMatches = append(refreshMatches, entry) - } - } - if len(refreshMatches) != 1 { - t.Fatalf("Expected one refresh result for %q, got %+v", portedMarketplaceName, refresh.Results) - } - if !refreshMatches[0].Success { - t.Fatalf("Expected refresh success, got error %v", refreshMatches[0].Error) - } - - remove, err := client.RPC.Plugins.Marketplaces().Remove(t.Context(), &rpc.PluginsMarketplacesRemoveRequest{Name: portedMarketplaceName}) - if err != nil { - t.Fatalf("Plugins.Marketplaces.Remove failed: %v", err) - } - if !remove.Removed { - t.Fatalf("Expected marketplace removal, got %+v", remove) - } - - afterRemove, err := client.RPC.Plugins.Marketplaces().List(t.Context()) - if err != nil { - t.Fatalf("Plugins.Marketplaces.List after remove failed: %v", err) - } - if findPortedMarketplace(afterRemove.Marketplaces, portedMarketplaceName) != nil { - t.Fatalf("Expected marketplace %q to be removed, got %+v", portedMarketplaceName, afterRemove.Marketplaces) - } - }) - - t.Run("should_reload_mcp_config_cache", func(t *testing.T) { - if testharness.RunInIsolatedProcess(t) { - return - } - ctx.ConfigureForTest(t) - client := newStartedIsolatedPortedClient(t, ctx) - defer client.ForceStop() - - if _, err := client.RPC.MCP.Config().Reload(t.Context()); err != nil { - t.Fatalf("MCP.Config.Reload failed: %v", err) - } - }) -} - -func newStartedPortedClient(t *testing.T, ctx *testharness.TestContext, opts ...func(*copilot.ClientOptions)) *copilot.Client { - t.Helper() - client := ctx.NewClient(opts...) - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Start failed: %v", err) - } - return client -} - -func newStartedIsolatedPortedClient(t *testing.T, ctx *testharness.TestContext) *copilot.Client { - t.Helper() - home, err := os.MkdirTemp(ctx.WorkDir, "plugin-home-") - if err != nil { - t.Fatalf("Failed to create isolated plugin home: %v", err) - } - return newStartedPortedClient(t, ctx, func(opts *copilot.ClientOptions) { - opts.Env = append(opts.Env, - "COPILOT_HOME="+home, - "GH_CONFIG_DIR="+home, - "XDG_CONFIG_HOME="+home, - "XDG_STATE_HOME="+home, - ) - }) -} - -func createPortedSession(t *testing.T, client *copilot.Client, config *copilot.SessionConfig) *copilot.Session { - t.Helper() - if config == nil { - config = &copilot.SessionConfig{} - } - if config.OnPermissionRequest == nil { - config.OnPermissionRequest = copilot.PermissionHandler.ApproveAll - } - session, err := client.CreateSession(t.Context(), config) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - return session -} - -func assertPortedNoUnhandledMethod(t *testing.T, message string) { - t.Helper() - if strings.Contains(strings.ToLower(message), "unhandled method") { - t.Fatalf("Expected RPC to reach runtime, got %s", message) - } -} - -func assertPortedContainsFold(t *testing.T, message string, fragments ...string) { - t.Helper() - lower := strings.ToLower(message) - for _, fragment := range fragments { - if strings.Contains(lower, strings.ToLower(fragment)) { - return - } - } - t.Fatalf("Expected %q to contain one of %v", message, fragments) -} - -func createPortedLocalMarketplaceFixture(t *testing.T) string { - t.Helper() - dir := t.TempDir() - manifest := `{ - "name": "` + portedMarketplaceName + `", - "owner": { "name": "Copilot SDK E2E" }, - "metadata": { "description": "Local marketplace fixture for SDK E2E tests." }, - "plugins": [ - { - "name": "` + portedPluginName + `", - "source": "./` + portedPluginName + `", - "description": "E2E demo plugin advertised by the local marketplace.", - "version": "1.0.0" - } - ] -}` - if err := os.WriteFile(filepath.Join(dir, "marketplace.json"), []byte(manifest), 0644); err != nil { - t.Fatalf("Failed to write marketplace manifest: %v", err) - } - pluginDir := filepath.Join(dir, portedPluginName) - if err := os.MkdirAll(pluginDir, 0755); err != nil { - t.Fatalf("Failed to create marketplace plugin directory: %v", err) - } - writePortedSkillFile(t, pluginDir) - return dir -} - -func createPortedDirectPluginFixture(t *testing.T) string { - t.Helper() - dir := t.TempDir() - manifest := `{ - "name": "` + portedDirectPluginName + `", - "description": "E2E demo plugin installed directly from a local path.", - "version": "1.0.0" -}` - if err := os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(manifest), 0644); err != nil { - t.Fatalf("Failed to write direct plugin manifest: %v", err) - } - writePortedSkillFile(t, dir) - return dir -} - -func writePortedSkillFile(t *testing.T, pluginDir string) { - t.Helper() - const skill = `--- -name: go-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. -` - if err := os.WriteFile(filepath.Join(pluginDir, "SKILL.md"), []byte(skill), 0644); err != nil { - t.Fatalf("Failed to write skill file: %v", err) - } -} - -func getPortedInstalledPlugin(t *testing.T, client *copilot.Client, name, marketplace string) *rpc.InstalledPluginInfo { - t.Helper() - list, err := client.RPC.Plugins.List(t.Context()) - if err != nil { - t.Fatalf("Plugins.List failed: %v", err) - } - plugin := findPortedInstalledPlugin(list.Plugins, name, marketplace) - if plugin == nil { - t.Fatalf("Expected installed plugin %q in marketplace %q, got %+v", name, marketplace, list.Plugins) - } - return plugin -} - -func findPortedInstalledPlugin(plugins []rpc.InstalledPluginInfo, name, marketplace string) *rpc.InstalledPluginInfo { - for i := range plugins { - if plugins[i].Name == name && plugins[i].Marketplace == marketplace { - return &plugins[i] - } - } - return nil -} - -func countPortedInstalledPluginByName(plugins []rpc.InstalledPluginInfo, name string) int { - count := 0 - for _, plugin := range plugins { - if plugin.Name == name { - count++ - } - } - return count -} - -func findPortedMarketplace(marketplaces []rpc.MarketplaceInfo, name string) *rpc.MarketplaceInfo { - for i := range marketplaces { - if marketplaces[i].Name == name { - return &marketplaces[i] - } - } - return nil -} - -func containsPortedDefaultMarketplace(marketplaces []rpc.MarketplaceInfo) bool { - for _, marketplace := range marketplaces { - if marketplace.IsDefault != nil && *marketplace.IsDefault { - return true - } - } - return false -} diff --git a/go/internal/e2e/rpc_server_remote_control_e2e_test.go b/go/internal/e2e/rpc_server_remote_control_e2e_test.go deleted file mode 100644 index 7990b32c04..0000000000 --- a/go/internal/e2e/rpc_server_remote_control_e2e_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestRpcServerRemoteControl(t *testing.T) { - ctx := testharness.NewTestContext(t) - - t.Run("should_report_remote_control_status_as_off", func(t *testing.T) { - ctx.ConfigureForTest(t) - client := newStartedPortedClient(t, ctx) - defer client.ForceStop() - - result, err := client.RPC.Sessions.GetRemoteControlStatus(t.Context()) - if err != nil { - t.Fatalf("Sessions.GetRemoteControlStatus failed: %v", err) - } - assertPortedRemoteControlOff(t, result.Status) - }) - - t.Run("should_treat_set_steering_as_no_op_when_off", func(t *testing.T) { - ctx.ConfigureForTest(t) - client := newStartedPortedClient(t, ctx) - defer client.ForceStop() - - result, err := client.RPC.Sessions.SetRemoteControlSteering(t.Context(), &rpc.SessionsSetRemoteControlSteeringRequest{Enabled: false}) - if err != nil { - t.Fatalf("Sessions.SetRemoteControlSteering failed: %v", err) - } - assertPortedRemoteControlOff(t, result.Status) - }) - - t.Run("should_report_not_stopped_when_remote_control_is_off", func(t *testing.T) { - ctx.ConfigureForTest(t) - client := newStartedPortedClient(t, ctx) - defer client.ForceStop() - - result, err := client.RPC.Sessions.StopRemoteControl(t.Context(), &rpc.SessionsStopRemoteControlRequest{}) - if err != nil { - t.Fatalf("Sessions.StopRemoteControl failed: %v", err) - } - if result.Stopped { - t.Fatalf("Expected Stopped=false, got %+v", result) - } - assertPortedRemoteControlOff(t, result.Status) - }) - - t.Run("should_reject_transfer_when_off_with_compare_and_swap", func(t *testing.T) { - ctx.ConfigureForTest(t) - client := newStartedPortedClient(t, ctx) - defer client.ForceStop() - - from := "rc-from-" + randomHex(t) - result, err := client.RPC.Sessions.TransferRemoteControl(t.Context(), &rpc.SessionsTransferRemoteControlRequest{ - ToSessionID: "rc-to-" + randomHex(t), - ExpectedFromSessionID: &from, - }) - if err != nil { - t.Fatalf("Sessions.TransferRemoteControl failed: %v", err) - } - if result.Transferred { - t.Fatalf("Expected Transferred=false, got %+v", result) - } - assertPortedRemoteControlOff(t, result.Status) - }) - - t.Run("should_reach_runtime_when_starting_remote_control_for_unknown_session", func(t *testing.T) { - ctx.ConfigureForTest(t) - client := newStartedPortedClient(t, ctx) - defer client.ForceStop() - defer func() { - force := true - _, _ = client.RPC.Sessions.StopRemoteControl(t.Context(), &rpc.SessionsStopRemoteControlRequest{Force: &force}) - }() - - _, err := client.RPC.Sessions.StartRemoteControl(t.Context(), &rpc.SessionsStartRemoteControlRequest{ - SessionID: "missing-session-" + randomHex(t), - Config: rpc.RemoteControlConfig{ - Remote: false, - Explicit: false, - Silent: true, - Steerable: false, - }, - }) - if err == nil { - t.Fatal("Expected StartRemoteControl for an unknown session to fail") - } - message := err.Error() - assertPortedNoUnhandledMethod(t, message) - if !strings.Contains(strings.ToLower(message), "session") && !strings.Contains(strings.ToLower(message), "remote") { - t.Fatalf("Expected error to mention session or remote, got %s", message) - } - }) -} - -func assertPortedRemoteControlOff(t *testing.T, status rpc.RemoteControlStatus) { - t.Helper() - if status == nil { - t.Fatal("Expected remote control status, got nil") - } - if status.State() != rpc.RemoteControlStatusStateOff { - t.Fatalf("Expected remote control state off, got %s (%T)", status.State(), status) - } - if _, ok := status.(*rpc.RemoteControlStatusOff); !ok { - t.Fatalf("Expected *RemoteControlStatusOff, got %T", status) - } -} diff --git a/go/internal/e2e/rpc_session_state_e2e_test.go b/go/internal/e2e/rpc_session_state_e2e_test.go deleted file mode 100644 index 25852021ed..0000000000 --- a/go/internal/e2e/rpc_session_state_e2e_test.go +++ /dev/null @@ -1,1188 +0,0 @@ -package e2e - -import ( - "path/filepath" - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/RpcSessionStateTests.cs (snapshot category "rpc_session_state"). -// -// Reuses snapshot files in test/snapshots/rpc_session_state/. Tests that don't issue -// LLM calls don't need snapshots. -func TestRPCSessionStateE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - if err := client.Start(t.Context()); err != nil { - t.Fatalf("Failed to start client: %v", err) - } - - t.Run("should call session rpc model getCurrent", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-5", - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - result, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Model.GetCurrent failed: %v", err) - } - if result.ModelID == nil || *result.ModelID != "claude-sonnet-5" { - t.Fatalf("Expected current model claude-sonnet-5, got %+v", result) - } - }) - - // The runtime caches /models per (auth, base_url) for 30 minutes. SwitchTo - // needs gpt-5.4 in the cache, so use a distinct token to give this client an - // independent cache entry without changing the process-wide proxy. - t.Run("should call session rpc model switchTo", func(t *testing.T) { - const switchToken = "go-rpc-session-state-switch-token" - if err := ctx.SetDefaultCopilotUserByToken(switchToken); err != nil { - t.Fatalf("Failed to configure switch client user: %v", err) - } - switchClient := ctx.NewClient(func(options *copilot.ClientOptions) { - options.BaseDirectory = t.TempDir() - options.GitHubToken = switchToken - }) - t.Cleanup(func() { switchClient.ForceStop() }) - if err := switchClient.Start(t.Context()); err != nil { - t.Fatalf("Failed to start switch client: %v", err) - } - ctx.ConfigureForTest(t) - - session, err := switchClient.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-5", - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - before, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Model.GetCurrent before switch failed: %v", err) - } - if before.ModelID == nil { - t.Fatalf("Expected non-empty model before switch, got %+v", before) - } - - reasoningEffort := "high" - result, err := session.RPC.Model.SwitchTo(t.Context(), &rpc.ModelSwitchToRequest{ - ModelID: "gpt-5.4", - ReasoningEffort: &reasoningEffort, - }) - if err != nil { - t.Fatalf("Model.SwitchTo failed: %v", err) - } - if result.ModelID == nil || *result.ModelID != "gpt-5.4" { - t.Fatalf("Expected switch result model gpt-5.4, got %+v", result) - } - after, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Model.GetCurrent after switch failed: %v", err) - } - if after.ModelID == nil || *after.ModelID != "gpt-5.4" { - t.Fatalf("Model.GetCurrent did not reflect SwitchTo; before=%q after=%+v", *before.ModelID, after) - } - }) - - t.Run("should get and set session mode", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - initial, err := session.RPC.Mode.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get mode: %v", err) - } - if initial == nil || *initial != rpc.SessionModeInteractive { - t.Errorf("Expected initial mode 'interactive', got %v", initial) - } - - if _, err := session.RPC.Mode.Set(t.Context(), &rpc.ModeSetRequest{Mode: rpc.SessionModePlan}); err != nil { - t.Fatalf("Failed to set mode to plan: %v", err) - } - afterPlan, err := session.RPC.Mode.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get mode after plan: %v", err) - } - if afterPlan == nil || *afterPlan != rpc.SessionModePlan { - t.Errorf("Expected mode 'plan' after set, got %v", afterPlan) - } - - if _, err := session.RPC.Mode.Set(t.Context(), &rpc.ModeSetRequest{Mode: rpc.SessionModeInteractive}); err != nil { - t.Fatalf("Failed to set mode to interactive: %v", err) - } - final, err := session.RPC.Mode.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get mode after revert: %v", err) - } - if final == nil || *final != rpc.SessionModeInteractive { - t.Errorf("Expected mode 'interactive' after revert, got %v", final) - } - }) - - t.Run("should shutdown session with routine type", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - awaitShutdown := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionShutdown, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionShutdownData) - return ok && data.ShutdownType == copilot.ShutdownTypeRoutine - }, - "session.shutdown routine event", - ) - - reason := "Go SDK E2E shutdown coverage" - shutdownType := rpc.ShutdownTypeRoutine - if _, err := session.RPC.Shutdown(t.Context(), &rpc.ShutdownRequest{Type: &shutdownType, Reason: &reason}); err != nil { - t.Fatalf("Shutdown failed: %v", err) - } - event := awaitEvent(t, awaitShutdown) - if data := event.Data.(*copilot.SessionShutdownData); data.ShutdownType != copilot.ShutdownTypeRoutine { - t.Fatalf("Expected routine shutdown event, got %+v", data) - } - }) - - t.Run("should set and get each session mode value", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - for _, mode := range []rpc.SessionMode{rpc.SessionModeInteractive, rpc.SessionModePlan, rpc.SessionModeAutopilot} { - if _, err := session.RPC.Mode.Set(t.Context(), &rpc.ModeSetRequest{Mode: mode}); err != nil { - t.Fatalf("Failed to set mode %q: %v", mode, err) - } - got, err := session.RPC.Mode.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get mode %q: %v", mode, err) - } - if got == nil || *got != mode { - t.Fatalf("Expected mode %q, got %v", mode, got) - } - } - }) - - t.Run("should read update and delete plan", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - initial, err := session.RPC.Plan.Read(t.Context()) - if err != nil { - t.Fatalf("Failed to read plan: %v", err) - } - if initial.Exists { - t.Error("Expected plan to not exist initially") - } - if initial.Content != nil { - t.Error("Expected plan content to be nil initially") - } - - const planContent = "# Test Plan\n\n- Step 1\n- Step 2" - if _, err := session.RPC.Plan.Update(t.Context(), &rpc.PlanUpdateRequest{Content: planContent}); err != nil { - t.Fatalf("Failed to update plan: %v", err) - } - - afterUpdate, err := session.RPC.Plan.Read(t.Context()) - if err != nil { - t.Fatalf("Failed to read plan after update: %v", err) - } - if !afterUpdate.Exists { - t.Error("Expected plan to exist after update") - } - if afterUpdate.Content == nil || *afterUpdate.Content != planContent { - t.Errorf("Expected plan content %q, got %v", planContent, afterUpdate.Content) - } - - if _, err := session.RPC.Plan.Delete(t.Context()); err != nil { - t.Fatalf("Failed to delete plan: %v", err) - } - - afterDelete, err := session.RPC.Plan.Read(t.Context()) - if err != nil { - t.Fatalf("Failed to read plan after delete: %v", err) - } - if afterDelete.Exists { - t.Error("Expected plan to not exist after delete") - } - if afterDelete.Content != nil { - t.Error("Expected plan content to be nil after delete") - } - }) - - t.Run("should call workspace file rpc methods", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - initial, err := session.RPC.Workspaces.ListFiles(t.Context()) - if err != nil { - t.Fatalf("Failed to list workspace files: %v", err) - } - if initial.Files == nil { - t.Error("Expected workspace files slice to be non-nil") - } - - if _, err := session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{ - Path: "test.txt", - Content: "Hello, workspace!", - }); err != nil { - t.Fatalf("Failed to create workspace file: %v", err) - } - - afterCreate, err := session.RPC.Workspaces.ListFiles(t.Context()) - if err != nil { - t.Fatalf("Failed to list workspace files after create: %v", err) - } - if !containsString(afterCreate.Files, "test.txt") { - t.Errorf("Expected workspace files to contain 'test.txt', got %v", afterCreate.Files) - } - - file, err := session.RPC.Workspaces.ReadFile(t.Context(), &rpc.WorkspacesReadFileRequest{Path: "test.txt"}) - if err != nil { - t.Fatalf("Failed to read workspace file: %v", err) - } - if file.Content != "Hello, workspace!" { - t.Errorf("Expected file content 'Hello, workspace!', got %q", file.Content) - } - - workspace, err := session.RPC.Workspaces.GetWorkspace(t.Context()) - if err != nil { - t.Fatalf("Failed to get workspace: %v", err) - } - if workspace.Workspace == nil { - t.Fatal("Expected non-nil workspace metadata") - } - if workspace.Workspace.ID == "" { - t.Error("Expected workspace.ID to be non-empty") - } - }) - - t.Run("should reject workspace file path traversal", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - for _, path := range []string{"../escaped.txt", "../../escaped.txt", "nested/../../../escaped.txt"} { - _, err := session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{ - Path: path, - Content: "should not land outside workspace", - }) - if err == nil || !strings.Contains(strings.ToLower(err.Error()), "workspace files directory") { - t.Fatalf("Expected CreateFile(%q) to reject traversal, got %v", path, err) - } - _, err = session.RPC.Workspaces.ReadFile(t.Context(), &rpc.WorkspacesReadFileRequest{Path: path}) - if err == nil || !strings.Contains(strings.ToLower(err.Error()), "workspace files directory") { - t.Fatalf("Expected ReadFile(%q) to reject traversal, got %v", path, err) - } - } - }) - - t.Run("should create workspace file with nested path auto creating dirs", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - nestedPath := "nested-" + randomHex(t) + "/subdir/file.txt" - if _, err := session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{Path: nestedPath, Content: "nested content"}); err != nil { - t.Fatalf("Failed to create nested workspace file: %v", err) - } - read, err := session.RPC.Workspaces.ReadFile(t.Context(), &rpc.WorkspacesReadFileRequest{Path: nestedPath}) - if err != nil { - t.Fatalf("Failed to read nested workspace file: %v", err) - } - if read.Content != "nested content" { - t.Fatalf("Expected nested content, got %q", read.Content) - } - list, err := session.RPC.Workspaces.ListFiles(t.Context()) - if err != nil { - t.Fatalf("Failed to list files: %v", err) - } - found := false - for _, file := range list.Files { - if filepath.ToSlash(file) == nestedPath { - found = true - break - } - } - if !found { - t.Fatalf("Expected list to contain nested file %q, got %v", nestedPath, list.Files) - } - }) - - t.Run("should report error reading nonexistent workspace file", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - _, err = session.RPC.Workspaces.ReadFile(t.Context(), &rpc.WorkspacesReadFileRequest{Path: "never-exists-" + randomHex(t) + ".txt"}) - if err == nil { - t.Fatal("Expected reading nonexistent workspace file to fail") - } - }) - - t.Run("should update existing workspace file with update operation", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - path := "reused-" + randomHex(t) + ".txt" - if _, err := session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{Path: path, Content: "v1"}); err != nil { - t.Fatalf("Failed to create workspace file: %v", err) - } - awaitUpdate := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionWorkspaceFileChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionWorkspaceFileChangedData) - return ok && data.Path == path && data.Operation == copilot.WorkspaceFileChangedOperationUpdate - }, - "workspace_file_changed update event", - ) - if _, err := session.RPC.Workspaces.CreateFile(t.Context(), &rpc.WorkspacesCreateFileRequest{Path: path, Content: "v2"}); err != nil { - t.Fatalf("Failed to update workspace file: %v", err) - } - event := awaitEvent(t, awaitUpdate) - if data := event.Data.(*copilot.SessionWorkspaceFileChangedData); data.Operation != copilot.WorkspaceFileChangedOperationUpdate { - t.Fatalf("Expected update operation, got %+v", data) - } - read, err := session.RPC.Workspaces.ReadFile(t.Context(), &rpc.WorkspacesReadFileRequest{Path: path}) - if err != nil { - t.Fatalf("Failed to read updated workspace file: %v", err) - } - if read.Content != "v2" { - t.Fatalf("Expected updated content v2, got %q", read.Content) - } - }) - - t.Run("should get and set session metadata", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - if _, err := session.RPC.Name.Set(t.Context(), &rpc.NameSetRequest{Name: "SDK test session"}); err != nil { - t.Fatalf("Failed to set session name: %v", err) - } - name, err := session.RPC.Name.Get(t.Context()) - if err != nil { - t.Fatalf("Failed to get session name: %v", err) - } - if name.Name == nil || *name.Name != "SDK test session" { - t.Errorf("Expected session name 'SDK test session', got %v", name.Name) - } - - sources, err := session.RPC.Instructions.GetSources(t.Context()) - if err != nil { - t.Fatalf("Failed to get instruction sources: %v", err) - } - if sources.Sources == nil { - t.Error("Expected instructions.Sources to be non-nil") - } - }) - - t.Run("should reject empty or whitespace session name", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - for _, name := range []string{"", " ", "\t\n \r"} { - _, err := session.RPC.Name.Set(t.Context(), &rpc.NameSetRequest{Name: name}) - if err == nil || !strings.Contains(strings.ToLower(err.Error()), "empty") { - t.Fatalf("Expected setting whitespace name %q to fail with empty-name error, got %v", name, err) - } - } - }) - - t.Run("should emit title changed event each time name set is called", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - titleA := "Title-A-" + randomHex(t) - awaitFirst := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionTitleChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionTitleChangedData) - return ok && data.Title == titleA - }, - "first title_changed event", - ) - if _, err := session.RPC.Name.Set(t.Context(), &rpc.NameSetRequest{Name: titleA}); err != nil { - t.Fatalf("Failed to set first session name: %v", err) - } - awaitEvent(t, awaitFirst) - - titleB := "Title-B-" + randomHex(t) - awaitSecond := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionTitleChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionTitleChangedData) - return ok && data.Title == titleB - }, - "second title_changed event", - ) - if _, err := session.RPC.Name.Set(t.Context(), &rpc.NameSetRequest{Name: titleB}); err != nil { - t.Fatalf("Failed to set second session name: %v", err) - } - event := awaitEvent(t, awaitSecond) - if data := event.Data.(*copilot.SessionTitleChangedData); data.Title != titleB { - t.Fatalf("Expected title %q, got %+v", titleB, data) - } - }) - - t.Run("should call metadata snapshot set working directory and record context change", func(t *testing.T) { - firstDirectory := createUniqueRPCWorkDirectory(t, ctx, "rpc-session-state-first") - secondDirectory := createUniqueRPCWorkDirectory(t, ctx, "rpc-session-state-second") - branch := "rpc-context-" + randomHex(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-5", - WorkingDirectory: firstDirectory, - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - initial, err := session.RPC.Metadata.Snapshot(t.Context()) - if err != nil { - t.Fatalf("Metadata.Snapshot failed: %v", err) - } - if initial.SessionID != session.SessionID || initial.CurrentMode != rpc.MetadataSnapshotCurrentModeInteractive || - initial.SelectedModel == nil || *initial.SelectedModel != "claude-sonnet-5" || - initial.IsRemote || initial.AlreadyInUse || initial.StartTime.IsZero() || initial.ModifiedTime.IsZero() || - initial.Workspace == nil || initial.WorkspacePath == nil || *initial.WorkspacePath == "" { - t.Fatalf("Unexpected initial metadata snapshot: %+v", initial) - } - assertRPCPathEqual(t, firstDirectory, initial.WorkingDirectory) - - setWorkingDirectory, err := session.RPC.Metadata.SetWorkingDirectory(t.Context(), &rpc.MetadataSetWorkingDirectoryRequest{WorkingDirectory: secondDirectory}) - if err != nil { - t.Fatalf("Metadata.SetWorkingDirectory failed: %v", err) - } - assertRPCPathEqual(t, secondDirectory, setWorkingDirectory.WorkingDirectory) - - waitForRPCCondition(t, 15*time.Second, "metadata snapshot working directory update", func() (bool, error) { - snapshot, err := session.RPC.Metadata.Snapshot(t.Context()) - return err == nil && rpcPathsEqual(secondDirectory, snapshot.WorkingDirectory), err - }) - - awaitContextChanged := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionContextChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionContextChangedData) - return ok && data.Branch != nil && *data.Branch == branch - }, - "session.context_changed event", - ) - - repo := "github/copilot-sdk-e2e" - repoHost := "github.com" - hostType := rpc.SessionWorkingDirectoryContextHostTypeGitHub - baseCommit := "0000000000000000000000000000000000000000" - headCommit := "1111111111111111111111111111111111111111" - // For local sessions the CLI treats the session cwd as authoritative, so a - // RecordContextChange that reports a divergent cwd is ignored and emits no event. - // Report the current working directory (secondDirectory) to observe the change. - if _, err := session.RPC.Metadata.RecordContextChange(t.Context(), &rpc.MetadataRecordContextChangeRequest{ - Context: rpc.SessionWorkingDirectoryContext{ - Cwd: secondDirectory, - GitRoot: &firstDirectory, - Branch: &branch, - Repository: &repo, - RepositoryHost: &repoHost, - HostType: &hostType, - BaseCommit: &baseCommit, - HeadCommit: &headCommit, - }, - }); err != nil { - t.Fatalf("Metadata.RecordContextChange failed: %v", err) - } - contextChanged := awaitEvent(t, awaitContextChanged) - data := contextChanged.Data.(*copilot.SessionContextChangedData) - assertRPCPathEqual(t, secondDirectory, data.Cwd) - if data.GitRoot == nil { - t.Fatal("Expected context changed git root") - } - assertRPCPathEqual(t, firstDirectory, *data.GitRoot) - if data.Branch == nil || *data.Branch != branch || - data.Repository == nil || *data.Repository != repo || - data.RepositoryHost == nil || *data.RepositoryHost != repoHost || - data.HostType == nil || string(*data.HostType) != "github" || - data.BaseCommit == nil || *data.BaseCommit != baseCommit || - data.HeadCommit == nil || *data.HeadCommit != headCommit { - t.Fatalf("Unexpected context changed payload: %+v", data) - } - }) - - t.Run("should update options and initialize session services", func(t *testing.T) { - // TODO(cli-1.0.81-2): under CLI 1.0.81-2 this subtest issues model traffic before the - // replaying proxy is configured, so the proxy rejects it with "not yet initialized". - // Re-enable once the runtime restores the previous option-update ordering. - t.Skip("blocked on CLI 1.0.81-2 session option/service initialization ordering") - initialDirectory := createUniqueRPCWorkDirectory(t, ctx, "rpc-session-state-initial") - optionsDirectory := createUniqueRPCWorkDirectory(t, ctx, "rpc-session-state-options") - featureName := "rpc-session-state-" + randomHex(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - WorkingDirectory: initialDirectory, - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - update, err := session.RPC.Options.Update(t.Context(), &rpc.SessionUpdateOptionsParams{ - ClientName: rpcPtr("go-sdk-rpc-session-state-e2e"), - LspClientName: rpcPtr("go-sdk-rpc-session-state-lsp"), - IntegrationID: rpcPtr("go-sdk-" + randomHex(t)), - FeatureFlags: map[string]bool{featureName: true}, - WorkingDirectory: &optionsDirectory, - CoauthorEnabled: rpcPtr(false), - EnableStreaming: rpcPtr(false), - AskUserDisabled: rpcPtr(true), - }) - if err != nil { - t.Fatalf("Options.Update failed: %v", err) - } - if !update.Success { - t.Fatalf("Expected Options.Update Success=true, got %+v", update) - } - - waitForRPCCondition(t, 15*time.Second, "options working directory to reach metadata snapshot", func() (bool, error) { - snapshot, err := session.RPC.Metadata.Snapshot(t.Context()) - return err == nil && rpcPathsEqual(optionsDirectory, snapshot.WorkingDirectory), err - }) - - if _, err := session.RPC.Lsp.Initialize(t.Context(), &rpc.LspInitializeRequest{ - WorkingDirectory: &optionsDirectory, - GitRoot: &initialDirectory, - Force: rpcPtr(true), - }); err != nil { - t.Fatalf("Lsp.Initialize failed: %v", err) - } - if _, err := session.RPC.Telemetry.SetFeatureOverrides(t.Context(), &rpc.TelemetrySetFeatureOverridesRequest{ - Features: map[string]string{ - "rpc_session_state_feature": featureName, - "rpc_session_state_value": "enabled", - }, - }); err != nil { - t.Fatalf("Telemetry.SetFeatureOverrides failed: %v", err) - } - if _, err := session.RPC.Tools.InitializeAndValidate(t.Context()); err != nil { - t.Fatalf("Tools.InitializeAndValidate failed: %v", err) - } - snapshot, err := session.RPC.Metadata.Snapshot(t.Context()) - if err != nil { - t.Fatalf("Metadata.Snapshot after options update failed: %v", err) - } - assertRPCPathEqual(t, optionsDirectory, snapshot.WorkingDirectory) - }) - - t.Run("should set reasoning effort and auto name", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-5", - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - reasoning, err := session.RPC.Model.SetReasoningEffort(t.Context(), &rpc.ModelSetReasoningEffortRequest{ReasoningEffort: "high"}) - if err != nil { - t.Fatalf("Model.SetReasoningEffort failed: %v", err) - } - if reasoning.ReasoningEffort != "high" { - t.Fatalf("Expected reasoning effort high, got %+v", reasoning) - } - current, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Model.GetCurrent failed: %v", err) - } - if current.ModelID == nil || *current.ModelID != "claude-sonnet-5" || - current.ReasoningEffort == nil || *current.ReasoningEffort != "high" { - t.Fatalf("Expected current model claude-sonnet-5/high, got %+v", current) - } - - autoName := "Auto Session " + randomHex(t) - awaitAutoTitle := waitForMatchingEvent( - session, - copilot.SessionEventTypeSessionTitleChanged, - func(event copilot.SessionEvent) bool { - data, ok := event.Data.(*copilot.SessionTitleChangedData) - return ok && data.Title == autoName - }, - "session.title_changed after name.setAuto", - ) - autoResult, err := session.RPC.Name.SetAuto(t.Context(), &rpc.NameSetAutoRequest{Summary: " " + autoName + " "}) - if err != nil { - t.Fatalf("Name.SetAuto failed: %v", err) - } - if !autoResult.Applied { - t.Fatalf("Expected first Name.SetAuto to apply, got %+v", autoResult) - } - awaitEvent(t, awaitAutoTitle) - name, err := session.RPC.Name.Get(t.Context()) - if err != nil { - t.Fatalf("Name.Get failed: %v", err) - } - if name.Name == nil || *name.Name != autoName { - t.Fatalf("Expected auto name %q, got %+v", autoName, name) - } - - explicitName := "Explicit Session " + randomHex(t) - if _, err := session.RPC.Name.Set(t.Context(), &rpc.NameSetRequest{Name: explicitName}); err != nil { - t.Fatalf("Name.Set explicit failed: %v", err) - } - ignoredAuto, err := session.RPC.Name.SetAuto(t.Context(), &rpc.NameSetAutoRequest{Summary: "Ignored " + randomHex(t)}) - if err != nil { - t.Fatalf("Name.SetAuto after explicit name failed: %v", err) - } - if ignoredAuto.Applied { - t.Fatal("Expected SetAuto to be ignored after explicit name") - } - name, err = session.RPC.Name.Get(t.Context()) - if err != nil { - t.Fatalf("Name.Get explicit failed: %v", err) - } - if name.Name == nil || *name.Name != explicitName { - t.Fatalf("Expected explicit name %q to remain, got %+v", explicitName, name) - } - }) - - t.Run("should set auth credentials", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - login := "sdk-rpc-" + randomHex(t) - - api := ctx.ProxyURL - telemetry := "https://localhost:1/telemetry" - setCredentials, err := session.RPC.GitHubAuth.SetCredentials(t.Context(), &rpc.SessionSetCredentialsParams{ - Credentials: &rpc.UserAuthInfo{ - CopilotUser: &rpc.CopilotUserResponse{ - AnalyticsTrackingID: rpcPtr("rpc-session-state-tracking-id"), - ChatEnabled: rpcPtr(true), - CopilotPlan: rpcPtr("individual_pro"), - Endpoints: &rpc.CopilotUserResponseEndpoints{ - API: &api, - Telemetry: &telemetry, - }, - Login: &login, - }, - Host: "https://github.com", - Login: login, - }, - }) - if err != nil { - t.Fatalf("Auth.SetCredentials failed: %v", err) - } - if !setCredentials.Success { - t.Fatalf("Expected Auth.SetCredentials Success=true, got %+v", setCredentials) - } - - status, err := session.RPC.GitHubAuth.GetStatus(t.Context()) - if err != nil { - t.Fatalf("Auth.GetStatus failed: %v", err) - } - if !status.IsAuthenticated || status.AuthType == nil || *status.AuthType != rpc.AuthInfoTypeUser || - status.Host == nil || *status.Host != "https://github.com" || - status.Login == nil || *status.Login != login { - t.Fatalf("Unexpected auth status after SetCredentials: %+v", status) - } - }) - - t.Run("should report idle processing and context token shapes", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - processing, err := session.RPC.Metadata.IsProcessing(t.Context()) - if err != nil { - t.Fatalf("Metadata.IsProcessing failed: %v", err) - } - if processing.Processing { - t.Fatal("Expected fresh session to be idle") - } - - model := "claude-sonnet-5" - contextInfo, err := session.RPC.Metadata.ContextInfo(t.Context(), &rpc.MetadataContextInfoRequest{ - PromptTokenLimit: 128000, - OutputTokenLimit: 4096, - SelectedModel: &model, - }) - if err != nil { - t.Fatalf("Metadata.ContextInfo failed: %v", err) - } - if contextInfo.ContextInfo != nil { - info := contextInfo.ContextInfo - if info.ModelName != model || info.PromptTokenLimit != 128000 || info.TotalTokens < 0 || - info.SystemTokens < 0 || info.ConversationTokens < 0 || info.ToolDefinitionsTokens < 0 { - t.Fatalf("Unexpected context info: %+v", info) - } - } - - recomputed, err := session.RPC.Metadata.RecomputeContextTokens(t.Context(), &rpc.MetadataRecomputeContextTokensRequest{ModelID: model}) - if err != nil { - t.Fatalf("Metadata.RecomputeContextTokens failed: %v", err) - } - if recomputed.SystemTokenCount < 0 || recomputed.MessagesTokenCount < 0 || - recomputed.TotalTokens != recomputed.SystemTokenCount+recomputed.MessagesTokenCount { - t.Fatalf("Unexpected recomputed context tokens: %+v", recomputed) - } - }) - - t.Run("should fork session with persisted messages", func(t *testing.T) { - ctx.ConfigureForTest(t) - - const sourcePrompt = "Say FORK_SOURCE_ALPHA exactly." - const forkPrompt = "Now say FORK_CHILD_BETA exactly." - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - initialAnswer, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: sourcePrompt}) - if err != nil { - t.Fatalf("Failed to send sourcePrompt: %v", err) - } - if assistant, ok := initialAnswer.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(assistant.Content, "FORK_SOURCE_ALPHA") { - t.Errorf("Expected initial answer to contain FORK_SOURCE_ALPHA, got %v", initialAnswer.Data) - } - - sourceMessages, err := session.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read source messages: %v", err) - } - sourceConversation := conversationMessages(sourceMessages) - if !containsConversation(sourceConversation, "user", sourcePrompt, false) { - t.Errorf("Expected source conversation to contain user message %q, got %v", sourcePrompt, sourceConversation) - } - if !containsConversation(sourceConversation, "assistant", "FORK_SOURCE_ALPHA", true) { - t.Errorf("Expected source conversation to contain assistant text 'FORK_SOURCE_ALPHA', got %v", sourceConversation) - } - - fork, err := client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{SessionID: session.SessionID}) - if err != nil { - t.Fatalf("Failed to fork session: %v", err) - } - if strings.TrimSpace(fork.SessionID) == "" { - t.Fatal("Expected non-empty fork session id") - } - if fork.SessionID == session.SessionID { - t.Errorf("Expected fork session id to differ from source %q", session.SessionID) - } - - forkedSession, err := client.ResumeSession(t.Context(), fork.SessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to resume forked session: %v", err) - } - - forkedMessages, err := forkedSession.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read forked messages: %v", err) - } - forkedConversation := conversationMessages(forkedMessages) - if len(forkedConversation) < len(sourceConversation) { - t.Fatalf("Expected forked conversation to include source conversation, got source=%v fork=%v", sourceConversation, forkedConversation) - } - for i := range sourceConversation { - if forkedConversation[i] != sourceConversation[i] { - t.Errorf("Forked conversation diverges at index %d: got %+v, expected %+v", i, forkedConversation[i], sourceConversation[i]) - } - } - - forkAnswer, err := forkedSession.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: forkPrompt}) - if err != nil { - t.Fatalf("Failed to send forkPrompt to fork: %v", err) - } - if assistant, ok := forkAnswer.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(assistant.Content, "FORK_CHILD_BETA") { - t.Errorf("Expected forked answer to contain FORK_CHILD_BETA, got %v", forkAnswer.Data) - } - - sourceAfterFork, err := session.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read source messages after fork: %v", err) - } - for _, m := range conversationMessages(sourceAfterFork) { - if m.content == forkPrompt { - t.Errorf("Source conversation should not contain fork prompt %q after fork", forkPrompt) - } - } - - forkAfterPrompt, err := forkedSession.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read forked messages after prompt: %v", err) - } - forkConv := conversationMessages(forkAfterPrompt) - if !containsConversation(forkConv, "user", forkPrompt, false) { - t.Errorf("Expected fork conversation to contain user prompt %q, got %v", forkPrompt, forkConv) - } - if !containsConversation(forkConv, "assistant", "FORK_CHILD_BETA", true) { - t.Errorf("Expected fork conversation to contain assistant text 'FORK_CHILD_BETA', got %v", forkConv) - } - - forkedSession.Disconnect() - }) - - t.Run("should handle forking session without persisted events", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - defer session.Disconnect() - - fork, err := client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{SessionID: session.SessionID}) - if err != nil { - errText := strings.ToLower(err.Error()) - if !strings.Contains(errText, "not found or has no persisted events") { - t.Errorf("Expected error mentioning 'not found or has no persisted events', got %v", err) - } - if strings.Contains(errText, "unhandled method sessions.fork") { - t.Errorf("sessions.fork should be implemented; error suggests it isn't: %v", err) - } - return - } - if fork == nil { - t.Fatal("Expected non-nil fork result") - return - } - if strings.TrimSpace(fork.SessionID) == "" { - t.Fatal("Expected non-empty fork session id") - } - if fork.SessionID == session.SessionID { - t.Errorf("Expected fork session id to differ from source %q", session.SessionID) - } - - forkedSession, err := client.ResumeSession(t.Context(), fork.SessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to resume forked session: %v", err) - } - defer forkedSession.Disconnect() - - forkedMessages, err := forkedSession.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read forked messages: %v", err) - } - if forkedConversation := conversationMessages(forkedMessages); len(forkedConversation) != 0 { - t.Errorf("Expected empty forked conversation, got %v", forkedConversation) - } - }) - - t.Run("should fork session to event id excluding boundary event", func(t *testing.T) { - ctx.ConfigureForTest(t) - - const firstPrompt = "Say FORK_BOUNDARY_FIRST exactly." - const secondPrompt = "Say FORK_BOUNDARY_SECOND exactly." - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - defer session.Disconnect() - - if _, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: firstPrompt}); err != nil { - t.Fatalf("Failed to send first prompt: %v", err) - } - if _, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: secondPrompt}); err != nil { - t.Fatalf("Failed to send second prompt: %v", err) - } - - sourceEvents, err := session.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read source messages: %v", err) - } - var secondUserEvent *copilot.SessionEvent - for i := range sourceEvents { - data, ok := sourceEvents[i].Data.(*copilot.UserMessageData) - if ok && data.Content == secondPrompt { - secondUserEvent = &sourceEvents[i] - break - } - } - if secondUserEvent == nil { - t.Fatal("Expected the second user.message in persisted history") - return - } - boundaryEventID := secondUserEvent.ID - - fork, err := client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{ - SessionID: session.SessionID, - ToEventID: &boundaryEventID, - }) - if err != nil { - t.Fatalf("Failed to fork session to event id: %v", err) - } - if strings.TrimSpace(fork.SessionID) == "" { - t.Fatal("Expected non-empty fork session id") - } - if fork.SessionID == session.SessionID { - t.Errorf("Expected fork session id to differ from source %q", session.SessionID) - } - - forkedSession, err := client.ResumeSession(t.Context(), fork.SessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to resume forked session: %v", err) - } - defer forkedSession.Disconnect() - - forkedEvents, err := forkedSession.GetEvents(t.Context()) - if err != nil { - t.Fatalf("Failed to read forked messages: %v", err) - } - for _, event := range forkedEvents { - if event.ID == boundaryEventID { - t.Fatalf("toEventId is exclusive; boundary event %q must not be in forked session", boundaryEventID) - } - } - forkedConversation := conversationMessages(forkedEvents) - if !containsConversation(forkedConversation, "user", firstPrompt, false) { - t.Errorf("Expected forked conversation to contain first prompt %q, got %v", firstPrompt, forkedConversation) - } - if containsConversation(forkedConversation, "user", secondPrompt, false) { - t.Errorf("Expected forked conversation to exclude second prompt %q, got %v", secondPrompt, forkedConversation) - } - }) - - t.Run("should report error when forking session to unknown event id", func(t *testing.T) { - ctx.ConfigureForTest(t) - - const sourcePrompt = "Say FORK_UNKNOWN_EVENT_OK exactly." - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - defer session.Disconnect() - - if _, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: sourcePrompt}); err != nil { - t.Fatalf("Failed to send source prompt: %v", err) - } - - bogusEventID := "00000000-0000-4000-8000-000000000000" - _, err = client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{ - SessionID: session.SessionID, - ToEventID: &bogusEventID, - }) - if err == nil { - t.Fatal("Expected sessions.fork to fail for unknown event id") - } - if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower("Event "+bogusEventID+" not found")) { - t.Errorf("Expected error mentioning unknown event %q, got %v", bogusEventID, err) - } - if strings.Contains(strings.ToLower(err.Error()), "unhandled method sessions.fork") { - t.Errorf("sessions.fork should be implemented; error suggests it isn't: %v", err) - } - }) - - t.Run("should call session usage and permission rpcs", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - metrics, err := session.RPC.Usage.GetMetrics(t.Context()) - if err != nil { - t.Fatalf("Failed to get usage metrics: %v", err) - } - if metrics.SessionStartTime.IsZero() { - t.Errorf("Expected non-zero sessionStartTime, got %s", metrics.SessionStartTime) - } - if metrics.TotalNanoAiu != nil && *metrics.TotalNanoAiu < 0 { - t.Errorf("Expected non-negative totalNanoAiu, got %f", *metrics.TotalNanoAiu) - } - for k, detail := range metrics.TokenDetails { - if detail.TokenCount < 0 { - t.Errorf("Expected non-negative tokenCount for %q, got %d", k, detail.TokenCount) - } - } - for modelName, modelMetric := range metrics.ModelMetrics { - if modelMetric.TotalNanoAiu != nil && *modelMetric.TotalNanoAiu < 0 { - t.Errorf("Expected non-negative totalNanoAiu for model %q, got %f", modelName, *modelMetric.TotalNanoAiu) - } - for tokenType, detail := range modelMetric.TokenDetails { - if detail.TokenCount < 0 { - t.Errorf("Expected non-negative tokenCount for model %q type %q, got %d", modelName, tokenType, detail.TokenCount) - } - } - } - - approve, err := session.RPC.Permissions.SetApproveAll(t.Context(), &rpc.PermissionsSetApproveAllRequest{Enabled: true}) - if err != nil { - t.Fatalf("Failed to call SetApproveAll(true): %v", err) - } - if !approve.Success { - t.Errorf("Expected SetApproveAll(true) to succeed, got %+v", approve) - } - - reset, err := session.RPC.Permissions.ResetSessionApprovals(t.Context(), &rpc.PermissionsResetSessionApprovalsRequest{}) - if err != nil { - t.Fatalf("Failed to call ResetSessionApprovals: %v", err) - } - if !reset.Success { - t.Errorf("Expected ResetSessionApprovals to succeed, got %+v", reset) - } - - // Restore. - if _, err := session.RPC.Permissions.SetApproveAll(t.Context(), &rpc.PermissionsSetApproveAllRequest{Enabled: false}); err != nil { - t.Errorf("Failed to restore SetApproveAll(false): %v", err) - } - }) - - t.Run("should report implemented errors for unsupported session rpc paths", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - _, err = session.RPC.History.Truncate(t.Context(), &rpc.HistoryTruncateRequest{EventID: "missing-event"}) - if err == nil { - t.Fatal("Expected History.Truncate with unknown event id to fail") - } - if strings.Contains(strings.ToLower(err.Error()), "unhandled method session.history.truncate") { - t.Errorf("session.history.truncate should be implemented; error suggests it isn't: %v", err) - } - - _, err = session.RPC.MCP.Oauth().Login(t.Context(), &rpc.MCPOauthLoginRequest{ServerName: "missing-server"}) - if err == nil { - t.Fatal("Expected MCP.Oauth.Login with unknown server to fail") - } - if strings.Contains(strings.ToLower(err.Error()), "unhandled method session.mcp.oauth.login") { - t.Errorf("session.mcp.oauth.login should be implemented; error suggests it isn't: %v", err) - } - }) - - t.Run("should compact session history after messages", func(t *testing.T) { - ctx.ConfigureForTest(t) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - if _, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "What is 2+2?"}); err != nil { - t.Fatalf("Failed to send message: %v", err) - } - - result, err := session.RPC.History.Compact(t.Context()) - if err != nil { - t.Fatalf("Failed to compact session: %v", err) - } - if result == nil { - t.Fatal("Expected non-nil compaction result") - } - }) -} - -type roleContent struct { - role string - content string -} - -func conversationMessages(events []copilot.SessionEvent) []roleContent { - var msgs []roleContent - for _, evt := range events { - switch d := evt.Data.(type) { - case *copilot.UserMessageData: - msgs = append(msgs, roleContent{role: "user", content: d.Content}) - case *copilot.AssistantMessageData: - msgs = append(msgs, roleContent{role: "assistant", content: d.Content}) - } - } - return msgs -} - -func containsConversation(msgs []roleContent, role, contentNeedle string, contains bool) bool { - for _, m := range msgs { - if m.role != role { - continue - } - if contains { - if strings.Contains(m.content, contentNeedle) { - return true - } - } else if m.content == contentNeedle { - return true - } - } - return false -} diff --git a/go/internal/e2e/rpc_session_state_extras_e2e_test.go b/go/internal/e2e/rpc_session_state_extras_e2e_test.go deleted file mode 100644 index c74ed45cc8..0000000000 --- a/go/internal/e2e/rpc_session_state_extras_e2e_test.go +++ /dev/null @@ -1,351 +0,0 @@ -package e2e - -import ( - "encoding/json" - "strings" - "testing" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestRpcSessionStateExtras(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should_list_models_for_session", func(t *testing.T) { - ctx.ConfigureForTest(t) - const token = "rpc-session-model-list-token" - registerProxyUser(t, ctx, token, "rpc-session-extras-user", nil) - authClient := newAuthenticatedClient(ctx, token) - defer authClient.ForceStop() - - session := createPortedSession(t, authClient, &copilot.SessionConfig{Model: "claude-sonnet-5"}) - defer session.Disconnect() - - result, err := session.RPC.Model.List(t.Context()) - if err != nil { - t.Fatalf("Model.List failed: %v", err) - } - if result.List == nil { - t.Fatal("Expected non-nil model list") - } - if len(result.List) == 0 { - t.Fatal("Expected non-empty model list") - } - found := false - for _, model := range result.List { - data, err := json.Marshal(model) - if err == nil && strings.Contains(string(data), "claude-sonnet-5") { - found = true - break - } - } - if !found { - t.Fatalf("Expected model list to include claude-sonnet-5, got %+v", result.List) - } - }) - - t.Run("should_report_session_activity_when_idle", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - activity, err := session.RPC.Metadata.Activity(t.Context()) - if err != nil { - t.Fatalf("Metadata.Activity failed: %v", err) - } - if activity.HasActiveWork { - t.Fatal("Expected a fresh session to report no active work") - } - if activity.Abortable { - t.Fatal("Expected a fresh session to have nothing abortable") - } - }) - - t.Run("should_get_and_set_allowall_permissions", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - defer func() { - _, _ = session.RPC.Permissions.SetMode(t.Context(), &rpc.PermissionsSetModeRequest{Mode: rpc.PermissionModeManual}) - }() - - initial, err := session.RPC.Permissions.GetMode(t.Context()) - if err != nil { - t.Fatalf("Permissions.GetMode initial failed: %v", err) - } - if initial.Mode != rpc.PermissionModeManual { - t.Fatalf("Expected manual mode on a fresh session, got %q", initial.Mode) - } - - enable, err := session.RPC.Permissions.SetMode(t.Context(), &rpc.PermissionsSetModeRequest{Mode: rpc.PermissionModeAllowAll}) - if err != nil { - t.Fatalf("Permissions.SetMode(allow-all) failed: %v", err) - } - if !enable.Success || enable.Mode != rpc.PermissionModeAllowAll { - t.Fatalf("Expected successful allow-all mode change, got %+v", enable) - } - afterEnable, err := session.RPC.Permissions.GetMode(t.Context()) - if err != nil { - t.Fatalf("Permissions.GetMode after allow-all failed: %v", err) - } - if afterEnable.Mode != rpc.PermissionModeAllowAll { - t.Fatalf("Expected allow-all mode, got %q", afterEnable.Mode) - } - - disable, err := session.RPC.Permissions.SetMode(t.Context(), &rpc.PermissionsSetModeRequest{Mode: rpc.PermissionModeManual}) - if err != nil { - t.Fatalf("Permissions.SetMode(manual) failed: %v", err) - } - if !disable.Success || disable.Mode != rpc.PermissionModeManual { - t.Fatalf("Expected successful manual mode change, got %+v", disable) - } - afterDisable, err := session.RPC.Permissions.GetMode(t.Context()) - if err != nil { - t.Fatalf("Permissions.GetMode after manual failed: %v", err) - } - if afterDisable.Mode != rpc.PermissionModeManual { - t.Fatalf("Expected manual mode, got %q", afterDisable.Mode) - } - }) - - t.Run("should_read_empty_sql_todos_for_fresh_session", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - result, err := session.RPC.Plan.ReadSqlTodos(t.Context()) - if err != nil { - t.Fatalf("Plan.ReadSqlTodos failed: %v", err) - } - if result.Rows == nil { - t.Fatal("Expected non-nil SQL todo rows") - } - if len(result.Rows) != 0 { - t.Fatalf("Expected empty SQL todo rows, got %+v", result.Rows) - } - }) - - t.Run("should_get_telemetry_engagement_id", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - result, err := session.RPC.Telemetry.GetEngagementId(t.Context()) - if err != nil { - t.Fatalf("Telemetry.GetEngagementId failed: %v", err) - } - if result == nil { - t.Fatal("Expected non-nil telemetry engagement result") - } - }) - - t.Run("should_get_current_tool_metadata_after_initialization", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - answer, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "What is 2+2?"}) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - if answer == nil { - t.Fatal("Expected a final assistant message") - } - - result, err := session.RPC.Tools.GetCurrentMetadata(t.Context()) - if err != nil { - t.Fatalf("Tools.GetCurrentMetadata failed: %v", err) - } - if result.Tools == nil { - t.Fatal("Expected non-nil current tool metadata") - } - if len(result.Tools) == 0 { - t.Fatal("Expected non-empty current tool metadata") - } - for _, tool := range result.Tools { - if strings.TrimSpace(tool.Name) == "" { - t.Fatalf("Expected non-empty tool name, got %+v", tool) - } - if strings.TrimSpace(tool.Description) == "" { - t.Fatalf("Expected non-empty tool description, got %+v", tool) - } - } - }) - - t.Run("should_add_byok_provider_and_model_at_runtime", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - apiKey := "provider-key" - providerType := rpc.ProviderConfigTypeOpenai - wireAPI := rpc.ProviderConfigWireAPICompletions - modelName := "Go Added Model" - maxPromptTokens := float64(4096) - result, err := session.RPC.Provider.Add(t.Context(), &rpc.ProviderAddRequest{ - Providers: []rpc.NamedProviderConfig{{ - Name: "go-e2e-provider", - Type: &providerType, - BaseURL: "https://models.example.test/v1", - APIKey: &apiKey, - Headers: map[string]string{"x-provider": "go"}, - WireAPI: &wireAPI, - }}, - Models: []rpc.ProviderModelConfig{{ - ID: "small", - Provider: "go-e2e-provider", - Name: &modelName, - MaxPromptTokens: &maxPromptTokens, - }}, - }) - if err != nil { - t.Fatalf("Provider.Add failed: %v", err) - } - if len(result.Models) != 1 { - t.Fatalf("Expected one added provider model, got %+v", result.Models) - } - - selectionID := "go-e2e-provider/small" - if _, err := session.RPC.Model.SwitchTo(t.Context(), &rpc.ModelSwitchToRequest{ModelID: selectionID}); err != nil { - t.Fatalf("Model.SwitchTo added model failed: %v", err) - } - current, err := session.RPC.Model.GetCurrent(t.Context()) - if err != nil { - t.Fatalf("Model.GetCurrent after provider add failed: %v", err) - } - if current.ModelID == nil || *current.ModelID != selectionID { - t.Fatalf("Expected current model %q, got %+v", selectionID, current) - } - }) - - t.Run("should_return_empty_completions_when_host_does_not_provide_them", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - result, err := session.RPC.Completions.Request(t.Context(), &rpc.CompletionsRequestRequest{ - Text: "Use @ to mention context", - Offset: 5, - }) - if err != nil { - t.Fatalf("Completions.Request failed: %v", err) - } - if result.Items == nil { - t.Fatal("Expected non-nil completion items list") - } - }) - - t.Run("should_report_visibility_as_unsynced_for_local_session", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - status := rpc.SessionVisibilityStatusUnshared - set, err := session.RPC.Visibility.Set(t.Context(), &rpc.VisibilitySetRequest{Status: status}) - if err != nil { - t.Fatalf("Visibility.Set failed: %v", err) - } - if set.Synced || set.Status != nil || set.ShareURL != nil { - t.Fatalf("Expected unsynced visibility set result, got %+v", set) - } - get, err := session.RPC.Visibility.Get(t.Context()) - if err != nil { - t.Fatalf("Visibility.Get failed: %v", err) - } - if get.Synced || get.Status != nil || get.ShareURL != nil { - t.Fatalf("Expected unsynced visibility get result, got %+v", get) - } - }) - - t.Run("should_get_context_attribution_and_heaviest_messages_after_turn", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - answer, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Say CONTEXT_METADATA_OK exactly."}) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) - } - if answer == nil { - t.Fatal("Expected final assistant message") - } - - attribution, err := session.RPC.Metadata.GetContextAttribution(t.Context()) - if err != nil { - t.Fatalf("Metadata.GetContextAttribution failed: %v", err) - } - if attribution == nil { - t.Fatal("Expected attribution result") - } - limit := int64(5) - heaviest, err := session.RPC.Metadata.GetContextHeaviestMessages(t.Context(), &rpc.MetadataContextHeaviestMessagesRequest{Limit: &limit}) - if err != nil { - t.Fatalf("Metadata.GetContextHeaviestMessages failed: %v", err) - } - if heaviest.Messages == nil { - t.Fatal("Expected non-nil heaviest messages list") - } - }) - - t.Run("should_update_and_clear_live_subagent_settings", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - contextTier := rpc.SubagentSettingsEntryContextTierLongContext - model := "gpt-5-mini" - reasoningEffort := "low" - update, err := session.RPC.Tools.UpdateSubagentSettings(t.Context(), &rpc.UpdateSubagentSettingsRequest{ - Subagents: &rpc.SubagentSettings{ - DisabledSubagents: []string{"legacy-agent"}, - Agents: map[string]rpc.SubagentSettingsEntry{ - "general-purpose": { - ContextTier: &contextTier, - Model: &model, - EffortLevel: &reasoningEffort, - }, - }, - }, - }) - if err != nil { - t.Fatalf("Tools.UpdateSubagentSettings failed: %v", err) - } - if update == nil { - t.Fatal("Expected update result") - } - - clear, err := session.RPC.Tools.UpdateSubagentSettings(t.Context(), &rpc.UpdateSubagentSettingsRequest{}) - if err != nil { - t.Fatalf("Tools.UpdateSubagentSettings clear failed: %v", err) - } - if clear == nil { - t.Fatal("Expected clear result") - } - }) - - t.Run("should_reload_session_plugins", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - if _, err := session.RPC.Plugins.Reload(t.Context()); err != nil { - t.Fatalf("Plugins.Reload failed: %v", err) - } - plugins, err := session.RPC.Plugins.List(t.Context()) - if err != nil { - t.Fatalf("Plugins.List failed: %v", err) - } - if plugins.Plugins == nil { - t.Fatal("Expected non-nil session plugin list") - } - for _, plugin := range plugins.Plugins { - if strings.TrimSpace(plugin.Name) == "" { - t.Fatalf("Expected non-empty plugin name, got %+v", plugin) - } - } - }) -} diff --git a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go b/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go deleted file mode 100644 index a426f6bdc9..0000000000 --- a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package e2e - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/RpcShellAndFleetTests.cs (snapshot category "rpc_shell_and_fleet"). -func TestRPCShellAndFleetE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should execute shell command", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - commandDir := filepath.Join(ctx.WorkDir, "shell-rpc-"+randomHex(t)) - if err := os.Mkdir(commandDir, 0755); err != nil { - t.Fatalf("Failed to create shell command directory: %v", err) - } - markerPath := filepath.Join(commandDir, "marker.txt") - const marker = "copilot-sdk-shell-rpc" - - cwd := commandDir - result, err := session.RPC.Shell.Exec(t.Context(), &rpc.ShellExecRequest{ - Command: writeFileCommand(filepath.Base(markerPath), marker), - Cwd: &cwd, - }) - if err != nil { - t.Fatalf("Failed to call session.shell.exec: %v", err) - } - if strings.TrimSpace(result.ProcessID) == "" { - t.Fatal("Expected non-empty processId from shell.exec") - } - - waitForFileText(t, markerPath, marker) - }) - - t.Run("should kill shell process", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - t.Cleanup(func() { _ = session.Disconnect() }) - - var command string - if runtime.GOOS == "windows" { - 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. - cwd := os.TempDir() - exec, err := session.RPC.Shell.Exec(t.Context(), &rpc.ShellExecRequest{Command: command, Cwd: &cwd}) - if err != nil { - t.Fatalf("Failed to call session.shell.exec: %v", err) - } - if strings.TrimSpace(exec.ProcessID) == "" { - t.Fatal("Expected non-empty processId from shell.exec") - } - - kill, err := session.RPC.Shell.Kill(t.Context(), &rpc.ShellKillRequest{ProcessID: exec.ProcessID}) - if err != nil { - t.Fatalf("Failed to call session.shell.kill: %v", err) - } - if !kill.Killed { - t.Errorf("Expected shell.kill to report Killed=true, got %+v", kill) - } - }) - - t.Run("should start fleet and complete custom tool task", func(t *testing.T) { - ctx.ConfigureForTest(t) - - markerPath := filepath.Join(ctx.WorkDir, "fleet-rpc-"+randomHex(t)+".txt") - const marker = "copilot-sdk-fleet-rpc" - const toolName = "record_fleet_completion" - - type RecordParams struct { - Content string `json:"content" jsonschema:"Content to record"` - } - recordTool := copilot.DefineTool(toolName, "Records completion of the fleet validation task.", - func(params RecordParams, inv copilot.ToolInvocation) (string, error) { - if err := os.WriteFile(markerPath, []byte(params.Content), 0644); err != nil { - return "", err - } - return params.Content, nil - }) - - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Tools: []copilot.Tool{recordTool}, - }) - if err != nil { - t.Fatalf("Failed to create session: %v", err) - } - - prompt := fmt.Sprintf("Use the %s tool with content '%s', then report that the fleet task is complete.", toolName, marker) - promptCopy := prompt - - fleet, err := session.RPC.Fleet.Start(t.Context(), &rpc.FleetStartRequest{Prompt: &promptCopy}) - if err != nil { - t.Fatalf("Failed to call session.fleet.start: %v", err) - } - if !fleet.Started { - t.Fatal("Expected fleet.start to report Started=true") - } - - waitForFileText(t, markerPath, marker) - - // Fleet-mode tasks do not emit SessionIdleEvent; poll session messages until the - // assistant reply contains the expected text. - messages := waitForFleetCompletion(t, session, "fleet task") - - var sawUser, sawAssistant bool - var sawToolStart, sawToolComplete bool - for _, evt := range messages { - switch d := evt.Data.(type) { - case *copilot.UserMessageData: - if strings.Contains(d.Content, prompt) { - sawUser = true - } - case *copilot.AssistantMessageData: - if strings.Contains(strings.ToLower(d.Content), "fleet task") { - sawAssistant = true - } - case *copilot.ToolExecutionStartData: - if d.ToolName == toolName { - sawToolStart = true - } - case *copilot.ToolExecutionCompleteData: - if d.Success && d.Result != nil && strings.Contains(d.Result.Content, marker) { - sawToolComplete = true - } - } - } - - if !sawUser { - t.Errorf("Expected user message containing original prompt; messages: %d", len(messages)) - } - if !sawAssistant { - t.Errorf("Expected assistant message containing 'fleet task'") - } - if !sawToolStart { - t.Errorf("Expected ToolExecutionStart for %q", toolName) - } - if !sawToolComplete { - t.Errorf("Expected successful ToolExecutionComplete with content containing %q", marker) - } - }) -} - -func randomHex(t *testing.T) string { - t.Helper() - var buf [8]byte - if _, err := rand.Read(buf[:]); err != nil { - t.Fatalf("Failed to generate random bytes: %v", err) - } - return hex.EncodeToString(buf[:]) -} - -func writeFileCommand(markerName, marker string) string { - if runtime.GOOS == "windows" { - return fmt.Sprintf("echo %s>\"%s\"", marker, markerName) - } - return fmt.Sprintf("sh -c \"printf '%%s' '%s' > '%s'\"", marker, markerName) -} - -func waitForFileText(t *testing.T, path, expected string) { - t.Helper() - deadline := time.Now().Add(30 * time.Second) - for time.Now().Before(deadline) { - if data, err := os.ReadFile(path); err == nil && strings.Contains(string(data), expected) { - return - } - time.Sleep(100 * time.Millisecond) - } - t.Fatalf("Timed out waiting for shell command to write %q to %q", expected, path) -} - -func waitForFleetCompletion(t *testing.T, session *copilot.Session, contentNeedle string) []copilot.SessionEvent { - t.Helper() - deadline := time.Now().Add(120 * time.Second) - for time.Now().Before(deadline) { - messages, err := session.GetEvents(t.Context()) - if err == nil { - for _, evt := range messages { - if d, ok := evt.Data.(*copilot.AssistantMessageData); ok && strings.Contains(strings.ToLower(d.Content), contentNeedle) { - return messages - } - } - } - time.Sleep(250 * time.Millisecond) - } - t.Fatal("Timed out waiting for fleet-mode assistant reply") - return nil -} diff --git a/go/internal/e2e/rpc_shell_user_requested_e2e_test.go b/go/internal/e2e/rpc_shell_user_requested_e2e_test.go deleted file mode 100644 index 0c388a3c17..0000000000 --- a/go/internal/e2e/rpc_shell_user_requested_e2e_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package e2e - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestRpcShellUserRequested(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should_execute_user_requested_shell_command", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - marker := "copilotusershell" + randomHex(t) - requestID := "req-" + randomHex(t) - - result, err := session.RPC.Shell.ExecuteUserRequested(t.Context(), &rpc.ShellExecuteUserRequestedRequest{ - RequestID: requestID, - Command: "echo " + marker, - }) - if err != nil { - t.Fatalf("Shell.ExecuteUserRequested failed: %v", err) - } - if !result.Success { - t.Fatalf("Expected shell command to succeed, got error %v", result.Error) - } - if result.ExitCode == nil || *result.ExitCode != 0 { - t.Fatalf("Expected exit code 0, got %v", result.ExitCode) - } - if !strings.Contains(result.Output, marker) { - t.Fatalf("Expected output to contain %q, got %q", marker, result.Output) - } - if strings.TrimSpace(result.ToolCallID) == "" { - t.Fatal("Expected non-empty tool call ID") - } - }) - - t.Run("should_cancel_user_requested_shell_command", func(t *testing.T) { - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - missing, err := session.RPC.Shell.CancelUserRequested(t.Context(), &rpc.ShellCancelUserRequestedRequest{RequestID: "missing-" + randomHex(t)}) - if err != nil { - t.Fatalf("Shell.CancelUserRequested(missing) failed: %v", err) - } - if missing.Cancelled { - t.Fatal("Expected cancelling an unknown request to return Cancelled=false") - } - - requestID := "req-" + randomHex(t) - markerPath := filepath.Join(os.TempDir(), "shell-cancel-"+randomHex(t)+".txt") - defer tryRemovePortedFile(markerPath) - - type executeResult struct { - result *rpc.UserRequestedShellCommandResult - err error - } - executeCh := make(chan executeResult, 1) - execDone := false - go func() { - result, err := session.RPC.Shell.ExecuteUserRequested(t.Context(), &rpc.ShellExecuteUserRequestedRequest{ - RequestID: requestID, - Command: createPortedMarkerThenSleepCommand(markerPath, 60), - }) - executeCh <- executeResult{result: result, err: err} - }() - defer func() { - if execDone { - return - } - _, _ = session.RPC.Shell.CancelUserRequested(t.Context(), &rpc.ShellCancelUserRequestedRequest{RequestID: requestID}) - select { - case <-executeCh: - case <-time.After(30 * time.Second): - } - }() - - waitForRPCCondition(t, 30*time.Second, "user-requested shell marker file", func() (bool, error) { - _, err := os.Stat(markerPath) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err - }) - - waitForRPCCondition(t, 15*time.Second, "user-requested shell command to become cancellable", func() (bool, error) { - cancel, err := session.RPC.Shell.CancelUserRequested(t.Context(), &rpc.ShellCancelUserRequestedRequest{RequestID: requestID}) - if err != nil { - return false, err - } - return cancel.Cancelled, nil - }) - - select { - case execution := <-executeCh: - execDone = true - if execution.err != nil { - t.Fatalf("ExecuteUserRequested returned error after cancellation: %v", execution.err) - } - if execution.result == nil { - t.Fatal("Expected execution result after cancellation") - } - if execution.result.Success { - t.Fatalf("Expected cancelled execution to be unsuccessful, got %+v", execution.result) - } - case <-time.After(30 * time.Second): - t.Fatal("Timed out waiting for cancelled user-requested shell command to finish") - } - }) -} - -func createPortedMarkerThenSleepCommand(markerPath string, seconds int) string { - if runtime.GOOS == "windows" { - escaped := strings.ReplaceAll(markerPath, "'", "''") - return fmt.Sprintf("Set-Content -LiteralPath '%s' -Value 'running'; Start-Sleep -Seconds %d", escaped, seconds) - } - escaped := strings.ReplaceAll(markerPath, "'", "'\\''") - return fmt.Sprintf("echo running > '%s'; sleep %d", escaped, seconds) -} - -func tryRemovePortedFile(path string) { - if err := os.Remove(path); err != nil && !os.IsNotExist(err) { - _ = err - } -} diff --git a/go/internal/e2e/rpc_smoke_e2e_test.go b/go/internal/e2e/rpc_smoke_e2e_test.go new file mode 100644 index 0000000000..a89dac2623 --- /dev/null +++ b/go/internal/e2e/rpc_smoke_e2e_test.go @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package e2e + +import ( + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +func TestGeneratedRPCRoundTrip(t *testing.T) { + client := testharness.NewTestContext(t).NewClient() + t.Cleanup(client.ForceStop) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + + result, err := client.RPC.Ping(t.Context(), &rpc.PingRequest{Message: copilot.String("typed rpc test")}) + if err != nil { + t.Fatalf("RPC.Ping failed: %v", err) + } + if result.Message != "pong: typed rpc test" || result.Timestamp.IsZero() { + t.Fatalf("RPC.Ping returned %#v", result) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + mode, err := session.RPC.Mode.Get(t.Context()) + if err != nil { + t.Fatalf("RPC.Mode.Get failed: %v", err) + } + if mode == nil || *mode != rpc.SessionModeInteractive { + t.Fatalf("RPC.Mode.Get returned %v, want interactive", mode) + } +} diff --git a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go b/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go deleted file mode 100644 index 648855e5a3..0000000000 --- a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go +++ /dev/null @@ -1,444 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - "time" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/RpcTasksAndHandlersTests.cs (snapshot category "rpc_tasks_and_handlers"). -func TestRPCTasksAndHandlersE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should list task state and return false for missing task operations", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - tasks, err := session.RPC.Tasks.List(t.Context()) - if err != nil { - t.Fatalf("Tasks.List failed: %v", err) - } - if tasks.Tasks == nil { - t.Error("Expected non-nil Tasks list") - } - if len(tasks.Tasks) != 0 { - t.Errorf("Expected empty Tasks list, got %d tasks", len(tasks.Tasks)) - } - - if _, err := session.RPC.Tasks.Refresh(t.Context()); err != nil { - t.Fatalf("Tasks.Refresh failed: %v", err) - } - if _, err := session.RPC.Tasks.WaitForPending(t.Context()); err != nil { - t.Fatalf("Tasks.WaitForPending failed: %v", err) - } - - progress, err := session.RPC.Tasks.GetProgress(t.Context(), &rpc.TasksGetProgressRequest{ID: "missing-task"}) - if err != nil { - t.Fatalf("Tasks.GetProgress failed: %v", err) - } - if progress.Progress != nil { - t.Errorf("Expected nil Progress for missing task, got %+v", progress.Progress) - } - - current, err := session.RPC.Tasks.GetCurrentPromotable(t.Context()) - if err != nil { - t.Fatalf("Tasks.GetCurrentPromotable failed: %v", err) - } - if current.Task != nil { - t.Errorf("Expected nil current promotable task, got %+v", current.Task) - } - - promote, err := session.RPC.Tasks.PromoteToBackground(t.Context(), &rpc.TasksPromoteToBackgroundRequest{ID: "missing-task"}) - if err != nil { - t.Fatalf("PromoteToBackground failed: %v", err) - } - if promote.Promoted { - t.Error("Expected Promoted=false for missing task") - } - - promoteCurrent, err := session.RPC.Tasks.PromoteCurrentToBackground(t.Context()) - if err != nil { - t.Fatalf("Tasks.PromoteCurrentToBackground failed: %v", err) - } - if promoteCurrent.Task != nil { - t.Errorf("Expected nil task from PromoteCurrentToBackground, got %+v", promoteCurrent.Task) - } - - cancel, err := session.RPC.Tasks.Cancel(t.Context(), &rpc.TasksCancelRequest{ID: "missing-task"}) - if err != nil { - t.Fatalf("Cancel failed: %v", err) - } - if cancel.Cancelled { - t.Error("Expected Cancelled=false for missing task") - } - - remove, err := session.RPC.Tasks.Remove(t.Context(), &rpc.TasksRemoveRequest{ID: "missing-task"}) - if err != nil { - t.Fatalf("Remove failed: %v", err) - } - if remove.Removed { - t.Error("Expected Removed=false for missing task") - } - - sendMessage, err := session.RPC.Tasks.SendMessage(t.Context(), &rpc.TasksSendMessageRequest{ - ID: "missing-task", - Message: "hello from the Go SDK E2E test", - }) - if err != nil { - t.Fatalf("Tasks.SendMessage failed: %v", err) - } - if sendMessage.Sent { - t.Error("Expected Sent=false for missing task") - } - if sendMessage.Error == nil || strings.TrimSpace(*sendMessage.Error) == "" { - t.Errorf("Expected missing task SendMessage to return an error message, got %+v", sendMessage) - } - }) - - t.Run("should report implemented error for missing task agent type", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - _, err = session.RPC.Tasks.StartAgent(t.Context(), &rpc.TasksStartAgentRequest{ - AgentType: "missing-agent-type", - Prompt: "Say hi", - Name: "sdk-test-task", - }) - if err == nil { - t.Fatal("Expected an error for missing agent type") - } - if strings.Contains(strings.ToLower(err.Error()), "unhandled method session.tasks.startagent") { - t.Errorf("Expected an implemented error, but the method appears unhandled: %v", err) - } - }) - - t.Run("should report implemented error for invalid task agent model", func(t *testing.T) { - ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - description := "SDK task agent validation" - model := "not-a-real-model" - _, err = session.RPC.Tasks.StartAgent(t.Context(), &rpc.TasksStartAgentRequest{ - AgentType: "general-purpose", - Prompt: "Say hi", - Name: "sdk-test-task", - Description: &description, - Model: &model, - }) - if err == nil { - t.Fatal("Expected an error for invalid agent model") - } - if strings.Contains(strings.ToLower(err.Error()), "unhandled method session.tasks.startagent") { - t.Errorf("Expected an implemented error, but the method appears unhandled: %v", err) - } - - tasks, err := session.RPC.Tasks.List(t.Context()) - if err != nil { - t.Fatalf("Tasks.List failed: %v", err) - } - if len(tasks.Tasks) != 0 { - t.Fatalf("Expected no task to be created for invalid model, got %+v", tasks.Tasks) - } - }) - - t.Run("should return expected results for missing pending handler request ids", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - tool, err := session.RPC.Tools.HandlePendingToolCall(t.Context(), &rpc.HandlePendingToolCallRequest{ - RequestID: "missing-tool-request", - Result: rpc.ExternalToolStringResult("tool result"), - }) - if err != nil { - t.Fatalf("Tools.HandlePendingToolCall failed: %v", err) - } - if tool.Success { - t.Error("Expected Success=false for missing tool request id") - } - - commandErr := "command error" - command, err := session.RPC.Commands.HandlePendingCommand(t.Context(), &rpc.CommandsHandlePendingCommandRequest{ - RequestID: "missing-command-request", - Error: &commandErr, - }) - if err != nil { - t.Fatalf("Commands.HandlePendingCommand failed: %v", err) - } - // Per dotnet RpcTasksAndHandlersTests, missing command requests return Success=true. - if !command.Success { - t.Error("Expected Success=true for missing command request id") - } - - elicitation, err := session.RPC.UI.HandlePendingElicitation(t.Context(), &rpc.UIHandlePendingElicitationRequest{ - RequestID: "missing-elicitation-request", - Result: rpc.UIElicitationResponse{Action: rpc.UIElicitationResponseActionCancel}, - }) - if err != nil { - t.Fatalf("UI.HandlePendingElicitation failed: %v", err) - } - if elicitation.Success { - t.Error("Expected Success=false for missing elicitation request id") - } - - userInput, err := session.RPC.UI.HandlePendingUserInput(t.Context(), &rpc.UIHandlePendingUserInputRequest{ - RequestID: "missing-user-input-request", - Response: rpc.UIUserInputResponse{Answer: "typed answer", WasFreeform: true}, - }) - if err != nil { - t.Fatalf("UI.HandlePendingUserInput failed: %v", err) - } - if userInput.Success { - t.Error("Expected Success=false for missing user input request id") - } - - sampling, err := session.RPC.UI.HandlePendingSampling(t.Context(), &rpc.UIHandlePendingSamplingRequest{ - RequestID: "missing-sampling-request", - Response: &rpc.UIHandlePendingSamplingResponse{}, - }) - if err != nil { - t.Fatalf("UI.HandlePendingSampling failed: %v", err) - } - if sampling.Success { - t.Error("Expected Success=false for missing sampling request id") - } - - autoModeSwitch, err := session.RPC.UI.HandlePendingAutoModeSwitch(t.Context(), &rpc.UIHandlePendingAutoModeSwitchRequest{ - RequestID: "missing-auto-mode-switch-request", - Response: rpc.UIAutoModeSwitchResponseNo, - }) - if err != nil { - t.Fatalf("UI.HandlePendingAutoModeSwitch failed: %v", err) - } - if autoModeSwitch.Success { - t.Error("Expected Success=false for missing auto mode switch request id") - } - - feedback := "No pending plan approval" - selectedAction := rpc.UIExitPlanModeActionExitOnly - exitPlanMode, err := session.RPC.UI.HandlePendingExitPlanMode(t.Context(), &rpc.UIHandlePendingExitPlanModeRequest{ - RequestID: "missing-exit-plan-mode-request", - Response: rpc.UIExitPlanModeResponse{ - Approved: false, - Feedback: &feedback, - SelectedAction: &selectedAction, - }, - }) - if err != nil { - t.Fatalf("UI.HandlePendingExitPlanMode failed: %v", err) - } - if exitPlanMode.Success { - t.Error("Expected Success=false for missing exit plan mode request id") - } - - permissionFeedback := "not approved" - permission, err := session.RPC.Permissions.HandlePendingPermissionRequest(t.Context(), &rpc.PermissionDecisionRequest{ - RequestID: "missing-permission-request", - Result: &rpc.PermissionDecisionReject{Feedback: &permissionFeedback}, - }) - if err != nil { - t.Fatalf("Permissions.HandlePendingPermissionRequest (reject) failed: %v", err) - } - if permission.Success { - t.Error("Expected Success=false for missing permission request id") - } - - domain := "example.com" - permanent, err := session.RPC.Permissions.HandlePendingPermissionRequest(t.Context(), &rpc.PermissionDecisionRequest{ - RequestID: "missing-permanent-permission-request", - Result: &rpc.PermissionDecisionApprovePermanently{Domain: domain}, - }) - if err != nil { - t.Fatalf("Permissions.HandlePendingPermissionRequest (approve-permanently) failed: %v", err) - } - if permanent.Success { - t.Error("Expected Success=false for missing permanent permission request id") - } - - sessionApproval, err := session.RPC.Permissions.HandlePendingPermissionRequest(t.Context(), &rpc.PermissionDecisionRequest{ - RequestID: "missing-session-approval-request", - Result: &rpc.PermissionDecisionApproveForSession{ - Approval: &rpc.PermissionDecisionApproveForSessionApprovalCustomTool{ToolName: "missing-tool"}, - }, - }) - if err != nil { - t.Fatalf("Permissions.HandlePendingPermissionRequest (approve-for-session) failed: %v", err) - } - if sessionApproval.Success { - t.Error("Expected Success=false for missing session approval request id") - } - - locationApproval, err := session.RPC.Permissions.HandlePendingPermissionRequest(t.Context(), &rpc.PermissionDecisionRequest{ - RequestID: "missing-location-approval-request", - Result: &rpc.PermissionDecisionApproveForLocation{ - Approval: &rpc.PermissionDecisionApproveForLocationApprovalCustomTool{ToolName: "missing-tool"}, - LocationKey: "missing-location", - }, - }) - if err != nil { - t.Fatalf("Permissions.HandlePendingPermissionRequest (approve-for-location) failed: %v", err) - } - if locationApproval.Success { - t.Error("Expected Success=false for missing location approval request id") - } - - sessionLimits, err := session.RPC.UI.HandlePendingSessionLimitsExhausted(t.Context(), &rpc.UIHandlePendingSessionLimitsExhaustedRequest{ - RequestID: "missing-session-limits-request", - Response: rpc.UISessionLimitsExhaustedResponse{Action: rpc.UISessionLimitsExhaustedResponseActionCancel}, - }) - if err != nil { - t.Fatalf("UI.HandlePendingSessionLimitsExhausted failed: %v", err) - } - if sessionLimits.Success { - t.Error("Expected Success=false for missing session limits request id") - } - - headers, err := session.RPC.MCP.Headers().HandlePendingHeadersRefreshRequest(t.Context(), &rpc.MCPHeadersHandlePendingHeadersRefreshRequestRequest{ - RequestID: "missing-headers-refresh-request", - Result: rpc.MCPHeadersHandlePendingHeadersRefreshRequestHeaders{Headers: map[string]string{"authorization": "Bearer refreshed"}}, - }) - if err != nil { - t.Fatalf("MCP.Headers.HandlePendingHeadersRefreshRequest failed: %v", err) - } - if headers.Success { - t.Error("Expected Success=false for missing MCP headers refresh request id") - } - - noHeaders, err := session.RPC.MCP.Headers().HandlePendingHeadersRefreshRequest(t.Context(), &rpc.MCPHeadersHandlePendingHeadersRefreshRequestRequest{ - RequestID: "missing-headers-refresh-none-request", - Result: rpc.MCPHeadersHandlePendingHeadersRefreshRequestNone{}, - }) - if err != nil { - t.Fatalf("MCP.Headers.HandlePendingHeadersRefreshRequest none failed: %v", err) - } - if noHeaders.Success { - t.Error("Expected Success=false for missing MCP headers refresh none request id") - } - }) - - t.Run("should round trip rpc elicitation through config handler", func(t *testing.T) { - handlerContext := make(chan copilot.ElicitationContext, 1) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - OnElicitationRequest: func(ctx copilot.ElicitationContext) (copilot.ElicitationResult, error) { - handlerContext <- ctx - return copilot.ElicitationResult{ - Action: copilot.ElicitationActionAccept, - Content: map[string]any{ - "answer": "from handler", - "confirmed": true, - }, - }, nil - }, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - response, err := session.RPC.UI.Elicitation(t.Context(), &rpc.UIElicitationRequest{ - Message: "Need details", - RequestedSchema: rpc.UIElicitationSchema{ - Type: rpc.UIElicitationSchemaTypeObject, - Properties: map[string]rpc.UIElicitationSchemaProperty{ - "answer": &rpc.UIElicitationSchemaPropertyString{}, - "confirmed": &rpc.UIElicitationSchemaPropertyBoolean{}, - }, - Required: []string{"answer"}, - }, - }) - if err != nil { - t.Fatalf("UI.Elicitation failed: %v", err) - } - - var ctx copilot.ElicitationContext - select { - case ctx = <-handlerContext: - case <-time.After(30 * time.Second): - t.Fatal("Timed out waiting for elicitation handler") - } - if ctx.SessionID != session.SessionID || ctx.Message != "Need details" { - t.Fatalf("Unexpected elicitation context: %+v", ctx) - } - if ctx.RequestedSchema == nil || ctx.RequestedSchema.Properties == nil { - t.Fatalf("Expected requested schema to include properties, got %+v", ctx.RequestedSchema) - } - if response.Action != rpc.UIElicitationResponseActionAccept { - t.Fatalf("Expected accept response, got %+v", response) - } - if got, ok := response.Content["answer"].(rpc.UIElicitationStringValue); !ok || string(got) != "from handler" { - t.Fatalf("Expected answer content from handler, got %+v", response.Content["answer"]) - } - if got, ok := response.Content["confirmed"].(rpc.UIElicitationBooleanValue); !ok || !bool(got) { - t.Fatalf("Expected confirmed content true, got %+v", response.Content["confirmed"]) - } - }) - - t.Run("should register and unregister direct auto mode switch handler", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - - missing, err := session.RPC.UI.UnregisterDirectAutoModeSwitchHandler(t.Context(), &rpc.UIUnregisterDirectAutoModeSwitchHandlerRequest{ - Handle: "missing-direct-auto-mode-handle", - }) - if err != nil { - t.Fatalf("UI.UnregisterDirectAutoModeSwitchHandler(missing) failed: %v", err) - } - if missing.Unregistered { - t.Fatal("Expected missing direct handler unregister to return false") - } - - registration, err := session.RPC.UI.RegisterDirectAutoModeSwitchHandler(t.Context()) - if err != nil { - t.Fatalf("UI.RegisterDirectAutoModeSwitchHandler failed: %v", err) - } - if strings.TrimSpace(registration.Handle) == "" { - t.Fatal("Expected non-empty direct auto mode switch handler handle") - } - - unregister, err := session.RPC.UI.UnregisterDirectAutoModeSwitchHandler(t.Context(), &rpc.UIUnregisterDirectAutoModeSwitchHandlerRequest{ - Handle: registration.Handle, - }) - if err != nil { - t.Fatalf("UI.UnregisterDirectAutoModeSwitchHandler failed: %v", err) - } - if !unregister.Unregistered { - t.Fatal("Expected registered direct handler to unregister") - } - - unregisterAgain, err := session.RPC.UI.UnregisterDirectAutoModeSwitchHandler(t.Context(), &rpc.UIUnregisterDirectAutoModeSwitchHandlerRequest{ - Handle: registration.Handle, - }) - if err != nil { - t.Fatalf("UI.UnregisterDirectAutoModeSwitchHandler second call failed: %v", err) - } - if unregisterAgain.Unregistered { - t.Fatal("Expected second direct handler unregister to return false") - } - }) -} diff --git a/go/internal/e2e/rpc_ui_ephemeral_query_e2e_test.go b/go/internal/e2e/rpc_ui_ephemeral_query_e2e_test.go deleted file mode 100644 index a431aaee04..0000000000 --- a/go/internal/e2e/rpc_ui_ephemeral_query_e2e_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestRpcUiEphemeralQuery(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should_answer_ephemeral_query", func(t *testing.T) { - // 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. - t.Skip("blocked on CLI 1.0.81-5 session.ui.ephemeralQuery regression on macOS") - - ctx.ConfigureForTest(t) - session := createPortedSession(t, client, nil) - defer session.Disconnect() - - result, err := session.RPC.UI.EphemeralQuery(t.Context(), &rpc.UIEphemeralQueryRequest{ - Question: "In one word, what is the primary color of a clear daytime sky?", - }) - if err != nil { - t.Fatalf("UI.EphemeralQuery failed: %v", err) - } - if result == nil { - t.Fatal("Expected non-nil ephemeral query result") - return - } - if strings.TrimSpace(result.Answer) == "" { - t.Fatal("Expected non-empty ephemeral query answer") - } - if !strings.Contains(strings.ToLower(result.Answer), "blue") { - t.Fatalf("Expected answer to contain blue, got %q", result.Answer) - } - }) -} diff --git a/go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go b/go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go deleted file mode 100644 index 849e3a5fa6..0000000000 --- a/go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package e2e - -import ( - "os" - "strings" - "testing" - - copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/e2e/testharness" - "github.com/github/copilot-sdk/go/rpc" -) - -// Mirrors dotnet/test/E2E/RpcWorkspaceCheckpointsE2ETests.cs (snapshot category "rpc_workspace_checkpoints"). -func TestRPCWorkspaceCheckpointsE2E(t *testing.T) { - ctx := testharness.NewTestContext(t) - client := ctx.NewClient() - t.Cleanup(func() { client.ForceStop() }) - - t.Run("should list no checkpoints for fresh session", func(t *testing.T) { - session := createWorkspaceRPCSession(t, client) - defer session.Disconnect() - - result, err := session.RPC.Workspaces.ListCheckpoints(t.Context()) - if err != nil { - t.Fatalf("Workspaces.ListCheckpoints failed: %v", err) - } - if result.Checkpoints == nil { - t.Fatal("Expected non-nil Checkpoints") - } - if len(result.Checkpoints) != 0 { - t.Fatalf("Expected no checkpoints for fresh session, got %+v", result.Checkpoints) - } - }) - - t.Run("should return nil or empty content for unknown checkpoint", func(t *testing.T) { - // In-process, session.workspaces.readCheckpoint is answered by the native - // runtime, which decodes the checkpoint number as a u32 and rejects the - // large sentinel this test uses. Covered by the default (stdio) transport. - // Mirrors Rust's should_return_null_or_empty_content_for_unknown_checkpoint. - testharness.SkipIfInProcess(t, "readCheckpoint decodes the id as u32 in-process") - session := createWorkspaceRPCSession(t, client) - defer session.Disconnect() - - result, err := session.RPC.Workspaces.ReadCheckpoint(t.Context(), &rpc.WorkspacesReadCheckpointRequest{Number: 1<<62 - 1}) - if err != nil { - t.Fatalf("Workspaces.ReadCheckpoint failed: %v", err) - } - if result.Content != nil && *result.Content != "" { - t.Fatalf("Expected nil or empty content for unknown checkpoint, got %q", *result.Content) - } - }) - - t.Run("should return typed workspace diff result", func(t *testing.T) { - session := createWorkspaceRPCSession(t, client) - defer session.Disconnect() - - result, err := session.RPC.Workspaces.Diff(t.Context(), &rpc.WorkspacesDiffRequest{Mode: rpc.WorkspaceDiffModeUnstaged}) - if err != nil { - t.Fatalf("Workspaces.Diff failed: %v", err) - } - if result.RequestedMode != rpc.WorkspaceDiffModeUnstaged { - t.Fatalf("Expected RequestedMode=unstaged, got %q", result.RequestedMode) - } - if result.Mode != rpc.WorkspaceDiffModeUnstaged && result.Mode != rpc.WorkspaceDiffModeBranch { - t.Fatalf("Unexpected effective diff mode %q", result.Mode) - } - if result.Changes == nil { - t.Fatal("Expected non-nil Changes") - } - for _, change := range result.Changes { - if strings.TrimSpace(change.Path) == "" { - t.Fatalf("Diff change has empty path: %+v", change) - } - switch change.ChangeType { - case rpc.WorkspaceDiffFileChangeTypeAdded, - rpc.WorkspaceDiffFileChangeTypeModified, - rpc.WorkspaceDiffFileChangeTypeDeleted, - rpc.WorkspaceDiffFileChangeTypeRenamed: - default: - t.Fatalf("Unexpected diff change type %q", change.ChangeType) - } - _ = change.Diff - } - }) - - t.Run("should save large paste and expose readable content", func(t *testing.T) { - session := createWorkspaceRPCSession(t, client) - defer session.Disconnect() - content := strings.Repeat("Large paste payload 🚀\n", 512) - - result, err := session.RPC.Workspaces.SaveLargePaste(t.Context(), &rpc.WorkspacesSaveLargePasteRequest{Content: content}) - if err != nil { - t.Fatalf("Workspaces.SaveLargePaste failed: %v", err) - } - if result.Saved == nil { - t.Fatal("Expected SaveLargePaste to return saved descriptor") - } - saved := result.Saved - if strings.TrimSpace(saved.Filename) == "" || strings.TrimSpace(saved.FilePath) == "" { - t.Fatalf("Expected saved filename and filepath, got %+v", saved) - } - if saved.SizeBytes != int64(len([]byte(content))) { - t.Fatalf("Expected SizeBytes=%d, got %d", len([]byte(content)), saved.SizeBytes) - } - - read, readErr := session.RPC.Workspaces.ReadFile(t.Context(), &rpc.WorkspacesReadFileRequest{Path: saved.Filename}) - if readErr == nil { - if read.Content != content { - t.Fatalf("Expected ReadFile content to match saved paste") - } - return - } - - bytes, err := os.ReadFile(saved.FilePath) - if err != nil { - t.Fatalf("ReadFile failed (%v), and saved file %q was not readable: %v", readErr, saved.FilePath, err) - } - if string(bytes) != content { - t.Fatalf("Expected saved file content to match large paste") - } - }) -} - -func createWorkspaceRPCSession(t *testing.T, client *copilot.Client) *copilot.Session { - t.Helper() - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - return session -} diff --git a/go/internal/e2e/runtime_host_e2e_test.go b/go/internal/e2e/runtime_host_e2e_test.go new file mode 100644 index 0000000000..b30325ca72 --- /dev/null +++ b/go/internal/e2e/runtime_host_e2e_test.go @@ -0,0 +1,330 @@ +package e2e + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +const ahpToolPrompt = "Use the magic_number tool with seed 'hello' and tell me the result" +const ahpComposedPrompt = "Call magic_number with seed 'hello' and client_echo with text 'ping', then report both results" +const ahpPromptMarker = "APPLICATION_OWNED_AHP_PROMPT" + +type ahpTestResponse struct { + ClientID string `json:"clientId"` + SessionID string `json:"sessionId"` + Text string `json:"text"` + ClientToolCalls int `json:"clientToolCalls"` + History []struct { + Message struct { + Text string `json:"text"` + } `json:"message"` + } `json:"history"` +} + +func driveAhp(t *testing.T, ahp *testharness.AhpTestClient, command map[string]any) ahpTestResponse { + t.Helper() + deadline, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + data, err := ahp.Request(deadline, command) + if err != nil { + t.Fatal(err) + } + var response ahpTestResponse + if err := json.Unmarshal(data, &response); err != nil { + t.Fatal(err) + } + return response +} + +func connectAhpTest(t *testing.T, ahp *testharness.AhpTestClient, host *copilot.AhpHost, clientID string) (string, func()) { + t.Helper() + if host.URL == nil { + t.Fatal("local host did not return a URL") + } + command := map[string]any{"op": "connect", "url": *host.URL, "githubToken": "fake-token-for-e2e-tests"} + if host.Token != nil { + command["token"] = *host.Token + } + if clientID != "" { + command["clientId"] = clientID + } + id := driveAhp(t, ahp, command).ClientID + var once sync.Once + close := func() { once.Do(func() { driveAhp(t, ahp, map[string]any{"op": "close", "clientId": id}) }) } + t.Cleanup(close) + return id, close +} + +type ahpApplication struct { + client *copilot.Client + workDir string + session atomic.Pointer[copilot.Session] + creates atomic.Int32 + resumes atomic.Int32 + tools atomic.Int32 + hooks atomic.Int32 + released chan *copilot.Session + exited chan copilot.AhpHostExit +} + +func newAhpApplication(client *copilot.Client, workDir string) *ahpApplication { + return &ahpApplication{ + client: client, workDir: workDir, + released: make(chan *copilot.Session, 4), exited: make(chan copilot.AhpHostExit, 4), + } +} + +func (a *ahpApplication) callbacks() ([]copilot.Tool, *copilot.SessionHooks) { + type seed struct { + Seed string `json:"seed" jsonschema:"A seed value"` + } + tool := copilot.DefineTool("magic_number", "Returns a magic number", func(args seed, invocation copilot.ToolInvocation) (string, error) { + if args.Seed != "hello" || a.session.Load() == nil || invocation.SessionID != a.session.Load().SessionID { + return "", fmt.Errorf("wrong tool arguments or session identity") + } + a.tools.Add(1) + return "MAGIC_hello_42", nil + }) + hooks := &copilot.SessionHooks{ + OnPreToolUse: func(_ copilot.PreToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { + if a.session.Load() == nil || invocation.SessionID != a.session.Load().SessionID { + return nil, fmt.Errorf("wrong hook session identity") + } + a.hooks.Add(1) + return nil, nil + }, + } + return []copilot.Tool{tool}, hooks +} + +func (a *ahpApplication) createConfig(config *copilot.SessionConfig) { + config.OnPermissionRequest = copilot.PermissionHandler.ApproveAll + config.SystemMessage = &copilot.SystemMessageConfig{Mode: "append", Content: ahpPromptMarker} + config.Tools, config.Hooks = a.callbacks() +} + +func (a *ahpApplication) options() *copilot.AhpHostOptions { + return &copilot.AhpHostOptions{ + LocalServer: &rpc.HostLocalServerOptions{}, + CreateSession: func(ctx context.Context, request copilot.AhpSessionCreateRequest) (*copilot.Session, error) { + a.creates.Add(1) + if ctx.Err() != nil || request.Config.WorkingDirectory != a.workDir { + return nil, fmt.Errorf("invalid create handoff") + } + a.createConfig(request.Config) + session, err := a.client.CreateSession(ctx, request.Config) + a.session.Store(session) + return session, err + }, + ResumeSession: func(ctx context.Context, request copilot.AhpSessionResumeRequest) (*copilot.Session, error) { + a.resumes.Add(1) + config := request.Config + if ctx.Err() != nil || config.WorkingDirectory != a.workDir || + config.ContinuePendingWork == nil || *config.ContinuePendingWork { + return nil, fmt.Errorf("invalid resume handoff") + } + config.OnPermissionRequest = copilot.PermissionHandler.ApproveAll + config.SystemMessage = &copilot.SystemMessageConfig{Mode: "append", Content: ahpPromptMarker} + config.Tools, config.Hooks = a.callbacks() + session, err := a.client.ResumeSessionWithOptions(ctx, request.SessionID, config) + a.session.Store(session) + return session, err + }, + OnSessionReleased: func(session *copilot.Session) error { a.released <- session; return nil }, + OnExit: func(event copilot.AhpHostExit) error { a.exited <- event; return nil }, + } +} + +func assertAhpApplication(t *testing.T, ctx *testharness.TestContext, app *ahpApplication) { + t.Helper() + if app.tools.Load() != 1 || app.hooks.Load() == 0 { + t.Fatalf("application callbacks missing: tools=%d hooks=%d", app.tools.Load(), app.hooks.Load()) + } + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatal(err) + } + advertised, prompted := false, false + for _, exchange := range exchanges { + for _, message := range exchange.Request.Messages { + prompted = prompted || strings.Contains(message.Content, ahpPromptMarker) + } + for _, tool := range exchange.Request.Tools { + advertised = advertised || tool.Function.Name == "magic_number" + } + } + if !advertised || !prompted { + t.Fatal("application prompt/tool declarations were not preserved") + } +} + +func awaitAhpRelease(t *testing.T, app *ahpApplication) { + t.Helper() + select { + case session := <-app.released: + if session != app.session.Load() { + t.Fatal("release did not receive the original session object") + } + case <-time.After(10 * time.Second): + t.Fatal("session was not released") + } +} + +func TestRuntimeHostE2E(t *testing.T) { + if os.Getenv("COPILOT_RUNTIME_HOST_E2E") != "1" { + t.Skip("Requires an integrated runtime; set COPILOT_RUNTIME_HOST_E2E=1") + } + for _, publish := range []bool{false, true} { + name := "creates application session" + if publish { + name = "publishes exact resident session" + } + t.Run(name, func(t *testing.T) { + ctx := testharness.NewTestContext(t) + ahp := testharness.NewAhpTestClient(t) + ctx.ConfigureSnapshot(t, "multi_client/both_clients_see_tool_request_and_completion_events") + client := ctx.NewClient() + t.Cleanup(func() { + if err := client.Stop(); err != nil { + t.Error(err) + } + }) + app := newAhpApplication(client, ctx.WorkDir) + if publish { + config := &copilot.SessionConfig{WorkingDirectory: ctx.WorkDir} + app.createConfig(config) + session, err := client.CreateSession(t.Context(), config) + if err != nil { + t.Fatal(err) + } + app.session.Store(session) + } + host, err := client.StartAhpHost(t.Context(), app.options()) + if err != nil { + t.Fatal(err) + } + if host.PID != nil { + t.Fatal("in-process listener returned a companion PID") + } + clientID, _ := connectAhpTest(t, ahp, host, "") + var sessionID string + if publish { + sessionID = app.session.Load().SessionID + result, err := host.PublishSession(t.Context(), sessionID) + if err != nil || result.SessionID != sessionID || result.SessionURI != "ahp-session:/"+sessionID { + t.Fatalf("incorrect publication: %v %v", result, err) + } + driveAhp(t, ahp, map[string]any{"op": "attach", "clientId": clientID, "sessionId": sessionID}) + } else { + sessionID = driveAhp(t, ahp, map[string]any{"op": "create", "clientId": clientID, "workDir": ctx.WorkDir}).SessionID + if app.creates.Load() != 1 || app.session.Load().SessionID != sessionID { + t.Fatal("create callback did not materialize the requested identity") + } + } + response := driveAhp(t, ahp, map[string]any{"op": "turn", "clientId": clientID, "sessionId": sessionID, "prompt": ahpToolPrompt}) + if !strings.Contains(response.Text, "MAGIC_hello_42") { + t.Fatal(response.Text) + } + assertAhpApplication(t, ctx, app) + for range 2 { + if err := host.Dispose(t.Context()); err != nil { + t.Fatal(err) + } + } + driveAhp(t, ahp, map[string]any{"op": "stopped", "clientId": clientID, "url": host.URL}) + if publish { + if app.creates.Load() != 0 || app.resumes.Load() != 0 || len(app.released) != 0 { + t.Fatal("publication invoked a factory or transferred ownership") + } + } else { + awaitAhpRelease(t, app) + } + select { + case <-app.exited: + case <-time.After(10 * time.Second): + t.Fatal("listener exit was not reported") + } + events, err := app.session.Load().GetEvents(t.Context()) + if err != nil || len(events) == 0 { + t.Fatalf("original session did not survive: %v", err) + } + pong, err := client.Ping(t.Context(), "still alive") + if err != nil || pong.Message != "pong: still alive" { + t.Fatalf("runtime did not survive: %v", err) + } + }) + } + + t.Run("resumes after runtime restart and composes tools", func(t *testing.T) { + ctx := testharness.NewTestContext(t) + ahp := testharness.NewAhpTestClient(t) + ctx.ConfigureSnapshot(t, "runtime_host/app_resume_callback_composes_tools_after_history") + firstOwner := ctx.NewClient() + t.Cleanup(firstOwner.ForceStop) + first := newAhpApplication(firstOwner, ctx.WorkDir) + host, err := firstOwner.StartAhpHost(t.Context(), first.options()) + if err != nil { + t.Fatal(err) + } + clientID, closeFirst := connectAhpTest(t, ahp, host, "") + sessionID := driveAhp(t, ahp, map[string]any{"op": "create", "clientId": clientID, "workDir": ctx.WorkDir, "clientTools": true}).SessionID + answer := driveAhp(t, ahp, map[string]any{"op": "turn", "clientId": clientID, "sessionId": sessionID, "prompt": "What is 2+2?"}) + if !strings.Contains(answer.Text, "4") { + t.Fatal(answer.Text) + } + if err := host.Dispose(t.Context()); err != nil { + t.Fatal(err) + } + driveAhp(t, ahp, map[string]any{"op": "stopped", "clientId": clientID, "url": host.URL}) + awaitAhpRelease(t, first) + closeFirst() + if err := firstOwner.Stop(); err != nil { + t.Fatal(err) + } + + resumedOwner := ctx.NewClient() + t.Cleanup(func() { + if err := resumedOwner.Stop(); err != nil { + t.Error(err) + } + }) + resumed := newAhpApplication(resumedOwner, ctx.WorkDir) + replacement, err := resumedOwner.StartAhpHost(t.Context(), resumed.options()) + if err != nil { + t.Fatal(err) + } + reconnected, _ := connectAhpTest(t, ahp, replacement, clientID) + history := driveAhp(t, ahp, map[string]any{"op": "attach", "clientId": reconnected, "sessionId": sessionID, "clientTools": true}).History + if len(history) != 1 || history[0].Message.Text != "What is 2+2?" { + t.Fatalf("lost durable history: %+v", history) + } + if resumed.creates.Load() != 0 || resumed.resumes.Load() != 1 || + resumed.session.Load() == first.session.Load() || resumed.session.Load().SessionID != sessionID { + t.Fatal("incorrect application resume callback or identity") + } + response := driveAhp(t, ahp, map[string]any{"op": "turn", "clientId": reconnected, "sessionId": sessionID, "prompt": ahpComposedPrompt, "clientTools": true}) + if response.ClientToolCalls != 1 || !strings.Contains(response.Text, "CLIENT_ECHO_ping") || !strings.Contains(response.Text, "MAGIC_hello_42") { + t.Fatalf("tools did not compose: %+v", response) + } + assertAhpApplication(t, ctx, resumed) + if err := replacement.Dispose(t.Context()); err != nil { + t.Fatal(err) + } + driveAhp(t, ahp, map[string]any{"op": "stopped", "clientId": reconnected, "url": replacement.URL}) + awaitAhpRelease(t, resumed) + if _, err := resumed.session.Load().GetEvents(t.Context()); err != nil { + t.Fatal(err) + } + }) +} diff --git a/go/internal/e2e/scenario_fixture_test.go b/go/internal/e2e/scenario_fixture_test.go new file mode 100644 index 0000000000..00c35ceb4d --- /dev/null +++ b/go/internal/e2e/scenario_fixture_test.go @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package e2e + +import ( + "context" + "encoding/json" + "net" + "reflect" + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/jsonrpc2" +) + +func assertJSONSubset(t *testing.T, label string, expected, actual any) { + t.Helper() + switch expected := expected.(type) { + case map[string]any: + actual, ok := actual.(map[string]any) + if !ok { + t.Fatalf("%s = %#v, want object", label, actual) + } + for key, value := range expected { + actualValue, exists := actual[key] + if !exists { + t.Fatalf("%s missing %q in %#v", label, key, actual) + } + assertJSONSubset(t, label+"."+key, value, actualValue) + } + case []any: + actual, ok := actual.([]any) + if !ok || len(actual) != len(expected) { + t.Fatalf("%s = %#v, want %d items", label, actual, len(expected)) + } + for i := range expected { + assertJSONSubset(t, label, expected[i], actual[i]) + } + default: + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("%s = %#v, want %#v", label, actual, expected) + } + } +} + +type generatedRPCFixture struct { + client *copilot.Client + session *copilot.Session + server *jsonrpc2.Client + conn net.Conn +} + +func newGeneratedRPCFixture(t *testing.T, ctx context.Context) *generatedRPCFixture { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = listener.Close() }) + + type serverConnection struct { + server *jsonrpc2.Client + conn net.Conn + } + ready := make(chan serverConnection, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + return + } + server := jsonrpc2.NewClient(conn, conn) + t.Cleanup(server.Stop) + for method, result := range map[string]string{ + "connect": `{"ok":true,"protocolVersion":3,"version":"test"}`, + "plugins.builtin.set": `{}`, + "session.create": `{"sessionId":"generated-rpc-surface"}`, + "session.options.update": `{"success":true}`, + "session.detach": `{"success":true}`, + } { + server.SetRequestHandler(method, func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return json.RawMessage(result), nil + }) + } + server.Start() + ready <- serverConnection{server: server, conn: conn} + }() + + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.URIConnection{URL: listener.Addr().String()}, + }) + t.Cleanup(client.ForceStop) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: "generated-rpc-surface", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatal(err) + } + + select { + case connection := <-ready: + return &generatedRPCFixture{client: client, session: session, server: connection.server, conn: connection.conn} + case <-ctx.Done(): + t.Fatal(ctx.Err()) + return nil + } +} diff --git a/go/internal/e2e/scenario_testing_control_state_e2e_test.go b/go/internal/e2e/scenario_testing_control_state_e2e_test.go deleted file mode 100644 index 5047038948..0000000000 --- a/go/internal/e2e/scenario_testing_control_state_e2e_test.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -package e2e - -import ( - "encoding/json" - "fmt" - "net" - "sync/atomic" - "testing" - - "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/internal/jsonrpc2" -) - -func TestScenarioTestingControlStateE2E(t *testing.T) { - t.Run("reports processing while scenario tool is running", func(t *testing.T) { - fixture := newGeneratedRPCFixture(t, t.Context()) - fixture.server.SetRequestHandler("session.create", func(_ json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return json.RawMessage(`{"sessionId":"processing-session"}`), nil - }) - - var processing atomic.Bool - fixture.server.SetRequestHandler("session.metadata.isProcessing", func(_ json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return mustJSON(t, map[string]any{"processing": processing.Load()}), nil - }) - fixture.server.SetRequestHandler("session.metadata.activity", func(_ json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - active := processing.Load() - return mustJSON(t, map[string]any{ - "hasActiveWork": active, - "abortable": active, - }), nil - }) - - toolStarted := make(chan struct{}) - releaseTool := make(chan struct{}) - toolCompleted := make(chan struct{}) - fixture.server.SetRequestHandler("session.tools.handlePendingToolCall", func(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - RequestID string `json:"requestId"` - Result any `json:"result"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - t.Errorf("Unmarshal tool result failed: %v", err) - } - if params.RequestID != "processing-request" || params.Result == nil { - t.Errorf("Unexpected tool completion: %#v", params) - } - processing.Store(false) - close(toolCompleted) - return json.RawMessage(`{"success":true}`), nil - }) - - session, err := fixture.client.CreateSession(t.Context(), &copilot.SessionConfig{ - SessionID: "processing-session", - Tools: []copilot.Tool{ - copilot.DefineTool("wait_for_scenario_control", "Waits for the scenario controller", - func(_ struct{}, _ copilot.ToolInvocation) (string, error) { - close(toolStarted) - <-releaseTool - return "SCENARIO_CONTROL_DONE", nil - }), - }, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - defer session.Disconnect() - - assertProcessingState(t, session, false) - - processing.Store(true) - writeScenarioNotification(t, fixture.conn, "session.event", map[string]any{ - "sessionId": session.SessionID, - "event": scenarioEvent("tool-request", &copilot.ExternalToolRequestedData{ - Arguments: map[string]any{}, - RequestID: "processing-request", - SessionID: session.SessionID, - ToolCallID: "processing-tool-call", - ToolName: "wait_for_scenario_control", - }), - }) - - select { - case <-toolStarted: - case <-t.Context().Done(): - t.Fatal("Test context ended before tool handler started") - } - assertProcessingState(t, session, true) - - close(releaseTool) - select { - case <-toolCompleted: - case <-t.Context().Done(): - t.Fatal("Test context ended before tool completion was handled") - } - assertProcessingState(t, session, false) - }) -} - -func writeScenarioNotification(t *testing.T, conn net.Conn, method string, params any) { - t.Helper() - message, err := json.Marshal(map[string]any{ - "jsonrpc": "2.0", - "method": method, - "params": params, - }) - if err != nil { - t.Fatal(err) - } - frame := append([]byte(fmt.Sprintf("Content-Length: %d\r\n\r\n", len(message))), message...) - if _, err := conn.Write(frame); err != nil { - t.Fatal(err) - } -} - -func assertProcessingState(t *testing.T, session *copilot.Session, want bool) { - t.Helper() - state, err := session.RPC.Metadata.IsProcessing(t.Context()) - if err != nil { - t.Fatalf("Metadata.IsProcessing failed: %v", err) - } - if state.Processing != want { - t.Fatalf("Processing = %t, want %t", state.Processing, want) - } - activity, err := session.RPC.Metadata.Activity(t.Context()) - if err != nil { - t.Fatalf("Metadata.Activity failed: %v", err) - } - if activity.HasActiveWork != want || activity.Abortable != want { - t.Fatalf("Activity = %#v, want active/abortable %t", activity, want) - } -} diff --git a/go/internal/e2e/scenario_testing_server_control_e2e_test.go b/go/internal/e2e/scenario_testing_server_control_e2e_test.go deleted file mode 100644 index 656bcf82f2..0000000000 --- a/go/internal/e2e/scenario_testing_server_control_e2e_test.go +++ /dev/null @@ -1,612 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -package e2e - -import ( - "encoding/json" - "reflect" - "sync" - "testing" - - "github.com/github/copilot-sdk/go/internal/jsonrpc2" - "github.com/github/copilot-sdk/go/rpc" -) - -func TestScenarioTestingServerControlE2E(t *testing.T) { - t.Run("searches server catalog with category contract", func(t *testing.T) { - tests := []struct { - name string - kinds []rpc.CatalogCandidateKind - capabilities []string - }{ - { - name: "all", - kinds: []rpc.CatalogCandidateKind{rpc.CatalogCandidateKindMCPServer, rpc.CatalogCandidateKindAiSkill}, - capabilities: []string{"mcp-server-card", "ai-skill-discovery"}, - }, - { - name: "mcp", - kinds: []rpc.CatalogCandidateKind{rpc.CatalogCandidateKindMCPServer}, - capabilities: []string{"mcp-server-card"}, - }, - { - name: "skills", - kinds: []rpc.CatalogCandidateKind{rpc.CatalogCandidateKindAiSkill}, - capabilities: []string{"ai-skill-discovery"}, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - fixture := newGeneratedRPCFixture(t, t.Context()) - var captured map[string]any - fixture.server.SetRequestHandler("catalog.search", func(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - if err := json.Unmarshal(request, &captured); err != nil { - t.Errorf("Unmarshal catalog request failed: %v", err) - } - return mustJSON(t, map[string]any{ - "kind": "succeeded", - "candidates": []any{}, - "negotiated": map[string]any{ - "grantedCapabilities": test.capabilities, - "runtimeProtocolVersion": 3, - }, - "searchId": "scenario-search", - "truncated": false, - }), nil - }) - - limit := int32(50) - result, err := fixture.client.RPC.Catalog.Search(t.Context(), &rpc.CatalogSearchRequest{ - Contract: rpc.CatalogClientContract{ - ProtocolVersion: 3, - RequiredCapabilities: test.capabilities, - }, - Kinds: test.kinds, - Limit: &limit, - Query: "scenario search", - }) - if err != nil { - t.Fatalf("Catalog.Search failed: %v", err) - } - succeeded, ok := result.(*rpc.CatalogSearchSucceeded) - if !ok { - t.Fatalf("Expected CatalogSearchSucceeded, got %T", result) - } - if len(succeeded.Candidates) != 0 || succeeded.SearchID != "scenario-search" || succeeded.Truncated { - t.Fatalf("Unexpected catalog result: %#v", succeeded) - } - if succeeded.Negotiated.RuntimeProtocolVersion != 3 || - !reflect.DeepEqual(succeeded.Negotiated.GrantedCapabilities, catalogCapabilities(test.capabilities)) { - t.Fatalf("Unexpected negotiated contract: %#v", succeeded.Negotiated) - } - - assertJSONSubset(t, "catalog.search", map[string]any{ - "query": "scenario search", - "limit": float64(50), - "contract": map[string]any{ - "protocolVersion": float64(3), - "requiredCapabilities": stringsToAny(test.capabilities), - }, - "kinds": catalogKindsToAny(test.kinds), - }, captured) - }) - } - }) - - t.Run("observes pages and cancels factory run", func(t *testing.T) { - fixture := newGeneratedRPCFixture(t, t.Context()) - var mu sync.Mutex - captured := map[string]map[string]any{} - setFactoryHandler := func(method string, result any) { - fixture.server.SetRequestHandler(method, func(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params map[string]any - if err := json.Unmarshal(request, ¶ms); err != nil { - t.Errorf("Unmarshal %s failed: %v", method, err) - } - mu.Lock() - captured[method] = params - mu.Unlock() - return mustJSON(t, result), nil - }) - } - setFactoryHandler("session.factory.listRuns", map[string]any{ - "runs": []any{map[string]any{ - "runId": "factory-run-1", - "factoryName": "scenario-factory", - "status": "running", - }}, - "oldestSeq": 7, - "newestSeq": 7, - "hasMoreNewer": false, - }) - setFactoryHandler("session.factory.getRunDetail", map[string]any{ - "runId": "factory-run-1", - "factoryName": "scenario-factory", - "status": "running", - "revision": 4, - }) - setFactoryHandler("session.factory.getRunProgress", map[string]any{ - "records": []any{map[string]any{ - "attempt": 1, - "kind": "log", - "phaseId": "verify", - "recordedAt": 1234, - "seq": 12, - "text": "Validation complete", - }}, - "revision": 4, - }) - setFactoryHandler("session.factory.cancel", map[string]any{ - "runId": "factory-run-1", - "status": "cancelled", - "reason": "cancelled by user", - }) - - after, before, limit := int64(3), int64(20), int32(10) - runs, err := fixture.session.RPC.Factory.ListRuns(t.Context(), &rpc.FactoryListRunsRequest{ - AfterSeq: &after, - BeforeSeq: &before, - Limit: &limit, - }) - if err != nil { - t.Fatalf("Factory.ListRuns failed: %v", err) - } - if len(runs.Runs) != 1 || runs.Runs[0].RunID != "factory-run-1" || - runs.Runs[0].FactoryName != "scenario-factory" || runs.Runs[0].Status != rpc.FactoryRunStatusRunning || - runs.OldestSeq == nil || *runs.OldestSeq != 7 || runs.NewestSeq == nil || *runs.NewestSeq != 7 || - runs.HasMoreNewer == nil || *runs.HasMoreNewer { - t.Fatalf("Unexpected factory run page: %#v", runs) - } - - detail, err := fixture.session.RPC.Factory.GetRunDetail(t.Context(), &rpc.FactoryGetRunRequest{RunID: "factory-run-1"}) - if err != nil { - t.Fatalf("Factory.GetRunDetail failed: %v", err) - } - if detail.RunID != "factory-run-1" || detail.FactoryName != "scenario-factory" || - detail.Status != rpc.FactoryRunStatusRunning || detail.Revision != 4 { - t.Fatalf("Unexpected factory detail: %#v", detail) - } - - progressAfter, progressBefore, progressLimit := int64(5), int64(20), int32(25) - phaseID := "verify" - progress, err := fixture.session.RPC.Factory.GetRunProgress(t.Context(), &rpc.FactoryGetRunProgressRequest{ - RunID: "factory-run-1", - PhaseID: &phaseID, - AfterSeq: &progressAfter, - BeforeSeq: &progressBefore, - Limit: &progressLimit, - }) - if err != nil { - t.Fatalf("Factory.GetRunProgress failed: %v", err) - } - if len(progress.Records) != 1 || progress.Records[0].Seq != 12 || - progress.Records[0].PhaseID == nil || *progress.Records[0].PhaseID != "verify" || - progress.Records[0].Kind != rpc.FactoryLogLineKindLog || - progress.Records[0].Text != "Validation complete" { - t.Fatalf("Unexpected factory progress: %#v", progress) - } - - cancelled, err := fixture.session.RPC.Factory.Cancel(t.Context(), &rpc.FactoryCancelRequest{RunID: "factory-run-1"}) - if err != nil { - t.Fatalf("Factory.Cancel failed: %v", err) - } - if cancelled.RunID != "factory-run-1" || cancelled.Status != rpc.FactoryRunStatusCancelled || - cancelled.Reason == nil || *cancelled.Reason != "cancelled by user" { - t.Fatalf("Unexpected cancelled factory run: %#v", cancelled) - } - - mu.Lock() - defer mu.Unlock() - assertJSONSubset(t, "session.factory.listRuns", map[string]any{ - "afterSeq": float64(3), - "beforeSeq": float64(20), - "limit": float64(10), - }, captured["session.factory.listRuns"]) - assertJSONSubset(t, "session.factory.getRunProgress", map[string]any{ - "runId": "factory-run-1", - "phaseId": "verify", - "afterSeq": float64(5), - "beforeSeq": float64(20), - "limit": float64(25), - }, captured["session.factory.getRunProgress"]) - }) - - t.Run("reads autopilot state and enables remote mode", func(t *testing.T) { - tests := []struct { - mode rpc.RemoteSessionMode - steerable bool - }{ - {mode: rpc.RemoteSessionModeOn, steerable: true}, - {mode: rpc.RemoteSessionModeExport, steerable: false}, - } - for _, test := range tests { - t.Run(string(test.mode), func(t *testing.T) { - fixture := newGeneratedRPCFixture(t, t.Context()) - fixture.server.SetRequestHandler("session.autopilotObjective.getState", func(_ json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return json.RawMessage(`{"state":{"id":17,"objective":"Ship the scenario.","status":"active","turnCount":3,"creditCountNanoAiu":"1250000000","creditLimit":{"credits":5,"creditsUsed":1.25,"creditsUsedNanoAiu":"1250000000"}}}`), nil - }) - var remoteRequest map[string]any - fixture.server.SetRequestHandler("session.remote.enable", func(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - if err := json.Unmarshal(request, &remoteRequest); err != nil { - t.Errorf("Unmarshal remote enable failed: %v", err) - } - return mustJSON(t, map[string]any{ - "remoteSteerable": test.steerable, - "url": "https://example.test/sessions/" + fixture.session.SessionID, - }), nil - }) - - state, err := fixture.session.RPC.AutopilotObjective.GetState(t.Context()) - if err != nil { - t.Fatalf("AutopilotObjective.GetState failed: %v", err) - } - objective := state.State - if objective == nil || objective.ID != 17 || objective.Objective != "Ship the scenario." || - objective.Status != rpc.AutopilotObjectiveStatusActive || objective.TurnCount != 3 || - objective.CreditCountNanoAiu != "1250000000" || objective.CreditLimit == nil || - objective.CreditLimit.Credits == nil || *objective.CreditLimit.Credits != 5 || - objective.CreditLimit.CreditsUsed != 1.25 || - objective.CreditLimit.CreditsUsedNanoAiu != "1250000000" { - t.Fatalf("Unexpected autopilot objective: %#v", objective) - } - - enabled, err := fixture.session.RPC.Remote.Enable(t.Context(), &rpc.RemoteEnableRequest{Mode: &test.mode}) - if err != nil { - t.Fatalf("Remote.Enable failed: %v", err) - } - expectedURL := "https://example.test/sessions/" + fixture.session.SessionID - if enabled.RemoteSteerable != test.steerable || enabled.URL == nil || *enabled.URL != expectedURL { - t.Fatalf("Unexpected remote result: %#v", enabled) - } - assertJSONSubset(t, "session.remote.enable", map[string]any{ - "sessionId": fixture.session.SessionID, - "mode": string(test.mode), - }, remoteRequest) - }) - } - }) - - t.Run("edits reorders duplicates removes and sends queued items", func(t *testing.T) { - fixture := newGeneratedRPCFixture(t, t.Context()) - queue := newScenarioQueue(t, fixture.server) - - if _, err := fixture.session.RPC.Queue.SetDrainPaused(t.Context(), &rpc.QueueSetDrainPausedRequest{Paused: true}); err != nil { - t.Fatalf("Queue.SetDrainPaused(true) failed: %v", err) - } - firstDisplay := "First visible prompt" - first, err := fixture.session.RPC.Queue.InsertAt(t.Context(), &rpc.QueueInsertAtRequest{ - Position: 0, - Message: rpc.QueueInsertMessage{ - Prompt: "First hidden prompt", - DisplayPrompt: &firstDisplay, - AgentMode: ptr(rpc.SendAgentModeInteractive), - }, - }) - if err != nil { - t.Fatalf("Queue.InsertAt(first) failed: %v", err) - } - secondDisplay := "Second visible prompt" - second, err := fixture.session.RPC.Queue.InsertAt(t.Context(), &rpc.QueueInsertAtRequest{ - Position: 1, - Message: rpc.QueueInsertMessage{ - Prompt: "Second hidden prompt", - DisplayPrompt: &secondDisplay, - AgentMode: ptr(rpc.SendAgentModePlan), - }, - }) - if err != nil { - t.Fatalf("Queue.InsertAt(second) failed: %v", err) - } - - updatedDisplay := "Updated visible prompt" - updated, err := fixture.session.RPC.Queue.UpdateText(t.Context(), &rpc.QueueUpdateTextRequest{ - ID: first.ID, - Prompt: "Updated hidden prompt", - DisplayPrompt: &updatedDisplay, - }) - if err != nil || !updated.Updated { - t.Fatalf("Queue.UpdateText: result=%#v err=%v", updated, err) - } - duplicate, err := fixture.session.RPC.Queue.DuplicateAt(t.Context(), &rpc.QueueDuplicateAtRequest{ID: first.ID}) - if err != nil || duplicate.ID == first.ID { - t.Fatalf("Queue.DuplicateAt: result=%#v err=%v", duplicate, err) - } - moved, err := fixture.session.RPC.Queue.MoveItem(t.Context(), &rpc.QueueMoveItemRequest{ - ID: second.ID, - ToPosition: 0, - }) - if err != nil || !moved.Changed { - t.Fatalf("Queue.MoveItem: result=%#v err=%v", moved, err) - } - - reordered, err := fixture.session.RPC.Queue.PendingItems(t.Context()) - if err != nil { - t.Fatalf("Queue.PendingItems failed: %v", err) - } - gotIDs := []string{reordered.Items[0].ID, reordered.Items[1].ID, reordered.Items[2].ID} - wantIDs := []string{second.ID, first.ID, duplicate.ID} - if !reflect.DeepEqual(gotIDs, wantIDs) { - t.Fatalf("Queue order: got %v, want %v", gotIDs, wantIDs) - } - if reordered.Items[1].DisplayText != updatedDisplay || - reordered.Items[1].AgentMode != rpc.SendAgentModeInteractive { - t.Fatalf("Unexpected edited queue item: %#v", reordered.Items[1]) - } - - sent, err := fixture.session.RPC.Queue.SendNow(t.Context(), &rpc.QueueSendNowRequest{ID: second.ID}) - if err != nil || !sent.Steered { - t.Fatalf("Queue.SendNow: result=%#v err=%v", sent, err) - } - removed, err := fixture.session.RPC.Queue.RemoveAt(t.Context(), &rpc.QueueRemoveAtRequest{ID: duplicate.ID}) - if err != nil || !removed.Removed { - t.Fatalf("Queue.RemoveAt: result=%#v err=%v", removed, err) - } - remaining, err := fixture.session.RPC.Queue.PendingItems(t.Context()) - if err != nil { - t.Fatalf("Queue.PendingItems after edits failed: %v", err) - } - if len(remaining.Items) != 1 || remaining.Items[0].ID != first.ID || - remaining.Items[0].DisplayText != updatedDisplay || - len(remaining.SteeringMessages) != 1 || remaining.SteeringMessages[0] != secondDisplay { - t.Fatalf("Unexpected remaining queue state: %#v", remaining) - } - if _, err := fixture.session.RPC.Queue.SetDrainPaused(t.Context(), &rpc.QueueSetDrainPausedRequest{Paused: false}); err != nil { - t.Fatalf("Queue.SetDrainPaused(false) failed: %v", err) - } - if !reflect.DeepEqual(queue.pauseRequests, []bool{true, false}) { - t.Fatalf("Pause requests: got %v, want [true false]", queue.pauseRequests) - } - }) -} - -func catalogCapabilities(values []string) []rpc.CatalogCapability { - result := make([]rpc.CatalogCapability, len(values)) - for i, value := range values { - result[i] = rpc.CatalogCapability(value) - } - return result -} - -func stringsToAny(values []string) []any { - result := make([]any, len(values)) - for i, value := range values { - result[i] = value - } - return result -} - -func catalogKindsToAny(values []rpc.CatalogCandidateKind) []any { - result := make([]any, len(values)) - for i, value := range values { - result[i] = string(value) - } - return result -} - -func ptr[T any](value T) *T { - return &value -} - -type scenarioQueueItem struct { - id string - prompt string - displayText string - agentMode rpc.SendAgentMode -} - -type scenarioQueue struct { - t *testing.T - mu sync.Mutex - items []scenarioQueueItem - steeringMessages []string - pauseRequests []bool - nextID int -} - -func newScenarioQueue(t *testing.T, server *jsonrpc2.Client) *scenarioQueue { - q := &scenarioQueue{t: t} - server.SetRequestHandler("session.queue.setDrainPaused", q.setDrainPaused) - server.SetRequestHandler("session.queue.insertAt", q.insertAt) - server.SetRequestHandler("session.queue.updateText", q.updateText) - server.SetRequestHandler("session.queue.duplicateAt", q.duplicateAt) - server.SetRequestHandler("session.queue.moveItem", q.moveItem) - server.SetRequestHandler("session.queue.pendingItems", q.pendingItems) - server.SetRequestHandler("session.queue.sendNow", q.sendNow) - server.SetRequestHandler("session.queue.removeAt", q.removeAt) - return q -} - -func (q *scenarioQueue) setDrainPaused(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - Paused bool `json:"paused"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - q.t.Errorf("Unmarshal setDrainPaused: %v", err) - } - q.mu.Lock() - defer q.mu.Unlock() - q.pauseRequests = append(q.pauseRequests, params.Paused) - return json.RawMessage(`{}`), nil -} - -func (q *scenarioQueue) insertAt(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - Position int `json:"position"` - Message struct { - Prompt string `json:"prompt"` - DisplayPrompt *string `json:"displayPrompt"` - AgentMode *rpc.SendAgentMode `json:"agentMode"` - } `json:"message"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - q.t.Errorf("Unmarshal insertAt: %v", err) - } - q.mu.Lock() - defer q.mu.Unlock() - q.nextID++ - item := scenarioQueueItem{ - id: "queue-" + string(rune('0'+q.nextID)), - prompt: params.Message.Prompt, - displayText: params.Message.Prompt, - agentMode: rpc.SendAgentModeInteractive, - } - if params.Message.DisplayPrompt != nil { - item.displayText = *params.Message.DisplayPrompt - } - if params.Message.AgentMode != nil { - item.agentMode = *params.Message.AgentMode - } - position := params.Position - if position < 0 { - position = 0 - } - if position > len(q.items) { - position = len(q.items) - } - q.items = append(q.items, scenarioQueueItem{}) - copy(q.items[position+1:], q.items[position:]) - q.items[position] = item - return mustJSON(q.t, map[string]any{"id": item.id}), nil -} - -func (q *scenarioQueue) updateText(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - ID string `json:"id"` - Prompt string `json:"prompt"` - DisplayPrompt *string `json:"displayPrompt"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - q.t.Errorf("Unmarshal updateText: %v", err) - } - q.mu.Lock() - defer q.mu.Unlock() - for i := range q.items { - if q.items[i].id == params.ID { - q.items[i].prompt = params.Prompt - q.items[i].displayText = params.Prompt - if params.DisplayPrompt != nil { - q.items[i].displayText = *params.DisplayPrompt - } - return json.RawMessage(`{"updated":true}`), nil - } - } - return json.RawMessage(`{"updated":false}`), nil -} - -func (q *scenarioQueue) duplicateAt(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - ID string `json:"id"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - q.t.Errorf("Unmarshal duplicateAt: %v", err) - } - q.mu.Lock() - defer q.mu.Unlock() - for i, item := range q.items { - if item.id == params.ID { - q.nextID++ - duplicate := item - duplicate.id = "queue-" + string(rune('0'+q.nextID)) - q.items = append(q.items, scenarioQueueItem{}) - copy(q.items[i+2:], q.items[i+1:]) - q.items[i+1] = duplicate - return mustJSON(q.t, map[string]any{"id": duplicate.id}), nil - } - } - return nil, &jsonrpc2.Error{Code: -32602, Message: "queue item not found"} -} - -func (q *scenarioQueue) moveItem(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - ID string `json:"id"` - ToPosition int `json:"toPosition"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - q.t.Errorf("Unmarshal moveItem: %v", err) - } - q.mu.Lock() - defer q.mu.Unlock() - from := -1 - for i := range q.items { - if q.items[i].id == params.ID { - from = i - break - } - } - if from < 0 { - return nil, &jsonrpc2.Error{Code: -32602, Message: "queue item not found"} - } - to := params.ToPosition - if to < 0 { - to = 0 - } - if to >= len(q.items) { - to = len(q.items) - 1 - } - if from == to { - return json.RawMessage(`{"changed":false}`), nil - } - item := q.items[from] - q.items = append(q.items[:from], q.items[from+1:]...) - q.items = append(q.items, scenarioQueueItem{}) - copy(q.items[to+1:], q.items[to:]) - q.items[to] = item - return json.RawMessage(`{"changed":true}`), nil -} - -func (q *scenarioQueue) pendingItems(_ json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - q.mu.Lock() - defer q.mu.Unlock() - items := make([]map[string]any, len(q.items)) - for i, item := range q.items { - messageID := "message-" + item.id - items[i] = map[string]any{ - "agentMode": item.agentMode, - "displayText": item.displayText, - "id": item.id, - "kind": "message", - "messageId": messageID, - } - } - return mustJSON(q.t, map[string]any{ - "items": items, - "steeringMessages": append([]string(nil), q.steeringMessages...), - }), nil -} - -func (q *scenarioQueue) sendNow(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - ID string `json:"id"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - q.t.Errorf("Unmarshal sendNow: %v", err) - } - q.mu.Lock() - defer q.mu.Unlock() - for i, item := range q.items { - if item.id == params.ID { - q.items = append(q.items[:i], q.items[i+1:]...) - q.steeringMessages = append(q.steeringMessages, item.displayText) - return json.RawMessage(`{"steered":true}`), nil - } - } - return json.RawMessage(`{"steered":false}`), nil -} - -func (q *scenarioQueue) removeAt(request json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var params struct { - ID string `json:"id"` - } - if err := json.Unmarshal(request, ¶ms); err != nil { - q.t.Errorf("Unmarshal removeAt: %v", err) - } - q.mu.Lock() - defer q.mu.Unlock() - for i, item := range q.items { - if item.id == params.ID { - q.items = append(q.items[:i], q.items[i+1:]...) - return json.RawMessage(`{"removed":true}`), nil - } - } - return json.RawMessage(`{"removed":false}`), nil -} diff --git a/go/internal/e2e/session_fs_e2e_test.go b/go/internal/e2e/session_fs_e2e_test.go index 4236e8e529..18d41a46b5 100644 --- a/go/internal/e2e/session_fs_e2e_test.go +++ b/go/internal/e2e/session_fs_e2e_test.go @@ -1,6 +1,7 @@ package e2e import ( + "encoding/base64" "fmt" "os" "path/filepath" @@ -76,6 +77,84 @@ func TestSessionFSE2E(t *testing.T) { } }) + t.Run("should view an image that exists only in the binary session fs provider", func(t *testing.T) { + ctx.ConfigureForTest(t) + + imagePath := "/sdk-provider-image.png" + imageBytes, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==") + if err != nil { + t.Fatal(err) + } + binaryConfig := *sessionFSConfig + binaryConfig.Capabilities = &copilot.SessionFSCapabilities{Binary: true} + binaryClient := ctx.NewClient(func(opts *copilot.ClientOptions) { + opts.SessionFS = &binaryConfig + }) + t.Cleanup(func() { binaryClient.ForceStop() }) + readPaths := make(chan string, 1) + session, err := binaryClient.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CreateSessionFSProvider: func(session *copilot.Session) copilot.SessionFSProvider { + return &testBinarySessionFSHandler{ + testSessionFSHandler: &testSessionFSHandler{root: providerRoot, sessionID: session.SessionID}, + readPaths: readPaths, + } + }, + }) + if err != nil { + t.Fatalf("Failed to create binary session: %v", err) + } + storedPath := p(session.SessionID, imagePath) + if err := os.MkdirAll(filepath.Dir(storedPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(storedPath, imageBytes, 0o644); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(imagePath); !os.IsNotExist(err) { + t.Fatalf("Image unexpectedly exists on the runtime filesystem: %v", err) + } + + msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + }) + if err != nil { + t.Fatalf("Failed to view provider image: %v", err) + } + if msg == nil { + t.Fatal("Expected a final assistant response") + } + response, ok := msg.Data.(*copilot.AssistantMessageData) + if !ok || !strings.Contains(response.Content, "SDK_PROVIDER_IMAGE_DONE") { + t.Fatalf("Expected final image response, got %#v", msg) + } + select { + case path := <-readPaths: + if path != imagePath { + t.Fatalf("Expected binary read of %q, got %q", imagePath, path) + } + default: + t.Fatal("Expected a binary read from the provider") + } + events, err := session.GetEvents(t.Context()) + if err != nil { + t.Fatalf("Failed to read session events: %v", err) + } + foundAsset := false + for _, event := range events { + if asset, ok := event.Data.(*copilot.SessionBinaryAssetData); ok && + asset.MIMEType == "image/png" && asset.Data == base64.StdEncoding.EncodeToString(imageBytes) { + foundAsset = true + } + } + if !foundAsset { + t.Fatal("Expected a binary asset with the exact provider image bytes") + } + if err := session.Disconnect(); err != nil { + t.Fatalf("Failed to disconnect session: %v", err) + } + }) + t.Run("should load session data from fs provider on resume", func(t *testing.T) { ctx.ConfigureForTest(t) @@ -346,6 +425,31 @@ type testSessionFSHandler struct { sessionID string } +type testBinarySessionFSHandler struct { + *testSessionFSHandler + readPaths chan<- string +} + +func (h *testBinarySessionFSHandler) ReadFileBytes(path string) ([]byte, error) { + select { + case h.readPaths <- path: + default: + } + return os.ReadFile(providerPath(h.root, h.sessionID, path)) +} + +func (h *testBinarySessionFSHandler) WriteFileBytes(path string, content []byte, mode *int) error { + fullPath := providerPath(h.root, h.sessionID, path) + if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil { + return err + } + perm := os.FileMode(0o666) + if mode != nil { + perm = os.FileMode(*mode) + } + return os.WriteFile(fullPath, content, perm) +} + func (h *testSessionFSHandler) ReadFile(path string) (string, error) { content, err := os.ReadFile(providerPath(h.root, h.sessionID, path)) if err != nil { diff --git a/go/internal/e2e/set_tools_e2e_test.go b/go/internal/e2e/set_tools_e2e_test.go new file mode 100644 index 0000000000..75b818fa9d --- /dev/null +++ b/go/internal/e2e/set_tools_e2e_test.go @@ -0,0 +1,311 @@ +package e2e + +import ( + "fmt" + "slices" + "strings" + "sync" + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +const ( + fruitPrompt = "Use lookup_fruit to find the fruit for code 42." + fruitAndVegetablePrompt = "Use lookup_fruit to find the fruit for code 42 again, and use lookup_vegetable to find the vegetable for code 7." + vegetablePrompt = "Use lookup_vegetable to find the vegetable for code 7." +) + +func TestSetToolsE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(client.ForceStop) + + t.Run("replaces_tools_on_a_created_session", func(t *testing.T) { + ctx.ConfigureSnapshot(t, "set_tools/replaces_tools_on_a_created_session") + var originalLookups, replacementLookups, vegetableLookups toolCallRecorder + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{ + lookupFruitTool("apple", &originalLookups), + retiredLookupTool(), + }, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + first, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitPrompt}) + if err != nil { + t.Fatalf("first SendAndWait failed: %v", err) + } + assertAssistantContains(t, first, "apple") + + if err := session.SetTools(t.Context(), []copilot.Tool{ + lookupFruitTool("dragonfruit", &replacementLookups), + lookupVegetableTool(&vegetableLookups), + }); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + + second, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitAndVegetablePrompt}) + if err != nil { + t.Fatalf("second SendAndWait failed: %v", err) + } + assertAssistantContains(t, second, "dragonfruit") + assertAssistantContains(t, second, "carrot") + assertCalls(t, "original fruit lookups", originalLookups.calls(), []int{42}) + assertCalls(t, "replacement fruit lookups", replacementLookups.calls(), []int{42}) + assertCalls(t, "vegetable lookups", vegetableLookups.calls(), []int{7}) + + // Model requests after the replacement offer exactly the new tool set. + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("GetExchanges failed: %v", err) + } + replacedFrom := indexOfPrompt(exchanges, fruitAndVegetablePrompt) + if replacedFrom <= 0 { + t.Fatalf("replacement prompt first sent in exchange %d, want an earlier exchange before it", replacedFrom) + } + assertOffered(t, exchanges[:replacedFrom], []string{"lookup_fruit", "retired_lookup"}, []string{"lookup_vegetable"}) + assertOffered(t, exchanges[replacedFrom:], []string{"lookup_fruit", "lookup_vegetable"}, []string{"retired_lookup"}) + }) + + t.Run("replaces_tools_on_a_resumed_session", func(t *testing.T) { + ctx.ConfigureSnapshot(t, "set_tools/replaces_tools_on_a_resumed_session") + var createdLookups toolCallRecorder + created, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{lookupFruitTool("apple", &createdLookups)}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + sessionID := created.SessionID + first, err := created.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitPrompt}) + if err != nil { + t.Fatalf("first SendAndWait failed: %v", err) + } + assertAssistantContains(t, first, "apple") + assertCalls(t, "created fruit lookups", createdLookups.calls(), []int{42}) + if err := created.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + + var fruitLookups, vegetableLookups toolCallRecorder + resumed, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{lookupFruitTool("apple", &fruitLookups)}, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = resumed.Disconnect() }) + if err := resumed.SetTools(t.Context(), []copilot.Tool{lookupVegetableTool(&vegetableLookups)}); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + + answer, err := resumed.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: vegetablePrompt}) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + assertAssistantContains(t, answer, "carrot") + assertCalls(t, "vegetable lookups", vegetableLookups.calls(), []int{7}) + assertCalls(t, "fruit lookups", fruitLookups.calls(), nil) + + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("GetExchanges failed: %v", err) + } + replacedFrom := indexOfPrompt(exchanges, vegetablePrompt) + if replacedFrom <= 0 { + t.Fatalf("replacement prompt first sent in exchange %d, want an earlier exchange before it", replacedFrom) + } + assertOffered(t, exchanges[replacedFrom:], []string{"lookup_vegetable"}, []string{"lookup_fruit"}) + }) + + t.Run("keeps_the_previous_tools_when_a_replacement_is_rejected", func(t *testing.T) { + ctx.ConfigureSnapshot(t, "set_tools/keeps_the_previous_tools_when_a_replacement_is_rejected") + var originalLookups, replacementLookups toolCallRecorder + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{lookupFruitTool("apple", &originalLookups)}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + err = session.SetTools(t.Context(), []copilot.Tool{ + lookupFruitTool("dragonfruit", &replacementLookups), + invalidTool(), + }) + if err == nil { + t.Fatal("SetTools succeeded; want runtime rejection") + } + + answer, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitPrompt}) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + assertAssistantContains(t, answer, "apple") + assertCalls(t, "original fruit lookups", originalLookups.calls(), []int{42}) + assertCalls(t, "replacement fruit lookups", replacementLookups.calls(), nil) + }) +} + +type toolCallRecorder struct { + mu sync.Mutex + codes []int +} + +func (r *toolCallRecorder) record(code int) { + if r == nil { + return + } + r.mu.Lock() + defer r.mu.Unlock() + r.codes = append(r.codes, code) +} + +func (r *toolCallRecorder) calls() []int { + r.mu.Lock() + defer r.mu.Unlock() + return append([]int(nil), r.codes...) +} + +func lookupFruitTool(fruit string, calls *toolCallRecorder) copilot.Tool { + return numericCodeTool("lookup_fruit", "Looks up the fruit for a numeric code", "Fruit code", func(code int) string { + calls.record(code) + return fruit + }) +} + +func lookupVegetableTool(calls *toolCallRecorder) copilot.Tool { + return numericCodeTool("lookup_vegetable", "Looks up the vegetable for a numeric code", "Vegetable code", func(code int) string { + calls.record(code) + return "carrot" + }) +} + +func retiredLookupTool() copilot.Tool { + return copilot.Tool{ + Name: "retired_lookup", + Description: "Looks up a retired value", + Handler: func(_ copilot.ToolInvocation) (copilot.ToolResult, error) { + return copilot.ToolResult{TextResultForLLM: "retired"}, nil + }, + } +} + +func invalidTool() copilot.Tool { + return copilot.Tool{ + Name: "invalid.tool", + Description: "Has a name the runtime rejects", + Handler: func(_ copilot.ToolInvocation) (copilot.ToolResult, error) { + return copilot.ToolResult{TextResultForLLM: "never"}, nil + }, + } +} + +func numericCodeTool(name, description, codeDescription string, handler func(int) string) copilot.Tool { + return copilot.Tool{ + Name: name, + Description: description, + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "code": map[string]any{ + "type": "integer", + "description": codeDescription, + }, + }, + "required": []string{"code"}, + }, + Handler: func(inv copilot.ToolInvocation) (copilot.ToolResult, error) { + code, err := codeArgument(inv.Arguments) + if err != nil { + return copilot.ToolResult{}, err + } + return copilot.ToolResult{TextResultForLLM: handler(code)}, nil + }, + } +} + +func codeArgument(arguments any) (int, error) { + args, ok := arguments.(map[string]any) + if !ok { + return 0, fmt.Errorf("arguments = %#v, want object", arguments) + } + switch code := args["code"].(type) { + case float64: + return int(code), nil + case int: + return code, nil + default: + return 0, fmt.Errorf("code argument = %#v, want number", args["code"]) + } +} + +func assertAssistantContains(t *testing.T, event *copilot.SessionEvent, want string) { + t.Helper() + if event == nil { + t.Fatalf("assistant response is nil, want content containing %q", want) + } + data, ok := event.Data.(*copilot.AssistantMessageData) + if !ok { + t.Fatalf("event data = %T, want AssistantMessageData", event.Data) + } + if !strings.Contains(data.Content, want) { + t.Fatalf("assistant response = %q, want content containing %q", data.Content, want) + } +} + +func assertCalls(t *testing.T, label string, got, want []int) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("%s = %v, want %v", label, got, want) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("%s = %v, want %v", label, got, want) + } + } +} + +// indexOfPrompt returns the index of the first model request that carries +// prompt as a user message, or -1. +func indexOfPrompt(exchanges []testharness.ParsedHttpExchange, prompt string) int { + for i, exchange := range exchanges { + for _, message := range exchange.Request.Messages { + if message.Role == "user" && (strings.Contains(message.Content, prompt) || strings.Contains(string(message.RawContent), prompt)) { + return i + } + } + } + return -1 +} + +// assertOffered checks that every exchange offered the tools in offered and +// none of the tools in notOffered. +func assertOffered(t *testing.T, exchanges []testharness.ParsedHttpExchange, offered, notOffered []string) { + t.Helper() + for _, exchange := range exchanges { + tools := make([]string, 0, len(exchange.Request.Tools)) + for _, tool := range exchange.Request.Tools { + tools = append(tools, tool.Function.Name) + } + for _, name := range offered { + if !slices.Contains(tools, name) { + t.Fatalf("model request offered %v, want it to include %q", tools, name) + } + } + for _, name := range notOffered { + if slices.Contains(tools, name) { + t.Fatalf("model request offered %v, want it to omit %q", tools, name) + } + } + } +} diff --git a/go/internal/e2e/skill_provider_e2e_test.go b/go/internal/e2e/skill_provider_e2e_test.go new file mode 100644 index 0000000000..d6c2dc93fb --- /dev/null +++ b/go/internal/e2e/skill_provider_e2e_test.go @@ -0,0 +1,586 @@ +package e2e + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +type providedSkill struct { + descriptor rpc.SkillProviderDescriptor + read func() (string, error) +} + +type testSkillProvider struct { + mu sync.Mutex + skills []providedSkill + calls []string +} + +func newTestSkillProvider(skills []providedSkill) *testSkillProvider { + return &testSkillProvider{skills: skills} +} + +func providedSkillWithMarkdown(name, description, markdown string) providedSkill { + return providedSkill{ + descriptor: rpc.SkillProviderDescriptor{Name: name, Description: description}, + read: func() (string, error) { return markdown, nil }, + } +} + +func (p *testSkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "list") + skills := make([]rpc.SkillProviderDescriptor, 0, len(p.skills)) + for _, skill := range p.skills { + skills = append(skills, skill.descriptor) + } + return skills, nil +} + +func (p *testSkillProvider) ReadSkill(ctx context.Context, name string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "read:"+name) + for _, skill := range p.skills { + if skill.descriptor.Name == name { + return skill.read() + } + } + return "", copilot.ErrSkillNotFound +} + +func (p *testSkillProvider) Calls() []string { + p.mu.Lock() + defer p.mu.Unlock() + calls := make([]string, len(p.calls)) + copy(calls, p.calls) + return calls +} + +func (p *testSkillProvider) Reads() []string { + calls := p.Calls() + reads := make([]string, 0, len(calls)) + for _, call := range calls { + if strings.HasPrefix(call, "read:") { + reads = append(reads, strings.TrimPrefix(call, "read:")) + } + } + return reads +} + +// blockingSkillProvider blocks ListSkills until its context is cancelled. +type blockingSkillProvider struct { + entered chan struct{} + cancelled chan struct{} + enterOnce sync.Once + cancelOnce sync.Once +} + +func newBlockingSkillProvider() *blockingSkillProvider { + return &blockingSkillProvider{entered: make(chan struct{}), cancelled: make(chan struct{})} +} + +func (p *blockingSkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.enterOnce.Do(func() { close(p.entered) }) + <-ctx.Done() + p.cancelOnce.Do(func() { close(p.cancelled) }) + return nil, ctx.Err() +} + +func (p *blockingSkillProvider) ReadSkill(context.Context, string) (string, error) { + return "", copilot.ErrSkillNotFound +} + +func TestSkillProviderE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + t.Run("should load provider skill lazily through skill tool", func(t *testing.T) { + ctx.ConfigureForTest(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n", + ), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + listed := findSkill(list, "provider-lookup") + if listed == nil { + t.Fatal("Expected provider-lookup skill to be listed") + } + if listed.Source != rpc.SkillSourceSDK || !listed.Enabled { + t.Fatalf("Expected provider-lookup source=sdk enabled=true, got %+v", listed) + } + if listed.Path != nil && *listed.Path != "" { + t.Fatalf("Expected provider-lookup path to be empty, got %q", *listed.Path) + } + if reads := provider.Reads(); len(reads) != 0 { + t.Fatalf("Provider reads before skill load = %v, want []", reads) + } + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, provider.Reads(), []string{"provider-lookup"}) + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "TANGERINE_QUARTZ_19") + }) + + t.Run("should load provider and file based skills together", func(t *testing.T) { + ctx.ConfigureForTest(t) + skillsDir := filepath.Join(ctx.WorkDir, "file-skills") + fileSkillDir := filepath.Join(skillsDir, "file-notes") + if err := os.MkdirAll(fileSkillDir, 0755); err != nil { + t.Fatalf("MkdirAll failed: %v", err) + } + if err := os.WriteFile( + filepath.Join(fileSkillDir, "SKILL.md"), + []byte("---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n"), + 0644, + ); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n", + ), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillDirectories: []string{skillsDir}, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + fileSkill := findSkill(list, "file-notes") + providerSkill := findSkill(list, "provider-audit") + if fileSkill == nil || fileSkill.Source == rpc.SkillSourceSDK || fileSkill.Path == nil || *fileSkill.Path == "" { + t.Fatalf("Unexpected file-notes skill: %+v", fileSkill) + } + if providerSkill == nil || providerSkill.Source != rpc.SkillSourceSDK { + t.Fatalf("Unexpected provider-audit skill: %+v", providerSkill) + } + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, provider.Reads(), []string{"provider-audit"}) + assertAssistantContains(t, message, "MAPLE_FALCON_27") + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "COBALT_HERON_58") + }) + + t.Run("should rebind skill provider on resume", func(t *testing.T) { + ctx.ConfigureForTest(t) + original := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n", + ), + }) + replacement := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n", + ), + }) + first, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: original, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + sessionID := first.SessionID + if _, err := first.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Without using any tools or skills, reply with exactly REBIND_READY.", + }); err != nil { + t.Fatalf("Initial SendAndWait failed: %v", err) + } + if err := first.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + if reads := original.Reads(); len(reads) != 0 { + t.Fatalf("Original provider reads before resume = %v, want []", reads) + } + originalCallsBeforeResume := len(original.Calls()) + + session, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: replacement, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, replacement.Reads(), []string{"rebind-check"}) + if got := len(original.Calls()); got != originalCallsBeforeResume { + t.Fatalf("Original provider call count after resume = %d, want %d", got, originalCallsBeforeResume) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "BRONZE_BETA_22") + assertAssistantNotContains(t, message, "AMBER_ALPHA_11") + }) + + t.Run("should report provider read failure without leaking details", func(t *testing.T) { + ctx.ConfigureForTest(t) + secret := "PROVIDER_SECRET_7F3A9C" + provider := newTestSkillProvider([]providedSkill{ + { + descriptor: rpc.SkillProviderDescriptor{ + Name: "broken-lookup", + Description: "Reports the broken lookup verification word.", + }, + read: func() (string, error) { + return "", errors.New("database unavailable: " + secret) + }, + }, + }) + events := newEventRecorder() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + OnEvent: events.Record, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertContainsString(t, provider.Reads(), "broken-lookup") + failures := failedToolCompletions(events.Snapshot()) + if len(failures) != 1 { + t.Fatalf("Expected exactly 1 failed tool completion, got %d", len(failures)) + } + rawEvents, err := json.Marshal(events.Snapshot()) + if err != nil { + t.Fatalf("Marshal events failed: %v", err) + } + if strings.Contains(string(rawEvents), secret) { + t.Fatalf("Provider error leaked secret in events: %s", rawEvents) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "LOAD_FAILED") + }) + + t.Run("should report missing provider skill as not found", func(t *testing.T) { + ctx.ConfigureForTest(t) + provider := newTestSkillProvider([]providedSkill{ + { + descriptor: rpc.SkillProviderDescriptor{ + Name: "vanished-lookup", + Description: "Reports the vanished lookup verification word.", + }, + read: func() (string, error) { + return "", copilot.ErrSkillNotFound + }, + }, + }) + events := newEventRecorder() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + OnEvent: events.Record, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertContainsString(t, provider.Reads(), "vanished-lookup") + failures := failedToolCompletions(events.Snapshot()) + if len(failures) != 1 { + t.Fatalf("Expected exactly 1 failed tool completion, got %d", len(failures)) + } + rawFailure, err := json.Marshal(failures[0]) + if err != nil { + t.Fatalf("Marshal failure failed: %v", err) + } + if !strings.Contains(strings.ToLower(string(rawFailure)), "not found") { + t.Fatalf("Expected failure to mention not found, got %s", rawFailure) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "LOAD_FAILED") + }) + + t.Run("should keep provider dormant when skills disabled", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("dormant-lookup", "Never listed.", "Never read.\n"), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + EnableSkills: copilot.Bool(false), + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + if _, err := session.RPC.Skills.EnsureLoaded(t.Context()); err != nil { + t.Fatalf("Skills.EnsureLoaded failed: %v", err) + } + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + if sdkSkills := sdkSkills(list); len(sdkSkills) != 0 { + t.Fatalf("Expected no sdk skills when disabled, got %+v", sdkSkills) + } + if calls := provider.Calls(); len(calls) != 0 { + t.Fatalf("Provider calls when skills disabled = %v, want []", calls) + } + }) + + t.Run("should unbind provider when resumed without one", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n"), + }) + first, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + before, err := first.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List before resume failed: %v", err) + } + if findSkill(before, "unbound-lookup") == nil { + t.Fatal("Expected unbound-lookup before resume") + } + callsBeforeResume := len(provider.Calls()) + + session, err := client.ResumeSession(t.Context(), first.SessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + if _, err := session.RPC.Skills.Reload(t.Context()); err != nil { + t.Fatalf("Skills.Reload failed: %v", err) + } + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List after resume failed: %v", err) + } + if sdkSkills := sdkSkills(list); len(sdkSkills) != 0 { + t.Fatalf("Expected no sdk skills after unbound resume, got %+v", sdkSkills) + } + if got := len(provider.Calls()); got != callsBeforeResume { + t.Fatalf("Provider call count after unbound resume = %d, want %d", got, callsBeforeResume) + } + }) + + t.Run("should cancel a blocked provider call when the session disconnects", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newBlockingSkillProvider() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + go func() { _, _ = session.RPC.Skills.List(context.Background()) }() + select { + case <-provider.entered: + case <-time.After(30 * time.Second): + t.Fatal("provider ListSkills was not called") + } + + if err := session.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + select { + case <-provider.cancelled: + case <-time.After(10 * time.Second): + t.Fatal("provider context was not cancelled after Disconnect") + } + }) + + t.Run("should reject skill provider for cloud sessions", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("cloud-lookup", "Never listed.", "Never read.\n"), + }) + + _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Cloud: &copilot.CloudSessionOptions{}, + SkillProvider: provider, + }) + if err == nil || err.Error() != "Skill providers are not supported for cloud sessions." { + t.Fatalf("CreateSession error = %v, want cloud skill provider rejection", err) + } + if calls := provider.Calls(); len(calls) != 0 { + t.Fatalf("Provider calls after cloud rejection = %v, want []", calls) + } + }) +} + +type eventRecorder struct { + mu sync.Mutex + events []copilot.SessionEvent +} + +func newEventRecorder() *eventRecorder { + return &eventRecorder{} +} + +func (r *eventRecorder) Record(event copilot.SessionEvent) { + r.mu.Lock() + defer r.mu.Unlock() + r.events = append(r.events, event) +} + +func (r *eventRecorder) Snapshot() []copilot.SessionEvent { + r.mu.Lock() + defer r.mu.Unlock() + events := make([]copilot.SessionEvent, len(r.events)) + copy(events, r.events) + return events +} + +func findSkill(list *rpc.SkillList, name string) *rpc.Skill { + if list == nil { + return nil + } + for i := range list.Skills { + if list.Skills[i].Name == name { + return &list.Skills[i] + } + } + return nil +} + +func sdkSkills(list *rpc.SkillList) []rpc.Skill { + if list == nil { + return nil + } + var matches []rpc.Skill + for _, skill := range list.Skills { + if skill.Source == rpc.SkillSourceSDK { + matches = append(matches, skill) + } + } + return matches +} + +func failedToolCompletions(events []copilot.SessionEvent) []*copilot.ToolExecutionCompleteData { + var failures []*copilot.ToolExecutionCompleteData + for _, event := range events { + if data, ok := event.Data.(*copilot.ToolExecutionCompleteData); ok && !data.Success { + failures = append(failures, data) + } + } + return failures +} + +func assertAssistantNotContains(t *testing.T, event *copilot.SessionEvent, text string) { + t.Helper() + data, ok := event.Data.(*copilot.AssistantMessageData) + if !ok { + t.Fatalf("Expected AssistantMessageData, got %T", event.Data) + } + if strings.Contains(data.Content, text) { + t.Fatalf("Expected assistant response not to contain %q, got %q", text, data.Content) + } +} + +func assertEqualStrings(t *testing.T, got, want []string) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("strings = %v, want %v", got, want) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("strings = %v, want %v", got, want) + } + } +} + +func assertContainsString(t *testing.T, got []string, want string) { + t.Helper() + for _, value := range got { + if value == want { + return + } + } + t.Fatalf("strings = %v, want to contain %q", got, want) +} diff --git a/go/internal/e2e/subagent_hooks_e2e_test.go b/go/internal/e2e/subagent_hooks_e2e_test.go index 0e2fde9f86..f74cb6355d 100644 --- a/go/internal/e2e/subagent_hooks_e2e_test.go +++ b/go/internal/e2e/subagent_hooks_e2e_test.go @@ -1,36 +1,52 @@ package e2e import ( + "bytes" + "encoding/json" + "io" "net/http" "os" "path/filepath" + "strings" "sync" "testing" + "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/google/uuid" ) +const childContext = "Subagent start hook verified: read the requested file." +const stopResponsePrefix = "Subagent stop hook verified: " + type subagentRequestRecord struct { agentID string parentAgentID string interactionType string + childPrompt string } type recordingForwardingTransport struct { - inner http.RoundTripper - mu sync.Mutex - records []subagentRequestRecord + inner http.RoundTripper + mu sync.Mutex + records []subagentRequestRecord + modifiedParentRequest chan struct{} } func newRecordingForwardingTransport() *recordingForwardingTransport { inner := http.DefaultTransport.(*http.Transport).Clone() inner.DisableCompression = true - return &recordingForwardingTransport{inner: inner} + return &recordingForwardingTransport{inner: inner, modifiedParentRequest: make(chan struct{}, 1)} } func (rt *recordingForwardingTransport) RoundTrip(req *http.Request) (*http.Response, error) { if isInferenceURL(req.URL.String()) { + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + req.Body = io.NopCloser(bytes.NewReader(body)) rctx := copilot.RequestContextFrom(req) record := subagentRequestRecord{} if rctx != nil { @@ -38,6 +54,50 @@ func (rt *recordingForwardingTransport) RoundTrip(req *http.Request) (*http.Resp record.parentAgentID = rctx.ParentAgentID record.interactionType = rctx.InteractionType } + if record.parentAgentID != "" { + var payload struct { + Messages []struct { + Role string `json:"role"` + Content json.RawMessage `json:"content"` + } `json:"messages"` + } + if err := json.Unmarshal(body, &payload); err != nil { + return nil, err + } + for _, message := range payload.Messages { + if message.Role == "user" && len(message.Content) > 0 { + var prompts []string + switch message.Content[0] { + case '"': + var prompt string + if err := json.Unmarshal(message.Content, &prompt); err != nil { + return nil, err + } + prompts = append(prompts, prompt) + case '[': + var parts []struct { + Text string `json:"text"` + } + if err := json.Unmarshal(message.Content, &parts); err != nil { + return nil, err + } + for _, part := range parts { + prompts = append(prompts, part.Text) + } + } + for _, prompt := range prompts { + if strings.Contains(prompt, childContext+"\n\n") { + record.childPrompt = prompt + } + } + } + } + } else if bytes.Contains(body, []byte(stopResponsePrefix)) { + select { + case rt.modifiedParentRequest <- struct{}{}: + default: + } + } rt.mu.Lock() rt.records = append(rt.records, record) rt.mu.Unlock() @@ -86,7 +146,7 @@ func TestSubagentHooksE2E(t *testing.T) { }) t.Cleanup(func() { client.ForceStop() }) - t.Run("should invoke preToolUse and postToolUse hooks for sub-agent tool calls", func(t *testing.T) { + t.Run("should apply subagent lifecycle hook outputs", func(t *testing.T) { ctx.ConfigureForTest(t) type hookEntry struct { @@ -96,8 +156,20 @@ func TestSubagentHooksE2E(t *testing.T) { } var hookLog []hookEntry var mu sync.Mutex + 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```" + parentSessionID := uuid.NewString() + parentWaiting := make(chan struct{}) + releaseView := sync.OnceFunc(func() { close(parentWaiting) }) + defer releaseView() + var startInputs []copilot.SubagentStartHookInput + var stopInputs []copilot.SubagentStopHookInput + var startInvocations []copilot.HookInvocation + var stopInvocations []copilot.HookInvocation + stopObserved := make(chan struct{}, 1) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + SessionID: parentSessionID, OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Hooks: &copilot.SessionHooks{ OnPreToolUse: func(input copilot.PreToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { @@ -110,29 +182,129 @@ func TestSubagentHooksE2E(t *testing.T) { mu.Lock() hookLog = append(hookLog, hookEntry{kind: "post", toolName: input.ToolName, sessionID: input.SessionID}) mu.Unlock() + // A fast child can inject its result before the fixture's waiting reply is requested. + if input.ToolName == "view" && input.SessionID != parentSessionID { + select { + case <-parentWaiting: + case <-t.Context().Done(): + return nil, t.Context().Err() + } + } return nil, nil }, + OnSubagentStart: func(input copilot.SubagentStartHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStartHookOutput, error) { + mu.Lock() + startInputs = append(startInputs, input) + startInvocations = append(startInvocations, invocation) + mu.Unlock() + return &copilot.SubagentStartHookOutput{AdditionalContext: childContext}, nil + }, + OnSubagentStop: func(input copilot.SubagentStopHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStopHookOutput, error) { + mu.Lock() + stopInputs = append(stopInputs, input) + stopInvocations = append(stopInvocations, invocation) + mu.Unlock() + select { + case stopObserved <- struct{}{}: + default: + } + return &copilot.SubagentStopHookOutput{ModifiedResponse: copilot.String(stopResponsePrefix + input.Response)}, nil + }, }, }) if err != nil { t.Fatalf("Failed to create session: %v", err) } - // Create a file for the sub-agent to read testFile := filepath.Join(ctx.WorkDir, "subagent-test.txt") if err := os.WriteFile(testFile, []byte("Hello from subagent test!"), 0644); err != nil { t.Fatalf("Failed to write test file: %v", err) } - _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{ + unsubscribe := session.On(func(event copilot.SessionEvent) { + if message, ok := event.Data.(*copilot.AssistantMessageData); ok && (event.AgentID == nil || *event.AgentID == "") && message.Content == waitingText { + releaseView() + } + }) + defer unsubscribe() + response, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ 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.", }) if err != nil { t.Fatalf("Failed to send message: %v", err) } + if response == nil { + t.Fatal("Missing parent final response") + } + message, ok := response.Data.(*copilot.AssistantMessageData) + if !ok || (response.AgentID != nil && *response.AgentID != "") || message.Content != finalText { + t.Fatalf("Unexpected parent final response: %+v", response) + } + events, err := session.GetEvents(t.Context()) + if err != nil { + t.Fatalf("Failed to read history: %v", err) + } + var replies []string + for _, event := range events { + if message, ok := event.Data.(*copilot.AssistantMessageData); ok && (event.AgentID == nil || *event.AgentID == "") && (message.Content == waitingText || message.Content == finalText) { + replies = append(replies, message.Content) + } + } + if len(replies) != 2 || replies[0] != waitingText || replies[1] != finalText { + t.Fatalf("Expected durable waiting reply followed by final reply, got %q", replies) + } + select { + case <-stopObserved: + case <-time.After(120 * time.Second): + t.Fatal("Timed out waiting for the subagentStop hook") + } + select { + case <-transport.modifiedParentRequest: + case <-time.After(120 * time.Second): + t.Fatal("Timed out waiting for the parent inference request with the rewritten response") + } mu.Lock() defer mu.Unlock() + if len(startInputs) != 1 || len(stopInputs) != 1 { + t.Fatalf("Expected one subagentStart and one subagentStop invocation, got starts=%+v stops=%+v", startInputs, stopInputs) + } + start, stop := startInputs[0], stopInputs[0] + if startInvocations[0].SessionID != session.SessionID || stopInvocations[0].SessionID != session.SessionID || + start.SessionID != session.SessionID || stop.SessionID != session.SessionID { + t.Errorf("Expected parent session ID %q in hook inputs and invocations, got start=%+v stop=%+v", session.SessionID, start, stop) + } + if !start.Timestamp.After(time.UnixMilli(0)) || !stop.Timestamp.After(time.UnixMilli(0)) || + stop.Timestamp.Before(start.Timestamp) { + t.Errorf("Expected ordered timestamps, got start=%v stop=%v", start.Timestamp, stop.Timestamp) + } + if start.WorkingDirectory != ctx.WorkDir || stop.WorkingDirectory != ctx.WorkDir { + t.Errorf("Expected working directory %q, got start=%q stop=%q", ctx.WorkDir, start.WorkingDirectory, stop.WorkingDirectory) + } + if stop.TranscriptPath != start.TranscriptPath || (start.TranscriptPath != "" && !filepath.IsAbs(start.TranscriptPath)) { + t.Errorf("Expected matching transcript paths (absolute when available), got start=%q stop=%q", start.TranscriptPath, stop.TranscriptPath) + } + if start.AgentName != "explore" || stop.AgentName != start.AgentName || stop.AgentType != "explore" { + t.Errorf("Expected explore agent name and type, got start=%+v stop=%+v", start, stop) + } + if start.AgentDisplayName != "" { + t.Errorf("Expected no optional display name on this task-created agent, got %q", start.AgentDisplayName) + } + if stop.AgentDisplayName != start.AgentDisplayName { + t.Errorf("Expected stop display name %q, got %q", start.AgentDisplayName, stop.AgentDisplayName) + } + if start.AgentDescription != "" { + t.Errorf("Expected no optional description on this task-created agent, got %q", start.AgentDescription) + } + if stop.AgentDescription != start.AgentDescription { + t.Errorf("Expected stop description %q, got %q", start.AgentDescription, stop.AgentDescription) + } + if stop.AgentID == "" { + t.Error("Expected a runtime-generated sub-agent ID") + } + if stop.StopReason != "end_turn" || !strings.Contains(stop.Response, "Hello from subagent test!") { + t.Errorf("Expected completed turn and file contents in stop input, got %+v", stop) + } // Parent tool hooks fire for "task" var taskPre *hookEntry @@ -169,6 +341,16 @@ func TestSubagentHooksE2E(t *testing.T) { if viewPre[0].sessionID == taskPre.sessionID { t.Error("Sub-agent tool hooks should have a different sessionId than parent tool hooks") } - assertSubagentRequestMetadata(t, transport.inferenceRecords()) + requests := transport.inferenceRecords() + assertSubagentRequestMetadata(t, requests) + startContextObserved := false + for _, request := range requests { + if strings.Contains(request.childPrompt, childContext+"\n\nRead the file \"subagent-test.txt\"") { + startContextObserved = true + } + } + if !startContextObserved { + t.Errorf("Start hook context did not reach a child inference prompt: %+v", requests) + } }) } diff --git a/go/internal/e2e/telemetry_e2e_test.go b/go/internal/e2e/telemetry_e2e_test.go index 6c03d83819..0958beb1c7 100644 --- a/go/internal/e2e/telemetry_e2e_test.go +++ b/go/internal/e2e/telemetry_e2e_test.go @@ -174,6 +174,163 @@ func TestTelemetryE2E(t *testing.T) { t.Errorf("Expected gen_ai.tool.call.result=%q, got %q", marker, got) } }) + + t.Run("should export per request subagent chat spans", func(t *testing.T) { + ctx := testharness.NewTestContext(t) + ctx.ConfigureForTest(t) + telemetryPath := filepath.Join(ctx.WorkDir, fmt.Sprintf("telemetry-%s.jsonl", randomHex(t))) + const sourceName = "go-sdk-subagent-telemetry-e2e" + const prompt = "Use the task tool in sync mode to ask a task agent to read subagent-otel.txt with the view tool. Then reply with SUBAGENT_OTEL_DONE." + if err := os.WriteFile(filepath.Join(ctx.WorkDir, "subagent-otel.txt"), []byte("SUBAGENT_OTEL_FILE_CONTENT"), 0600); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + client := ctx.NewClient(func(opts *copilot.ClientOptions) { + opts.Telemetry = &copilot.TelemetryConfig{ + FilePath: telemetryPath, ExporterType: "file", SourceName: sourceName, + CaptureContent: copilot.Bool(true), + } + }) + t.Cleanup(func() { client.ForceStop() }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + sessionID := session.SessionID + finalMessage := testharness.SubscribeToFinalAssistantMessage(session) + defer finalMessage.Close() + if _, err := session.Send(t.Context(), copilot.MessageOptions{Prompt: prompt}); err != nil { + t.Fatalf("Send failed: %v", err) + } + final, err := finalMessage.Wait(t.Context()) + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + assistant, ok := final.Data.(*copilot.AssistantMessageData) + if !ok || !strings.Contains(assistant.Content, "SUBAGENT_OTEL_DONE") { + t.Fatalf("Expected final response with SUBAGENT_OTEL_DONE, got %v", final.Data) + } + session.Disconnect() + if err := client.Stop(); err != nil { + t.Fatalf("Stop failed: %v", err) + } + + entries, err := readTelemetryEntries(t, telemetryPath) + if err != nil { + t.Fatalf("readTelemetryEntries failed: %v", err) + } + var spans []map[string]any + for _, entry := range entries { + if telemetryType(entry) == "span" { + spans = append(spans, entry) + } + } + for _, span := range spans { + if instrumentationScopeName(span) != sourceName || statusCode(span) == 2 { + t.Errorf("Unexpected source or error span: %v", span) + } + } + invocations := spansWithOperation(spans, "invoke_agent") + if len(invocations) != 2 { + t.Fatalf("Expected two invocation spans, got %d: %v", len(invocations), invocations) + } + var roots []map[string]any + for _, span := range invocations { + if isRootSpan(span) { + roots = append(roots, span) + } + } + root := requireSingleSpan(t, roots, "root invocation") + if got := stringAttr(root, "gen_ai.conversation.id"); got != sessionID { + t.Errorf("Expected conversation id %q, got %q", sessionID, got) + } + rootID, traceID := stringProp(root, "spanId"), stringProp(root, "traceId") + if rootID == "" || traceID == "" { + t.Fatalf("Root missing span/trace id: %v", root) + } + var tasks []map[string]any + for _, span := range spansWithOperation(spans, "execute_tool") { + if stringAttr(span, "gen_ai.tool.name") == "task" { + tasks = append(tasks, span) + } + } + task := requireSingleSpan(t, tasks, "task tool") + if stringProp(task, "parentSpanId") != rootID { + t.Errorf("Task parent is not root: %v", task) + } + var children []map[string]any + for _, span := range invocations { + if stringProp(span, "parentSpanId") == stringProp(task, "spanId") { + children = append(children, span) + } + } + child := requireSingleSpan(t, children, "child invocation") + if stringProp(task, "traceId") != traceID || stringProp(child, "traceId") != traceID { + t.Errorf("Task/child must share root trace: %v %v", task, child) + } + + chats := spansWithOperation(spans, "chat") + if len(chats) != 4 { + t.Fatalf("Expected four chat spans, got %d: %v", len(chats), chats) + } + var parentChats, childChats []map[string]any + for _, chat := range chats { + switch stringProp(chat, "parentSpanId") { + case rootID: + parentChats = append(parentChats, chat) + case stringProp(child, "spanId"): + childChats = append(childChats, chat) + } + } + if len(parentChats) != 2 || len(childChats) != 2 { + t.Fatalf("Expected two chats per agent, got parent=%d child=%d: %v", len(parentChats), len(childChats), chats) + } + for _, chat := range parentChats { + if stringProp(chat, "traceId") != traceID { + t.Errorf("Parent chat must share root trace: %v", chat) + } + } + var viewCount, finalCount int + for _, chat := range childChats { + if stringProp(chat, "traceId") != traceID || stringAttr(chat, "github.copilot.initiator") != "sub-agent" { + t.Errorf("Child chat missing trace or initiator: %v", chat) + } + if strings.Contains(stringAttr(chat, "gen_ai.output.messages"), `"view"`) { + viewCount++ + if strings.Contains(stringAttr(chat, "gen_ai.input.messages"), "SUBAGENT_OTEL_FILE_CONTENT") { + t.Errorf("Requesting child chat contains the later tool result: %v", chat) + } + } + if strings.Contains(stringAttr(chat, "gen_ai.output.messages"), "SUBAGENT_OTEL_CHILD_DONE") { + finalCount++ + if !strings.Contains(stringAttr(chat, "gen_ai.input.messages"), "SUBAGENT_OTEL_FILE_CONTENT") { + t.Errorf("Final child chat missing tool result: %v", chat) + } + } + } + if viewCount != 1 || finalCount != 1 { + t.Errorf("Expected distinct view and final child requests, got view=%d final=%d", viewCount, finalCount) + } + }) +} + +func spansWithOperation(spans []map[string]any, operation string) []map[string]any { + var matching []map[string]any + for _, span := range spans { + if stringAttr(span, "gen_ai.operation.name") == operation { + matching = append(matching, span) + } + } + return matching +} + +func requireSingleSpan(t *testing.T, spans []map[string]any, description string) map[string]any { + t.Helper() + if len(spans) != 1 { + t.Fatalf("Expected exactly one %s span, got %d: %v", description, len(spans), spans) + } + return spans[0] } func readTelemetryEntries(t *testing.T, path string) ([]map[string]any, error) { diff --git a/go/internal/e2e/testharness/ahp.go b/go/internal/e2e/testharness/ahp.go new file mode 100644 index 0000000000..6daa8761c4 --- /dev/null +++ b/go/internal/e2e/testharness/ahp.go @@ -0,0 +1,115 @@ +package testharness + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "sync" + "testing" + "time" +) + +// AhpTestClient runs the standard TypeScript AHP client independently of CAPI replay. +type AhpTestClient struct { + command *exec.Cmd + input io.WriteCloser + output *bufio.Reader + mu sync.Mutex + done chan struct{} + waitErr error +} + +func NewAhpTestClient(t *testing.T) *AhpTestClient { + t.Helper() + command := exec.Command("node", "--import", "tsx", + RepoPath("nodejs", "test", "e2e", "harness", "ahpTestDriver.ts")) + command.Dir = RepoPath("nodejs") + command.Stderr = os.Stderr + input, err := command.StdinPipe() + if err != nil { + t.Fatal(err) + } + output, err := command.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := command.Start(); err != nil { + t.Fatal(err) + } + client := &AhpTestClient{command: command, input: input, output: bufio.NewReader(output), done: make(chan struct{})} + go func() { client.waitErr = command.Wait(); close(client.done) }() + t.Cleanup(func() { + if err := client.input.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + t.Errorf("close AHP test input: %v", err) + } + select { + case <-client.done: + case <-time.After(10 * time.Second): + if err := command.Process.Kill(); err != nil { + t.Errorf("kill stalled AHP test client: %v", err) + } + <-client.done + t.Error("AHP test client did not exit after input closed") + } + if client.waitErr != nil { + t.Errorf("AHP test client exited: %v", client.waitErr) + } + }) + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + data, err := client.read(ctx) + if err != nil { + t.Fatal(err) + } + var ready struct{ Ready bool } + if err := json.Unmarshal(data, &ready); err != nil || !ready.Ready { + t.Fatalf("AHP test client did not become ready: %s (%v)", data, err) + } + return client +} + +func (c *AhpTestClient) read(ctx context.Context) ([]byte, error) { + type response struct { + data []byte + err error + } + done := make(chan response, 1) + go func() { data, err := c.output.ReadBytes('\n'); done <- response{data, err} }() + select { + case result := <-done: + return result.data, result.err + case <-ctx.Done(): + if err := c.command.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) { + return nil, fmt.Errorf("stop timed-out AHP test client: %w", err) + } + return nil, ctx.Err() + } +} + +func (c *AhpTestClient) Request(ctx context.Context, command map[string]any) (json.RawMessage, error) { + c.mu.Lock() + defer c.mu.Unlock() + if err := json.NewEncoder(c.input).Encode(command); err != nil { + return nil, err + } + data, err := c.read(ctx) + if err != nil { + return nil, err + } + var response struct { + Result json.RawMessage `json:"result"` + Error string `json:"error"` + } + if err := json.Unmarshal(data, &response); err != nil { + return nil, err + } + if response.Error != "" { + return nil, fmt.Errorf("AHP test client: %s", response.Error) + } + return response.Result, nil +} diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 5948112437..cc2927954f 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -192,6 +192,7 @@ func NewTestContext(t *testing.T) *TestContext { func defaultCopilotUser(proxyURL string) map[string]interface{} { return map[string]interface{}{ "login": "e2e-test-user", + "id": 12345, "copilot_plan": "individual_pro", "endpoints": map[string]interface{}{ "api": proxyURL, @@ -258,6 +259,14 @@ func (c *TestContext) ConfigureWithoutSnapshot(t *testing.T) { } } +// ConfigureSnapshot selects an existing cross-language replay fixture. +func (c *TestContext) ConfigureSnapshot(t *testing.T, name string) { + t.Helper() + if err := c.proxy.Configure(RepoPath("test", "snapshots", name+".yaml"), c.WorkDir); err != nil { + t.Fatalf("Failed to configure shared snapshot: %v", err) + } +} + // Close cleans up the test context resources. func (c *TestContext) Close(testFailed bool) error { if c.inProcess { diff --git a/go/internal/e2e/testharness/inprocess_isolation.go b/go/internal/e2e/testharness/inprocess_isolation.go index 4803526308..edf61970bb 100644 --- a/go/internal/e2e/testharness/inprocess_isolation.go +++ b/go/internal/e2e/testharness/inprocess_isolation.go @@ -2,12 +2,15 @@ package testharness import ( "context" + "errors" "flag" "fmt" "os" "os/exec" "regexp" + "runtime" "strings" + "syscall" "testing" "time" ) @@ -115,19 +118,38 @@ func listTopLevelTests() ([]string, error) { return tests, nil } -func runIsolatedProcess(ctx context.Context, name, selector string, timeout time.Duration) error { +func runIsolatedProcess(ctx context.Context, name, selector string, timeout time.Duration) (resultErr error) { executable, err := os.Executable() if err != nil { return fmt.Errorf("locate test executable: %w", err) } + outputFile, err := os.CreateTemp("", "copilot-sdk-isolated-e2e-") + if err != nil { + return fmt.Errorf("create isolated FFI output file: %w", err) + } + defer func() { + if err := os.Remove(outputFile.Name()); err != nil { + if runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(32)) { + // An inherited handle can keep the file open after the test exits. + fmt.Fprintf(os.Stderr, "isolated FFI output remains open by a descendant: %v\n", err) + return + } + resultErr = errors.Join(resultErr, fmt.Errorf("remove isolated FFI output file: %w", err)) + } + }() command := exec.CommandContext(ctx, executable, isolatedTestArgs(os.Args[1:], selector, false, timeout)...) command.Env = setEnvironmentValue(os.Environ(), isolatedInProcessTestEnv, name) command.WaitDelay = 5 * time.Second + // Native-host descendants can inherit stdout; avoid waiting for their pipe handles after the test exits. + command.Stdout = outputFile + command.Stderr = outputFile PrepareForProcessWait() - output, err := command.CombinedOutput() + runErr := command.Run() + closeErr := outputFile.Close() + output, readErr := os.ReadFile(outputFile.Name()) fmt.Print(string(output)) - if err != nil { - return fmt.Errorf("isolated FFI test %s failed: %w", name, err) + if runErr != nil || closeErr != nil || readErr != nil { + return fmt.Errorf("isolated FFI test %s failed: %w", name, errors.Join(runErr, closeErr, readErr)) } if !strings.Contains(string(output), "--- PASS: "+name+" (") && !strings.Contains(string(output), "--- SKIP: "+name+" (") { diff --git a/go/internal/e2e/testharness/proxy.go b/go/internal/e2e/testharness/proxy.go index e6a595ffe2..488215f8a6 100644 --- a/go/internal/e2e/testharness/proxy.go +++ b/go/internal/e2e/testharness/proxy.go @@ -154,10 +154,13 @@ func (p *CapiProxy) StopWithOptions(skipWritingCache bool) error { exited <- struct{}{} }() if !waitForProcessExit(exited, proxyShutdownTimeout) { - if err := killProcessTree(cmd); err != nil { - return fmt.Errorf("failed to kill proxy process: %w", err) - } + // Windows Wait can release the process handle before the kill attempt. + // A late kill error is harmless only when our own Wait confirms exit. + killErr := killProcessTree(cmd) if !waitForProcessExit(exited, proxyShutdownTimeout) { + if killErr != nil { + return fmt.Errorf("failed to kill proxy process: %w", killErr) + } return fmt.Errorf("proxy process did not exit after being killed") } } @@ -278,9 +281,18 @@ type CapturedRequest struct { // ParsedHttpExchange represents a captured HTTP exchange. type ParsedHttpExchange struct { - Request ChatCompletionRequest `json:"request"` - Response *ChatCompletionResponse `json:"response,omitempty"` - RequestHeaders map[string]json.RawMessage `json:"requestHeaders,omitempty"` + Request ChatCompletionRequest `json:"request"` + Response *ChatCompletionResponse `json:"response,omitempty"` + RequestHeaders map[string]json.RawMessage `json:"requestHeaders,omitempty"` + CompactionUsage *CompactionProviderUsage `json:"compactionUsage,omitempty"` +} + +// CompactionProviderUsage folds all responses in one correlated compaction request chain. +type CompactionProviderUsage struct { + InteractionID string `json:"interactionId"` + Summary string `json:"summary"` + ResponseCount int `json:"responseCount"` + InputTokens *int64 `json:"inputTokens,omitempty"` } // ChatCompletionRequest represents an OpenAI chat completion request. diff --git a/go/internal/e2e/testharness/proxy_test.go b/go/internal/e2e/testharness/proxy_test.go new file mode 100644 index 0000000000..cbd79c5548 --- /dev/null +++ b/go/internal/e2e/testharness/proxy_test.go @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package testharness + +import ( + "encoding/json" + "testing" +) + +func TestCompactionProviderUsageInputCounts(t *testing.T) { + for _, tc := range []struct { + name string + inputProperty string + want int64 + known bool + }{ + {"recorded", `,"inputTokens":37`, 37, true}, + {"replayed", `,"inputTokens":0`, 0, true}, + {"unavailable", ``, 0, false}, + {"input null", `,"inputTokens":null`, 0, false}, + } { + t.Run(tc.name, func(t *testing.T) { + var exchange ParsedHttpExchange + body := `{"compactionUsage":{"interactionId":"compaction-interaction","summary":"summary","responseCount":2` + + tc.inputProperty + `},"requestHeaders":{"x-interaction-type":"conversation-compaction"}}` + if err := json.Unmarshal([]byte(body), &exchange); err != nil { + t.Fatal(err) + } + if exchange.CompactionUsage == nil { + t.Fatal("Expected compaction provider usage") + } + var interactionType string + if err := json.Unmarshal(exchange.RequestHeaders["x-interaction-type"], &interactionType); err != nil || interactionType != "conversation-compaction" { + t.Fatalf("Compaction interaction header = %q, %v", interactionType, err) + } + usage := exchange.CompactionUsage + if usage.InteractionID != "compaction-interaction" || usage.Summary != "summary" || usage.ResponseCount != 2 { + t.Fatalf("Unexpected compaction chain metadata: %+v", usage) + } + var got int64 + known := usage.InputTokens != nil + if known != tc.known { + t.Fatalf("Provider input usage availability = %v, want %v", known, tc.known) + } + if known { + got = *usage.InputTokens + } + if got != tc.want { + t.Fatalf("Provider input tokens = %d, want %d", got, tc.want) + } + }) + } +} diff --git a/go/internal/e2e/tools_e2e_test.go b/go/internal/e2e/tools_e2e_test.go index 75613bebdf..a5109b154b 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + package e2e import ( @@ -18,6 +20,53 @@ func TestToolsE2E(t *testing.T) { client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) + t.Run("string schema apply_patch override binds patch input", func(t *testing.T) { + ctx.ConfigureForTest(t) + const patch = "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch" + type capturedPatch struct { + patch string + arguments any + } + calls := make(chan capturedPatch, 1) + tool := copilot.DefineTool("apply_patch", "Apply a patch", + func(input string, invocation copilot.ToolInvocation) (string, error) { + calls <- capturedPatch{input, invocation.Arguments} + return "HOST_PATCH_HANDLED", nil + }) + tool.OverridesBuiltInTool = true + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{tool}, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + finalMessage := testharness.SubscribeToFinalAssistantMessage(session) + defer finalMessage.Close() + _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use apply_patch to apply the supplied patch."}) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + answer, err := finalMessage.Wait(t.Context()) + if err != nil { + t.Fatalf("Failed to get assistant message: %v", err) + } + select { + case call := <-calls: + if call.patch != patch || call.arguments != patch { + t.Fatalf("Expected scalar patch %q, got %+v", patch, call) + } + default: + t.Fatal("String-schema handler was not invoked") + } + if message, ok := answer.Data.(*copilot.AssistantMessageData); !ok || message.Content != "Host override completed." { + t.Fatalf("Unexpected final response: %v", answer.Data) + } + if _, err := os.Stat(filepath.Join(ctx.WorkDir, "override-marker.txt")); !os.IsNotExist(err) { + t.Fatalf("Native apply_patch must not create the file: %v", err) + } + }) + t.Run("invokes built-in tools", func(t *testing.T) { ctx.ConfigureForTest(t) diff --git a/go/internal/jsonrpc2/jsonrpc2.go b/go/internal/jsonrpc2/jsonrpc2.go index fa0b45d43c..d4898b6a9c 100644 --- a/go/internal/jsonrpc2/jsonrpc2.go +++ b/go/internal/jsonrpc2/jsonrpc2.go @@ -1,6 +1,7 @@ package jsonrpc2 import ( + "bytes" "context" "crypto/rand" "encoding/json" @@ -61,6 +62,10 @@ type NotificationHandler func(method string, params json.RawMessage) // RequestHandler handles incoming server requests and returns a result or error type RequestHandler func(params json.RawMessage) (json.RawMessage, *Error) +// RequestContextHandler handles incoming server requests with a per-request +// context that is cancelled by JSON-RPC $/cancelRequest for numeric IDs. +type RequestContextHandler func(ctx context.Context, params json.RawMessage) (json.RawMessage, *Error) + // Client is a minimal JSON-RPC 2.0 client for stdio transport. type Client struct { reader *headerReader // reads frames from the remote side @@ -69,7 +74,8 @@ type Client struct { mu sync.Mutex pendingRequests map[string]chan *Response pendingInlineCallbacks map[string]func(json.RawMessage) error - requestHandlers map[string]RequestHandler + requestHandlers map[string]RequestContextHandler + incomingRequestCancels map[string]*incomingRequestCancel running atomic.Bool stopChan chan struct{} connectionClosed chan struct{} @@ -89,7 +95,8 @@ func NewClient(stdin io.WriteCloser, stdout io.ReadCloser) *Client { writer: make(chan *headerWriter, 1), pendingRequests: make(map[string]chan *Response), pendingInlineCallbacks: make(map[string]func(json.RawMessage) error), - requestHandlers: make(map[string]RequestHandler), + requestHandlers: make(map[string]RequestContextHandler), + incomingRequestCancels: make(map[string]*incomingRequestCancel), stopChan: make(chan struct{}), connectionClosed: make(chan struct{}), } @@ -196,6 +203,19 @@ func RequestHandlerFor[In, Out any](handler func(params In) (Out, *Error)) Reque // SetRequestHandler registers a handler for incoming requests from the server func (c *Client) SetRequestHandler(method string, handler RequestHandler) { + if handler == nil { + c.SetRequestContextHandler(method, nil) + return + } + c.SetRequestContextHandler(method, func(_ context.Context, params json.RawMessage) (json.RawMessage, *Error) { + return handler(params) + }) +} + +// SetRequestContextHandler registers a handler for incoming requests from the +// server. The supplied context is cancelled when a numeric $/cancelRequest for +// this request ID is received. +func (c *Client) SetRequestContextHandler(method string, handler RequestContextHandler) { c.mu.Lock() defer c.mu.Unlock() if handler == nil { @@ -205,6 +225,12 @@ func (c *Client) SetRequestHandler(method string, handler RequestHandler) { c.requestHandlers[method] = handler } +// ConnectionClosed returns a channel that closes when the JSON-RPC read loop +// exits or the client is stopped. +func (c *Client) ConnectionClosed() <-chan struct{} { + return c.connectionClosed +} + // Request sends a JSON-RPC request and waits for the response func (c *Client) Request(ctx context.Context, method string, params any) (json.RawMessage, error) { return c.RequestWithInlineResponse(ctx, method, params, nil) @@ -443,7 +469,10 @@ func (c *Client) handleResponse(response *Response) { } func (c *Client) handleRequest(request *Request) { - ctx := context.Background() + if request.Method == "$/cancelRequest" { + c.handleCancelRequest(request.Params) + return + } c.mu.Lock() handler := c.requestHandlers[request.Method] @@ -451,7 +480,7 @@ func (c *Client) handleRequest(request *Request) { if handler == nil { if request.IsCall() { - c.sendErrorResponse(ctx, request.ID, &Error{ + c.sendErrorResponse(context.Background(), request.ID, &Error{ Code: ErrMethodNotFound.Code, Message: fmt.Sprintf("Method not found: %s", request.Method), }) @@ -459,31 +488,95 @@ func (c *Client) handleRequest(request *Request) { return } + handlerCtx := context.Background() + var entry *incomingRequestCancel + var cancelKey string + if request.IsCall() { + if key, ok := numericRequestIDKey(request.ID); ok { + var cancel context.CancelFunc + handlerCtx, cancel = context.WithCancel(handlerCtx) + entry = &incomingRequestCancel{cancel: cancel} + cancelKey = key + c.mu.Lock() + c.incomingRequestCancels[cancelKey] = entry + c.mu.Unlock() + } + } + // Notifications run synchronously, calls run in a goroutine to avoid blocking if !request.IsCall() { - handler(request.Params) + handler(handlerCtx, request.Params) return } go func() { + defer func() { + if entry != nil { + c.mu.Lock() + if c.incomingRequestCancels[cancelKey] == entry { + delete(c.incomingRequestCancels, cancelKey) + } + c.mu.Unlock() + entry.cancel() + } + }() defer func() { if r := recover(); r != nil { - c.sendErrorResponse(ctx, request.ID, &Error{ + c.sendErrorResponse(context.Background(), request.ID, &Error{ Code: ErrInternal.Code, Message: fmt.Sprintf("request handler panic: %v", r), }) } }() - result, err := handler(request.Params) + result, err := handler(handlerCtx, request.Params) if err != nil { - c.sendErrorResponse(ctx, request.ID, err) + c.sendErrorResponse(context.Background(), request.ID, err) return } - c.sendResponse(ctx, request.ID, result) + c.sendResponse(context.Background(), request.ID, result) }() } +func (c *Client) handleCancelRequest(params json.RawMessage) { + var request struct { + ID json.RawMessage `json:"id"` + } + if err := json.Unmarshal(params, &request); err != nil { + return + } + key, ok := numericRequestIDKey(request.ID) + if !ok { + return + } + c.mu.Lock() + entry := c.incomingRequestCancels[key] + c.mu.Unlock() + if entry != nil { + entry.cancel() + } +} + +// incomingRequestCancel is stored by pointer so a completed handler only +// removes its own entry when a peer reuses the same request ID. +type incomingRequestCancel struct { + cancel context.CancelFunc +} + +func numericRequestIDKey(id json.RawMessage) (string, bool) { + decoder := json.NewDecoder(bytes.NewReader(id)) + decoder.UseNumber() + var value any + if err := decoder.Decode(&value); err != nil { + return "", false + } + number, ok := value.(json.Number) + if !ok { + return "", false + } + return number.String(), true +} + func (c *Client) sendResponse(ctx context.Context, id json.RawMessage, result json.RawMessage) { response := Response{ JSONRPC: version, diff --git a/go/rpc/legacy_parameters_test.go b/go/rpc/legacy_parameters_test.go new file mode 100644 index 0000000000..e9108d562f --- /dev/null +++ b/go/rpc/legacy_parameters_test.go @@ -0,0 +1,53 @@ +package rpc + +import ( + "encoding/json" + "testing" +) + +// Requests that declare x-legacy-parameters keep their Go struct: added inputs are optional +// pointer fields, so existing keyed literals compile and encode exactly as before. +func TestLegacyParameterRequestsKeepKeyedLiterals(t *testing.T) { + contract := CatalogClientContract{ProtocolVersion: 3, RequiredCapabilities: []string{"mcp-install-planning"}} + scope := MCPPlanScopeUser + limit := int32(4) + + existing := []any{ + MCPPlanInstallRequest{ + Contract: contract, + Source: &MCPPlanInstallSourceCandidate{CandidateHandle: "candidate", SearchID: "search"}, + Scope: &scope, + }, + CatalogSearchRequest{Contract: contract, Query: "catalogue query", Limit: &limit}, + } + for _, request := range existing { + wire := encodeRequest(t, request) + if _, ok := wire["policySessionId"]; ok { + t.Fatalf("existing literal %T sent policySessionId: %v", request, wire) + } + } + + session := "session" + added := encodeRequest(t, MCPPlanInstallRequest{ + Contract: contract, + Source: &MCPPlanInstallSourceCandidate{CandidateHandle: "candidate", SearchID: "search"}, + Scope: &scope, + PolicySessionID: &session, + }) + if added["policySessionId"] != session || added["scope"] != "user" { + t.Fatalf("added input not encoded alongside the legacy fields: %v", added) + } +} + +func encodeRequest(t *testing.T, request any) map[string]any { + t.Helper() + data, err := json.Marshal(request) + if err != nil { + t.Fatalf("marshal %T: %v", request, err) + } + var wire map[string]any + if err := json.Unmarshal(data, &wire); err != nil { + t.Fatalf("unmarshal %T: %v", request, err) + } + return wire +} diff --git a/go/rpc/managed_settings_test.go b/go/rpc/managed_settings_test.go new file mode 100644 index 0000000000..34e69401fc --- /dev/null +++ b/go/rpc/managed_settings_test.go @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "context" + "encoding/json" + "net" + "testing" + "time" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" +) + +func TestManagedSettingsResolveRequestEncoding(t *testing.T) { + selectionID := "selected-account" + for _, test := range []struct { + name string + params *ManagedSettingsResolveRequest + want string + }{ + {"nil", nil, `{}`}, + {"empty", &ManagedSettingsResolveRequest{}, `{}`}, + {"selected", &ManagedSettingsResolveRequest{SelectionID: &selectionID}, `{"selectionId":"selected-account"}`}, + } { + t.Run(test.name, func(t *testing.T) { + clientConn, serverConn := net.Pipe() + defer clientConn.Close() + defer serverConn.Close() + client := jsonrpc2.NewClient(clientConn, clientConn) + server := jsonrpc2.NewClient(serverConn, serverConn) + requests := make(chan json.RawMessage, 1) + server.SetRequestHandler("managedSettings.resolve", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + requests <- params + return json.RawMessage(`{}`), nil + }) + client.Start() + server.Start() + defer client.Stop() + defer server.Stop() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if _, err := NewServerRPC(client).ManagedSettings.Resolve(ctx, test.params); err != nil { + t.Fatal(err) + } + select { + case got := <-requests: + if string(got) != test.want { + t.Fatalf("params = %s, want %s", got, test.want) + } + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + }) + } +} diff --git a/go/rpc/mcp_list_test.go b/go/rpc/mcp_list_test.go new file mode 100644 index 0000000000..c2e9c47a09 --- /dev/null +++ b/go/rpc/mcp_list_test.go @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "context" + "encoding/json" + "io" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" +) + +var _ interface { + List(context.Context) (*MCPServerList, error) + ListConfigured(context.Context) (*MCPConfiguredServerList, error) +} = (*MCPAPI)(nil) + +func TestMCPListUsesParameterlessWireContract(t *testing.T) { + clientToServerReader, clientToServerWriter := io.Pipe() + serverToClientReader, serverToClientWriter := io.Pipe() + client := jsonrpc2.NewClient(clientToServerWriter, serverToClientReader) + server := jsonrpc2.NewClient(serverToClientWriter, clientToServerReader) + requests := make(chan map[string]any, 2) + server.SetRequestHandler("session.mcp.list", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + server.SetRequestHandler("session.mcp.listConfigured", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + client.Start() + server.Start() + t.Cleanup(func() { + client.Stop() + server.Stop() + _ = clientToServerWriter.Close() + _ = clientToServerReader.Close() + _ = serverToClientWriter.Close() + _ = serverToClientReader.Close() + }) + + mcp := NewSessionRPC(client, "session-1").MCP + if _, err := mcp.List(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := mcp.ListConfigured(t.Context()); err != nil { + t.Fatal(err) + } + got := make([]map[string]any, 2) + for i := range got { + got[i] = <-requests + } + for _, request := range got { + if request["sessionId"] != "session-1" || len(request) != 1 { + t.Fatalf("request = %#v, want only the bound session ID", request) + } + } +} diff --git a/go/rpc/mcp_prompts_test.go b/go/rpc/mcp_prompts_test.go new file mode 100644 index 0000000000..14cbe69fc9 --- /dev/null +++ b/go/rpc/mcp_prompts_test.go @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" +) + +// Generated serialization supplements the real MCP boundary tests in Node and .NET. +func TestMCPPromptResultsPreserveOpaqueJSON(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", "..", "test", "harness", "mcp-prompt-fixtures.json")) + if err != nil { + t.Fatal(err) + } + var fixtures map[string]json.RawMessage + if err := json.Unmarshal(raw, &fixtures); err != nil { + t.Fatal(err) + } + for _, name := range []string{"firstPage", "secondPage", "richPrompt"} { + t.Run(name, func(t *testing.T) { + var result any = &MCPPromptsListResult{} + if name == "richPrompt" { + result = &MCPPromptsGetResult{} + } + if err := json.Unmarshal(fixtures[name], result); err != nil { + t.Fatal(err) + } + assertPromptJSON(t, result, fixtures[name]) + }) + } +} + +func TestMCPPromptRequestsPreserveOptionalArguments(t *testing.T) { + for _, wire := range []string{ + `{"serverName":"fixture","promptName":"rich"}`, + `{"serverName":"fixture","promptName":"rich","arguments":{}}`, + `{"serverName":"fixture","promptName":"rich","arguments":{"topic":"日本語","style":""}}`, + } { + t.Run(wire, func(t *testing.T) { + var request MCPPromptsGetRequest + if err := json.Unmarshal([]byte(wire), &request); err != nil { + t.Fatal(err) + } + assertPromptJSON(t, request, []byte(wire)) + }) + } + for _, wire := range []string{ + `{"serverName":"fixture"}`, + `{"serverName":"fixture","cursor":"page:2/opaque+cursor="}`, + } { + var request MCPPromptsListRequest + if err := json.Unmarshal([]byte(wire), &request); err != nil { + t.Fatal(err) + } + assertPromptJSON(t, request, []byte(wire)) + } +} + +func assertPromptJSON(t *testing.T, value any, expected []byte) { + t.Helper() + actual, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var got, want any + if err := json.Unmarshal(actual, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(expected, &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("JSON mismatch\n got: %s\nwant: %s", actual, expected) + } +} diff --git a/go/rpc/permission_request_managed_approval.go b/go/rpc/permission_request_managed_approval.go index d1bfa37356..6724e2f430 100644 --- a/go/rpc/permission_request_managed_approval.go +++ b/go/rpc/permission_request_managed_approval.go @@ -34,7 +34,7 @@ func (r PermissionRequestExtensionPermissionAccess) RequiresManagedApproval() bo // RequiresManagedApproval reports whether managed policy requires an explicit // human decision for this request. -func (r PermissionRequestFactory) RequiresManagedApproval() bool { +func (r PermissionRequestWorkflow) RequiresManagedApproval() bool { return managedApprovalRequired(r.ManagedApprovalRequired) } diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 42550be80e..a8a921f58b 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -138,6 +138,49 @@ type AccountQuotaSnapshot struct { UsedRequests int64 `json:"usedRequests"` } +// Enumerate request carrying the typed collection query. +// Experimental: AccountsEnumerateRequest is part of an experimental API and may change or +// be removed. +type AccountsEnumerateRequest struct { + // Which typed accounts collection to enumerate. + Query AuthEnumerateQuery `json:"query"` +} + +// Read request carrying the typed datum query. +// Experimental: AccountsGetRequest is part of an experimental API and may change or be +// removed. +type AccountsGetRequest struct { + // Which typed accounts datum to read. + Query AuthReadQuery `json:"query"` +} + +// Mutation request carrying the typed write command. +// Experimental: AccountsSetRequest is part of an experimental API and may change or be +// removed. +type AccountsSetRequest struct { + // The non-interactive mutation command to apply. + Command AuthWrite `json:"command"` +} + +// One signed-in account in the roster forest. +// Experimental: AccountStatus is part of an experimental API and may change or be removed. +type AccountStatus struct { + // Whether this is the active account. + Active bool `json:"active"` + // 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. + DerivedFrom *string `json:"derivedFrom,omitempty"` + // Authentication host URL. + Host string `json:"host"` + // The provider kind of this account. + Kind AccountKind `json:"kind"` + // Authenticated login/username. + Login string `json:"login"` + // Opaque selection id used to switch to, or log out, this account. + SelectionID string `json:"selectionId"` +} + // Canonical directory where custom agents can be discovered or created, with scope, // preference, and optional project path. // Experimental: AgentDiscoveryPath is part of an experimental API and may change or be @@ -425,6 +468,37 @@ type AgentReloadResult struct { Agents []AgentInfo `json:"agents"` } +// The models a custom agent asks for, and the models actually available. +// Experimental: AgentsCustomAgentInitialModelDecisionParams is part of an experimental API +// and may change or be removed. +// Internal: AgentsCustomAgentInitialModelDecisionParams is an internal SDK API and is not +// part of the public surface. +type AgentsCustomAgentInitialModelDecisionParams struct { + // The agent's declared `model:` entry, serialized. A single name or an ordered list of + // acceptable names. + AgentModelsJSON string `json:"agentModelsJson"` + // The models available to this session, serialized in the shape the model list carries. + AvailableModelsJSON string `json:"availableModelsJson"` +} + +// The model to switch to, and the warning to show when the agent's preference could not be +// met. +// Experimental: AgentsCustomAgentInitialModelDecisionResult is part of an experimental API +// and may change or be removed. +// Internal: AgentsCustomAgentInitialModelDecisionResult is an internal SDK API and is not +// part of the public surface. +type AgentsCustomAgentInitialModelDecisionResult struct { + // The reasoning effort attached to the selected model preference. Absent when that + // preference does not specify an effort. + ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // The first available model that matches the agent's preferences. Absent when none of the + // requested models is available. + TargetModel *string `json:"targetModel,omitempty"` + // What to tell the user about an unmet preference. Absent when the preference was met. A + // warning with no `targetModel` means the agent's models are all unavailable. + Warning *string `json:"warning,omitempty"` +} + // Optional project paths to include in agent discovery. // Experimental: AgentsDiscoverRequest is part of an experimental API and may change or be // removed. @@ -463,6 +537,101 @@ type AgentSetPromptRequest struct { Prompt string `json:"prompt"` } +// The feature flags to evaluate shipped agents against. +// Experimental: AgentsGetAvailableBuiltinsRequest is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetAvailableBuiltinsRequest is an internal SDK API and is not part of the +// public surface. +type AgentsGetAvailableBuiltinsRequest struct { + // The surface asking, which gates agents that only apply to one client. Omit or pass null + // to apply no client filter. + Context *string `json:"context,omitempty"` + // Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or + // pass null for no flags. + FeatureFlags map[string]any `json:"featureFlags,omitzero"` + // Flag overrides keyed by name. A null entry uses the corresponding base flag; false + // explicitly disables it. Omit or pass null for no overrides. + Overrides map[string]any `json:"overrides,omitzero"` +} + +// The shipped agents available under the requested flags. +// Experimental: AgentsGetAvailableBuiltinsResult is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetAvailableBuiltinsResult is an internal SDK API and is not part of the +// public surface. +type AgentsGetAvailableBuiltinsResult struct { + // Available shipped agents, in the runtime's own order. + // Internal: Agents is part of the SDK's internal API surface and is not intended for + // external use. + Agents []BuiltinAgentSummary `json:"agents"` +} + +// The shipped agent whose definition to load. +// Experimental: AgentsGetBuiltinDefinitionRequest is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetBuiltinDefinitionRequest is an internal SDK API and is not part of the +// public surface. +type AgentsGetBuiltinDefinitionRequest struct { + // The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside + // that list is special-cased in code and has no definition, and is reported as an error + // rather than as an empty definition. + Name string `json:"name"` +} + +// One shipped agent's definition. +// Experimental: AgentsGetBuiltinDefinitionResult is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetBuiltinDefinitionResult is an internal SDK API and is not part of the +// public surface. +type AgentsGetBuiltinDefinitionResult struct { + // The agent's definition, serialized as JSON. It carries the authored keys plus the + // runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather + // than an object because the runtime parses it with the agent schema's tolerant shape, + // which accepts keys this contract does not name. + DefinitionJSON string `json:"definitionJson"` +} + +// The shipped agent whose listing entry to load. +// Experimental: AgentsGetBuiltinListingDefinitionRequest is part of an experimental API and +// may change or be removed. +// Internal: AgentsGetBuiltinListingDefinitionRequest is an internal SDK API and is not part +// of the public surface. +type AgentsGetBuiltinListingDefinitionRequest struct { + // The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the + // agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here + // too, from its in-code definition. + Name string `json:"name"` +} + +// One shipped agent, projected for a listing. +// Experimental: AgentsGetBuiltinListingDefinitionResult is part of an experimental API and +// may change or be removed. +// Internal: AgentsGetBuiltinListingDefinitionResult is an internal SDK API and is not part +// of the public surface. +type AgentsGetBuiltinListingDefinitionResult struct { + // The agent projected as a custom agent, serialized as JSON. It is a string rather than an + // object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying + // definition with the agent schema's tolerant shape, which accepts keys this contract does + // not name. + DefinitionJSON string `json:"definitionJson"` +} + +// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +// Experimental: AgentsGetBuiltinsResult is part of an experimental API and may change or be +// removed. +// Internal: AgentsGetBuiltinsResult is an internal SDK API and is not part of the public +// surface. +type AgentsGetBuiltinsResult struct { + // The subset of `names` a user is allowed to turn off. A shipped agent outside this list is + // always active and a client should not offer a toggle for it. + DisableableNames []string `json:"disableableNames"` + // Every agent name this runtime ships. + Names []string `json:"names"` + // The subset of `names` defined by a shipped YAML definition. The remainder are + // special-cased in code and have no definition to load. + YamlBasedNames []string `json:"yamlBasedNames"` +} + // Optional project paths to include when enumerating agent discovery directories. // Experimental: AgentsGetDiscoveryPathsRequest is part of an experimental API and may // change or be removed. @@ -867,6 +1036,82 @@ type AttachmentSelectionDetailsStart struct { Line int64 `json:"line"` } +// Selects which accounts collection to enumerate. A no-arg selector is the empty-payload +// variant. +// Experimental: AuthEnumerateQuery is part of an experimental API and may change or be +// removed. +type AuthEnumerateQuery interface { + authEnumerateQuery() + Kind() AuthEnumerateQueryKind +} + +type RawAuthEnumerateQueryData struct { + Discriminator AuthEnumerateQueryKind + Raw json.RawMessage +} + +func (RawAuthEnumerateQueryData) authEnumerateQuery() {} +func (r RawAuthEnumerateQueryData) Kind() AuthEnumerateQueryKind { + return r.Discriminator +} + +type AuthEnumerateQueryAccounts struct { +} + +func (AuthEnumerateQueryAccounts) authEnumerateQuery() {} +func (AuthEnumerateQueryAccounts) Kind() AuthEnumerateQueryKind { + return AuthEnumerateQueryKindAccounts +} + +type AuthEnumerateQueryProviders struct { + // Whether an interactive Entra broker is available on the host; gates Entra availability in + // the returned list. + BrokerAvailable *bool `json:"brokerAvailable,omitempty"` +} + +func (AuthEnumerateQueryProviders) authEnumerateQuery() {} +func (AuthEnumerateQueryProviders) Kind() AuthEnumerateQueryKind { + return AuthEnumerateQueryKindProviders +} + +// The enumerated collection, keyed by the same selector as the query. +// Experimental: AuthEnumerateValue is part of an experimental API and may change or be +// removed. +type AuthEnumerateValue interface { + authEnumerateValue() + Kind() AuthEnumerateValueKind +} + +type RawAuthEnumerateValueData struct { + Discriminator AuthEnumerateValueKind + Raw json.RawMessage +} + +func (RawAuthEnumerateValueData) authEnumerateValue() {} +func (r RawAuthEnumerateValueData) Kind() AuthEnumerateValueKind { + return r.Discriminator +} + +type AuthEnumerateValueAccounts struct { + // The signed-in account forest; empty when not logged in. + Items []AccountStatus `json:"items"` +} + +func (AuthEnumerateValueAccounts) authEnumerateValue() {} +func (AuthEnumerateValueAccounts) Kind() AuthEnumerateValueKind { + return AuthEnumerateValueKindAccounts +} + +type AuthEnumerateValueProviders struct { + // The providers offered for interactive login. + Items []ProviderDescriptor `json:"items"` +} + +func (AuthEnumerateValueProviders) authEnumerateValue() {} +func (AuthEnumerateValueProviders) Kind() AuthEnumerateValueKind { + return AuthEnumerateValueKindProviders +} + // Credential-free authentication identity safe to expose to hosts and user interfaces. // Experimental: AuthIdentity is part of an experimental API and may change or be removed. type AuthIdentity struct { @@ -885,6 +1130,18 @@ type AuthIdentity struct { Type AuthInfoType `json:"type"` } +// Credential-free identity metadata. +// Experimental: AuthIdentityMetadata is part of an experimental API and may change or be +// removed. +type AuthIdentityMetadata struct { + // Identity host. + Host string `json:"host"` + // User login. + Login string `json:"login"` + // Authentication type. + Type AuthInfoType `json:"type"` +} + // Authentication credentials accepted only at native protocol ingress. Runtime outputs use // credential-free `AuthIdentity` metadata. // Experimental: AuthInfo is part of an experimental API and may change or be removed. @@ -903,6 +1160,21 @@ func (r RawAuthInfoData) Type() AuthInfoType { return r.Discriminator } +// An interactive account whose model provider owns its credentials. It carries no GitHub +// credential. +// Experimental: AccountAuthInfo is part of an experimental API and may change or be removed. +type AccountAuthInfo struct { + // Host coordinate owned by the account's model provider. + Host string `json:"host"` + // Login identifying the provider-owned account. + Login string `json:"login"` +} + +func (AccountAuthInfo) authInfo() {} +func (AccountAuthInfo) Type() AuthInfoType { + return AuthInfoTypeAccount +} + // Authentication-info input variant for API-key authentication to a non-GitHub LLM // provider, carrying the secret `apiKey` and host. // Experimental: APIKeyAuthInfo is part of an experimental API and may change or be removed. @@ -1062,6 +1334,256 @@ func (UserAuthInfo) Type() AuthInfoType { return AuthInfoTypeUser } +// A credential-free account choice after sign-in. +// Experimental: AuthLoginAccount is part of an experimental API and may change or be +// removed. +type AuthLoginAccount struct { + // Host coordinate owned by the selected account's provider. + Host string `json:"host"` + // Provider kind that owns this account choice. + Kind AccountKind `json:"kind"` + // Human-readable login for the account choice. + Login string `json:"login"` + // Opaque identifier supplied to the next login step to select this account. + SelectionID string `json:"selectionId"` +} + +// Advance an in-flight login flow, optionally fulfilling an input-required step. +// Experimental: AuthLoginAdvanceRequest is part of an experimental API and may change or be +// removed. +type AuthLoginAdvanceRequest struct { + // Opaque flow id from begin. + FlowID string `json:"flowId"` + // Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored + // otherwise. + Input *string `json:"input,omitempty"` +} + +// Begin an interactive login flow for a provider kind. Dispatch is kind-only. +// Experimental: AuthLoginBeginRequest is part of an experimental API and may change or be +// removed. +type AuthLoginBeginRequest struct { + // The provider kind to sign in with. + Kind LoginProviderKind `json:"kind"` +} + +// A started login flow: its opaque id and first step. +// Experimental: AuthLoginBegun is part of an experimental API and may change or be removed. +type AuthLoginBegun struct { + // Opaque flow id used to advance or cancel this login. + FlowID string `json:"flowId"` + // The first step of the flow. + Step AuthLoginStep `json:"step"` +} + +// Cancel an in-flight login flow. +// Experimental: AuthLoginCancelRequest is part of an experimental API and may change or be +// removed. +type AuthLoginCancelRequest struct { + // Opaque flow id from begin. + FlowID string `json:"flowId"` +} + +// Result of an interactive login flow. Pending consent or account selection is not terminal. +// Experimental: AuthLoginResultDto is part of an experimental API and may change or be +// removed. +type AuthLoginResultDto struct { + // Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 + // first. + Accounts []AuthLoginAccount `json:"accounts,omitzero"` + // Host that was signed in, when completed. + Host *string `json:"host,omitempty"` + // Login that was signed in, when completed. + Login *string `json:"login,omitempty"` + // Current disposition of the login, including pending user decisions. + Status AuthLoginResultStatus `json:"status"` +} + +// 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). +// Experimental: AuthLoginStep is part of an experimental API and may change or be removed. +type AuthLoginStep interface { + authLoginStep() + Kind() AuthLoginStepKind +} + +type RawAuthLoginStepData struct { + Discriminator AuthLoginStepKind + Raw json.RawMessage +} + +func (RawAuthLoginStepData) authLoginStep() {} +func (r RawAuthLoginStepData) Kind() AuthLoginStepKind { + return r.Discriminator +} + +type AuthLoginStepAwaiting struct { +} + +func (AuthLoginStepAwaiting) authLoginStep() {} +func (AuthLoginStepAwaiting) Kind() AuthLoginStepKind { + return AuthLoginStepKindAwaiting +} + +type AuthLoginStepCompleted struct { + // Login result. When status is needs-plaintext-consent or needs-account-selection, advance + // with the user's decision to continue. + Result AuthLoginResultDto `json:"result"` +} + +func (AuthLoginStepCompleted) authLoginStep() {} +func (AuthLoginStepCompleted) Kind() AuthLoginStepKind { + return AuthLoginStepKindCompleted +} + +type AuthLoginStepError struct { + // Human-readable failure message. + Message string `json:"message"` +} + +func (AuthLoginStepError) authLoginStep() {} +func (AuthLoginStepError) Kind() AuthLoginStepKind { + return AuthLoginStepKindError +} + +type AuthLoginStepInputRequired struct { + // Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. + // a GitHub Enterprise Cloud host, *.ghe.com). + Prompt string `json:"prompt"` +} + +func (AuthLoginStepInputRequired) authLoginStep() {} +func (AuthLoginStepInputRequired) Kind() AuthLoginStepKind { + return AuthLoginStepKindInputRequired +} + +type AuthLoginStepNeedsInteraction struct { +} + +func (AuthLoginStepNeedsInteraction) authLoginStep() {} +func (AuthLoginStepNeedsInteraction) Kind() AuthLoginStepKind { + return AuthLoginStepKindNeedsInteraction +} + +type AuthLoginStepOpenURL struct { + // Authorize URL the consumer should open in a browser (consumer-driven browser-open). + URL string `json:"url"` +} + +func (AuthLoginStepOpenURL) authLoginStep() {} +func (AuthLoginStepOpenURL) Kind() AuthLoginStepKind { + return AuthLoginStepKindOpenURL +} + +// Selects which typed accounts datum to read. +// Experimental: AuthReadQuery is part of an experimental API and may change or be removed. +type AuthReadQuery interface { + authReadQuery() + Kind() AuthReadQueryKind +} + +type RawAuthReadQueryData struct { + Discriminator AuthReadQueryKind + Raw json.RawMessage +} + +func (RawAuthReadQueryData) authReadQuery() {} +func (r RawAuthReadQueryData) Kind() AuthReadQueryKind { + return r.Discriminator +} + +type AuthReadQueryActiveAccount struct { +} + +func (AuthReadQueryActiveAccount) authReadQuery() {} +func (AuthReadQueryActiveAccount) Kind() AuthReadQueryKind { + return AuthReadQueryKindActiveAccount +} + +type AuthReadQueryLastErrors struct { +} + +func (AuthReadQueryLastErrors) authReadQuery() {} +func (AuthReadQueryLastErrors) Kind() AuthReadQueryKind { + return AuthReadQueryKindLastErrors +} + +type AuthReadQueryStatus struct { +} + +func (AuthReadQueryStatus) authReadQuery() {} +func (AuthReadQueryStatus) Kind() AuthReadQueryKind { + return AuthReadQueryKindStatus +} + +// The read result, keyed by the same selector as the query. +// Experimental: AuthReadValue is part of an experimental API and may change or be removed. +type AuthReadValue interface { + authReadValue() + Kind() AuthReadValueKind +} + +type RawAuthReadValueData struct { + Discriminator AuthReadValueKind + Raw json.RawMessage +} + +func (RawAuthReadValueData) authReadValue() {} +func (r RawAuthReadValueData) Kind() AuthReadValueKind { + return r.Discriminator +} + +type AuthReadValueActiveAccount struct { + // The active account, or absent when not logged in. + Account *AccountStatus `json:"account,omitempty"` + // Credential-free identity metadata for the active account, including resolved Copilot user + // information when available. + AuthInfo *AuthIdentity `json:"authInfo,omitempty"` +} + +func (AuthReadValueActiveAccount) authReadValue() {} +func (AuthReadValueActiveAccount) Kind() AuthReadValueKind { + return AuthReadValueKindActiveAccount +} + +type AuthReadValueLastErrors struct { + // Validation errors from the most recent authentication attempt. + Errors []AuthValidationError `json:"errors"` +} + +func (AuthReadValueLastErrors) authReadValue() {} +func (AuthReadValueLastErrors) Kind() AuthReadValueKind { + return AuthReadValueKindLastErrors +} + +type AuthReadValueStatus struct { + // The neutral authentication status summary. + Status AuthStatusDto `json:"status"` +} + +func (AuthReadValueStatus) authReadValue() {} +func (AuthReadValueStatus) Kind() AuthReadValueKind { + return AuthReadValueKindStatus +} + +// Neutral authentication status summary. +// Experimental: AuthStatusDto is part of an experimental API and may change or be removed. +type AuthStatusDto struct { + // Number of signed-in accounts in the roster. + AccountCount int64 `json:"accountCount"` + // Active account host, if authenticated. + ActiveHost *string `json:"activeHost,omitempty"` + // Active account login, if authenticated. + ActiveLogin *string `json:"activeLogin,omitempty"` + // Copilot plan tier of the active account, if known. + CopilotPlan *string `json:"copilotPlan,omitempty"` + // Whether the session has resolved authentication. + IsAuthenticated bool `json:"isAuthenticated"` +} + // Validation error from an authentication attempt. // Experimental: AuthValidationError is part of an experimental API and may change or be // removed. @@ -1077,6 +1599,66 @@ type AuthValidationError struct { // removed. type AuthValidationErrors []AuthValidationError +// One non-interactive accounts mutation command (the selector is fused with its typed args). +// Experimental: AuthWrite is part of an experimental API and may change or be removed. +type AuthWrite interface { + authWrite() + Kind() AuthWriteKind +} + +type RawAuthWriteData struct { + Discriminator AuthWriteKind + Raw json.RawMessage +} + +func (RawAuthWriteData) authWrite() {} +func (r RawAuthWriteData) Kind() AuthWriteKind { + return r.Discriminator +} + +type AuthWriteLogout struct { + // Opaque selection id of the account to log out; absent logs out the active account. + SelectionID *string `json:"selectionId,omitempty"` +} + +func (AuthWriteLogout) authWrite() {} +func (AuthWriteLogout) Kind() AuthWriteKind { + return AuthWriteKindLogout +} + +type AuthWriteSetCredentials struct { + // Authentication host URL. + Host string `json:"host"` + // Login/username for the credential. + Login string `json:"login"` + // GitHub authentication token to install. + Token string `json:"token"` +} + +func (AuthWriteSetCredentials) authWrite() {} +func (AuthWriteSetCredentials) Kind() AuthWriteKind { + return AuthWriteKindSetCredentials +} + +type AuthWriteSwitchActive struct { + // Opaque selection id of the account to make active. + SelectionID string `json:"selectionId"` +} + +func (AuthWriteSwitchActive) authWrite() {} +func (AuthWriteSwitchActive) Kind() AuthWriteKind { + return AuthWriteKindSwitchActive +} + +// Result of a non-interactive accounts mutation. +// Experimental: AuthWriteResult is part of an experimental API and may change or be removed. +type AuthWriteResult struct { + // For a logout, whether other signed-in accounts remain. + MoreUsers *bool `json:"moreUsers,omitempty"` + // Whether the mutation was applied. + Ok bool `json:"ok"` +} + // Current per-window credit limit and consumption for an autopilot objective. // Experimental: AutopilotObjectiveCreditLimit is part of an experimental API and may change // or be removed. @@ -1120,6 +1702,18 @@ type AutopilotObjectiveState struct { TurnCount int64 `json:"turnCount"` } +// A shipped agent, named and described. +// Experimental: BuiltinAgentSummary is part of an experimental API and may change or be +// removed. +// Internal: BuiltinAgentSummary is an internal SDK API and is not part of the public +// surface. +type BuiltinAgentSummary struct { + // One-line description of what the agent does. + Description string `json:"description"` + // The agent name, as it appears in `getBuiltins`. + Name string `json:"name"` +} + // The running runtime's complete catalog of well-known built-in model IDs, including // supported models and additional IDs with built-in metadata. // Experimental: BuiltInModelCatalog is part of an experimental API and may change or be @@ -1541,8 +2135,9 @@ func (CatalogAgentPluginCandidate) Kind() CatalogCandidateKind { return CatalogCandidateKindPlugin } -// An inert AI skill catalog result. AI skills are discovery-only and cannot be represented -// as installable through this surface. +// An inert AI skill catalog result. Verified Skill candidates may be installable only when +// the runtime reports installability and the selected session is permitted to plan +// installation. // Experimental: CatalogAiSkillCandidate is part of an experimental API and may change or be // removed. type CatalogAiSkillCandidate struct { @@ -1556,7 +2151,8 @@ type CatalogAiSkillCandidate struct { Handle string `json:"handle"` // ISO 8601 timestamp after which the handle is stale and will be rejected. HandleExpiresAt string `json:"handleExpiresAt"` - // AI skills are discovery-only and cannot be installed through this surface + // Whether this AI skill candidate can be planned for verified installation in the selected + // session. Installability CatalogAiSkillInstallability `json:"installability"` // Media type of the underlying AI skill card MediaType CatalogAiSkillMediaType `json:"mediaType"` @@ -1744,9 +2340,10 @@ type CatalogResourceIdentity string // removed. type CatalogResourceVersion string -// An explicit numbered-page request. The SDK treats the token as opaque; only the runtime -// decodes it and changes its targetPage. Authority validation binds navigation to the -// original search. No snapshot stability or token TTL is promised. +// An explicit numbered-page request. SDK consumers treat the token as opaque. For bound +// search, the runtime unwraps an expiring owner-bound reference to the private authority +// token; only the runtime changes the authority token's targetPage. Legacy unbound +// navigation keeps its authority-issued token semantics. No snapshot stability is promised. // Experimental: CatalogSearchPage is part of an experimental API and may change or be // removed. type CatalogSearchPage struct { @@ -1754,8 +2351,9 @@ type CatalogSearchPage struct { // navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly // available pages beyond that signed pageCount. Number int32 `json:"number"` - // Opaque authority-issued pagination token from an earlier response. Never decode, modify - // or log it in an SDK consumer. + // Opaque pagination token from an earlier response, owner-bound when session-bound search + // was requested. Never decode, modify or log it in an SDK consumer. Expired or foreign + // bound references require a fresh bound search, not a legacy retry. Token string `json:"token"` } @@ -1781,9 +2379,11 @@ type CatalogSearchPagination struct { PageCount int64 `json:"pageCount"` // Page size bound to the search, equal to the effective request limit. PageSize int32 `json:"pageSize"` - // Opaque authority-issued pagination token. Only the runtime decodes it or changes - // targetPage; SDK consumers must not decode, modify or log it. It has no runtime-created - // expiry or cache. + // Opaque pagination token. Session-bound search returns an expiring runtime-owned reference + // retaining the exact private authority token, original search and authority. Legacy + // unbound search returns the authority token unchanged, without a runtime-created expiry. + // Only the runtime unwraps tokens or changes targetPage; SDK consumers must not decode, + // modify or log them. Token string `json:"token"` // Backend-reported count for this response, not the number of returned candidates. Its // relationship to the full query result set is unknown. @@ -1811,6 +2411,9 @@ type CatalogSearchRequest struct { // catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh // first-page search. Page *CatalogSearchPage `json:"page,omitempty"` + // Select an existing attached local session. Requires authenticated, session-bound search. + // The runtime never creates, resumes or reconfigures a session to honour this selector. + PolicySessionID *string `json:"policySessionId,omitempty"` // Free-text search query. Persisted as tool input for session continuity, but omitted from // telemetry. Query string `json:"query"` @@ -2678,6 +3281,10 @@ type ConnectorCapabilities struct { // Whether callers select a host-owned GitHub account through an opaque selection ID rather // than supplying a provider token. OpaqueAccountSelection bool `json:"opaqueAccountSelection"` + // Whether getAccount is supported. Absence means false. + SessionAccountSelection *bool `json:"sessionAccountSelection,omitempty"` + // Whether reconcile accepts forceConnectorName. Absence means false. + TargetedReconcile *bool `json:"targetedReconcile,omitempty"` } // Credential-free Connector catalog entry. @@ -2688,12 +3295,18 @@ type ConnectorCatalogEntry struct { Description *string `json:"description,omitempty"` // Untrusted display label from the service. DisplayName string `json:"displayName"` + // Optional catalog logo. + Logo *string `json:"logo,omitempty"` // Canonical Connector name used by lifecycle methods. Name string `json:"name"` + // Optional catalog release tag. + ReleaseTag *string `json:"releaseTag,omitempty"` // Opaque stable runtime IDs currently projected into the session for this Connector. RuntimeServerIDs []string `json:"runtimeServerIds"` // Current authoritative service connection state. Status ConnectorCatalogStatus `json:"status"` + // Optional catalog tier. + Tier *string `json:"tier,omitempty"` } // Validated Connector catalog snapshot cached by the session. @@ -2798,6 +3411,94 @@ type ConnectorDisconnectResult struct { Status ConnectorStatus `json:"status"` } +// Eligible account. +// Experimental: ConnectorDiscoveryAccount is part of an experimental API and may change or +// be removed. +type ConnectorDiscoveryAccount struct { + // Opaque account ID. + AccountID string `json:"accountId"` + // Account metadata. + AuthInfo ConnectorDiscoveryAuthInfo `json:"authInfo"` +} + +// Eligible accounts. +// Experimental: ConnectorDiscoveryAccountList is part of an experimental API and may change +// or be removed. +type ConnectorDiscoveryAccountList struct { + // Eligible accounts. + Accounts []ConnectorDiscoveryAccount `json:"accounts"` + // Availability. + Availability ConnectorDiscoveryAvailability `json:"availability"` +} + +// Selected account. +// Experimental: ConnectorDiscoveryAccountRequest is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryAccountRequest struct { + // Opaque account ID. + AccountID string `json:"accountId"` +} + +// Account metadata. +// Experimental: ConnectorDiscoveryAuthInfo is part of an experimental API and may change or +// be removed. +type ConnectorDiscoveryAuthInfo struct { + // Host. + Host string `json:"host"` + // Login. + Login string `json:"login"` + // Authentication type. + Type AuthInfoType `json:"type"` +} + +// Feature availability. +// Experimental: ConnectorDiscoveryCapabilities is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCapabilities struct { + // API version. + APIVersion int64 `json:"apiVersion"` + // Availability. + Availability ConnectorDiscoveryAvailability `json:"availability"` + // Whether results are cached. + ConditionalCache bool `json:"conditionalCache"` + // Whether accounts are selected by opaque ID. + OpaqueAccountSelection bool `json:"opaqueAccountSelection"` +} + +// Entry. +// Experimental: ConnectorDiscoveryCatalogEntry is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCatalogEntry struct { + // Description. + Description *string `json:"description,omitempty"` + // Display name. + DisplayName string `json:"displayName"` + // Logo. + Logo *string `json:"logo,omitempty"` + // Name. + Name string `json:"name"` + // Release tag. + ReleaseTag *string `json:"releaseTag,omitempty"` + // Status. + Status ConnectorCatalogStatus `json:"status"` + // Tier. + Tier *string `json:"tier,omitempty"` +} + +// Entries for the selected account. +// Experimental: ConnectorDiscoveryCatalogResult is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCatalogResult struct { + // Opaque account ID. + AccountID string `json:"accountId"` + // Entries. + Connectors []ConnectorDiscoveryCatalogEntry `json:"connectors"` + // Refresh time in Unix epoch milliseconds. + RefreshedAtMs int64 `json:"refreshedAtMs"` + // Revision. + Revision int64 `json:"revision"` +} + // Requests authoritative Connector-to-MCP reconciliation for the pinned account. // Experimental: ConnectorReconcileRequest is part of an experimental API and may change or // be removed. @@ -2805,6 +3506,8 @@ type ConnectorReconcileRequest struct { // Opaque account selection ID. It must match the account already pinned to the session, if // any. AccountID string `json:"accountId"` + // Optional Connector name to reinitialize. Requires the targetedReconcile capability. + ForceConnectorName *string `json:"forceConnectorName,omitempty"` // When true, refresh the catalog before reconciling. A disabled Connector API performs no // service request. RefreshCatalog *bool `json:"refreshCatalog,omitempty"` @@ -2822,6 +3525,21 @@ type ConnectorRuntimeStatus struct { Status ConnectorMCPStatus `json:"status"` } +// Session account selection. +// Experimental: ConnectorSessionAccount is part of an experimental API and may change or be +// removed. +type ConnectorSessionAccount struct { + // Opaque session-scoped account selection ID. + AccountID string `json:"accountId"` + // Credential-free identity metadata. + AuthInfo AuthIdentityMetadata `json:"authInfo"` +} + +// Session account selection, or null. +// Experimental: ConnectorSessionAccountResult is part of an experimental API and may change +// or be removed. +type ConnectorSessionAccountResult = *ConnectorSessionAccount + // Authoritative session connector state. Account IDs are opaque routing identifiers and // credentials are never included. // Experimental: ConnectorStatus is part of an experimental API and may change or be removed. @@ -3174,6 +3892,10 @@ type CurrentModel struct { // Latest unclaimed Auto preference waiting for a future user turn. Null means the pending // request is returning to provider-default routing. PendingAutoTier *AutoTier `json:"pendingAutoTier,omitempty"` + // Captured base model to restore when leaving plan mode. Omitted outside plan mode or when + // no plan override has captured a base model. Persistent agent model requirements apply to + // this model rather than the temporary plan model. + PlanBaseModelID *string `json:"planBaseModelId,omitempty"` // Reasoning effort level currently applied to the active model, when one is set. Reads // `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the // two values are reported as a snapshot. @@ -3200,6 +3922,33 @@ type CurrentToolMetadata struct { NamespacedName *string `json:"namespacedName,omitempty"` } +// Result of one customization reload component. +// Experimental: CustomizationReloadOutcome is part of an experimental API and may change or +// be removed. +type CustomizationReloadOutcome struct { + // Reason for a skipped component or description of a failure, when available + Detail *string `json:"detail,omitempty"` + // Whether the component reloaded, was skipped, or failed + Status CustomizationReloadStatus `json:"status"` + // Component whose reload was attempted or skipped + Subsystem CustomizationReloadSubsystem `json:"subsystem"` +} + +// Results of reloading discovered session customizations. Inspect outcomes for reloaded, +// skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the +// model-facing prompt and tools apply on the next turn. +// Experimental: CustomizationsReloadResult is part of an experimental API and may change or +// be removed. +type CustomizationsReloadResult struct { + // Errors from any component that could not be refreshed + Errors []string `json:"errors"` + // Outcome of each component in reload order; a skipped component was not configured or + // loaded + Outcomes []CustomizationReloadOutcome `json:"outcomes"` + // Warnings from skill discovery + Warnings []string `json:"warnings"` +} + // A file included in the session debug bundle. // Experimental: DebugCollectLogsCollectedEntry is part of an experimental API and may // change or be removed. @@ -3541,6 +4290,44 @@ type DiscoveredMCPServer struct { Type *DiscoveredMCPServerType `json:"type,omitempty"` } +// A model offered for agent conversations. Missing capability metadata does not disqualify +// a candidate. Models known to be incompatible, such as embedding-only models, are excluded +// by the adapter. +// Experimental: DiscoveredModel is part of an experimental API and may change or be removed. +type DiscoveredModel struct { + // Provider-reported model capabilities. Omitted capability fields are unknown; explicit + // false values are preserved. + Capabilities ModelCapabilities `json:"capabilities"` + // Provider-reported model artifact details. + Details ModelArtifactDetails `json:"details"` + // Provider-reported artifact digest. + Digest *string `json:"digest,omitempty"` + // Provider-native model identifier. + ID string `json:"id"` + // Provider-reported last-modified timestamp. + ModifiedAt *time.Time `json:"modifiedAt,omitempty"` + // Provider-reported display name. + Name *string `json:"name,omitempty"` + // Attribution for the adapter that produced this model row. + Provenance ModelProviderProvenance `json:"provenance"` + // Provider-reported artifact size in bytes. + SizeBytes *int64 `json:"sizeBytes,omitempty"` + // Non-fatal warnings encountered while enriching this model. + Warnings []ModelProviderWarning `json:"warnings"` +} + +// Models offered for agent conversations by one provider instance. Adapters exclude +// known-incompatible models, but retain candidates with unknown capabilities. Listing does +// not guarantee compatibility. +// Experimental: DiscoveredModelList is part of an experimental API and may change or be +// removed. +type DiscoveredModelList struct { + // Provider-native models in provider order. + Models []DiscoveredModel `json:"models"` + // Typed operation outcome. + Outcome ModelProviderOperationOutcome `json:"outcome"` +} + // Slash-prefixed command string to enqueue for FIFO processing. // Experimental: EnqueueCommandParams is part of an experimental API and may change or be // removed. @@ -3584,6 +4371,68 @@ func (UnsupportedEnqueueCommandResult) Queued() bool { return false } +// Hosting capabilities and session capacity advertised by an environment. +// Experimental: EnvironmentCapabilities is part of an experimental API and may change or be +// removed. +type EnvironmentCapabilities struct { + // Advertised Agent Host Protocol version. + AhpVersion *string `json:"ahpVersion,omitempty"` + // Current session count, when advertised. + CurrentSessions *int64 `json:"currentSessions,omitempty"` + // Feature identifiers advertised by the environment. + Features []string `json:"features"` + // Maximum session capacity, when advertised. + MaxSessions *int64 `json:"maxSessions,omitempty"` +} + +// Identify a user-managed Mission Control environment to delete. +// Experimental: EnvironmentsDeleteRequest is part of an experimental API and may change or +// be removed. +type EnvironmentsDeleteRequest struct { + // User-managed environment to delete. GitHub-managed environments cannot be deleted. + EnvironmentID string `json:"environmentId"` +} + +// Acknowledgement that the requested environment was deleted. +// Experimental: EnvironmentsDeleteResult is part of an experimental API and may change or +// be removed. +type EnvironmentsDeleteResult struct { +} + +// Identify a Mission Control environment to retrieve. +// Experimental: EnvironmentsGetRequest is part of an experimental API and may change or be +// removed. +type EnvironmentsGetRequest struct { + // Identifier assigned by Mission Control. + EnvironmentID string `json:"environmentId"` +} + +// Safe discovery information for the requested environment. +// Experimental: EnvironmentsGetResult is part of an experimental API and may change or be +// removed. +type EnvironmentsGetResult struct { + // The requested environment without relay bootstrap credentials. + Environment GitHubEnvironment `json:"environment"` +} + +// Optional discovery filters supported by GitHub Mission Control. +// Experimental: EnvironmentsListRequest is part of an experimental API and may change or be +// removed. +type EnvironmentsListRequest struct { + // Restrict discovery to this compute kind. + Kind *EnvironmentKind `json:"kind,omitempty"` + // Operational status, such as online, offline, degraded, waking, or draining. + Status *string `json:"status,omitempty"` +} + +// Environments visible to the authenticated caller and matching the supplied filters. +// Experimental: EnvironmentsListResult is part of an experimental API and may change or be +// removed. +type EnvironmentsListResult struct { + // Safe discovery records without relay bootstrap credentials. + Environments []GitHubEnvironment `json:"environments"` +} + // Cursor, batch size, and optional long-poll/filter parameters for reading session events. // Experimental: EventLogReadRequest is part of an experimental API and may change or be // removed. @@ -4058,730 +4907,6 @@ type ExternalToolTextResultForLlmContentResourceLinkIcon struct { Theme *ExternalToolTextResultForLlmContentResourceLinkIconTheme `json:"theme,omitempty"` } -// Parameters for cooperatively aborting a factory body. -// Experimental: FactoryAbortRequest is part of an experimental API and may change or be -// removed. -type FactoryAbortRequest struct { - // Opaque token identifying the execution attempt to abort. - ExecutionToken string `json:"executionToken"` - // Factory run identifier. - RunID string `json:"runId"` - // Target session identifier - SessionID string `json:"sessionId"` -} - -// Acknowledgement that a factory request was accepted. -// Experimental: FactoryAckResult is part of an experimental API and may change or be -// removed. -type FactoryAckResult struct { -} - -// Options for one factory-scoped subagent call. -// Experimental: FactoryAgentOptions is part of an experimental API and may change or be -// removed. -type FactoryAgentOptions struct { - // Optional built-in or custom agent name whose definition configures the subagent. - Agent *string `json:"agent,omitempty"` - // Optional context tier override for the subagent. - ContextTier *ContextTier `json:"contextTier,omitempty"` - // Optional label distinguishing otherwise identical memoized agent calls. - Label *string `json:"label,omitempty"` - // Optional model identifier for the subagent. - Model *string `json:"model,omitempty"` - // Optional reasoning effort override for the subagent. - ReasoningEffort *string `json:"reasoningEffort,omitempty"` - // Optional JSON Schema for structured agent output. - Schema any `json:"schema,omitempty"` -} - -// Parameters for one factory-scoped subagent call. -// Experimental: FactoryAgentRequest is part of an experimental API and may change or be -// removed. -type FactoryAgentRequest struct { - // Opaque token identifying the current factory execution attempt. - ExecutionToken string `json:"executionToken"` - // Factory run identifier that owns the subagent. - FactoryRunID string `json:"factoryRunId"` - // Subagent execution options. - Opts FactoryAgentOptions `json:"opts"` - // Prompt to send to the subagent. - Prompt string `json:"prompt"` -} - -// Result of one factory-scoped subagent call. -// Experimental: FactoryAgentResult is part of an experimental API and may change or be -// removed. -type FactoryAgentResult struct { - // Agent result, omitted when the agent produced no result. - Result any `json:"result,omitempty"` -} - -// Prompt-safe durable identity and live status for a direct factory agent. -// Experimental: FactoryAgentSummary is part of an experimental API and may change or be -// removed. -type FactoryAgentSummary struct { - // Accumulated active agent time in milliseconds. - ActiveMs int64 `json:"activeMs"` - // Prompt-safe live activity text. - Activity *string `json:"activity,omitempty"` - // Stable direct-agent identifier. - AgentID string `json:"agentId"` - // Registered agent type. - AgentType string `json:"agentType"` - // Epoch milliseconds when the agent completed. - CompletedAt *int64 `json:"completedAt,omitempty"` - // Friendly, non-unique name intended for display - DisplayName *string `json:"displayName,omitempty"` - // Friendly, non-unique name intended for display - Label string `json:"label"` - // Phase identifier active when the agent was launched, or null. - PhaseID *string `json:"phaseId"` - // Model requested when the agent was launched. - RequestedModel *string `json:"requestedModel,omitempty"` - // Concrete model resolved for the agent. - ResolvedModel *string `json:"resolvedModel,omitempty"` - // Owning factory run identifier. - RunID string `json:"runId"` - // Epoch milliseconds when the agent started. - StartedAt *int64 `json:"startedAt,omitempty"` - // Current durable or live agent status. - Status string `json:"status"` - // Tool-call identifier that launched the agent. - ToolCallID string `json:"toolCallId"` -} - -// Parameters for cancelling a factory run. -// Experimental: FactoryCancelRequest is part of an experimental API and may change or be -// removed. -type FactoryCancelRequest struct { - // Factory run identifier. - RunID string `json:"runId"` -} - -// Current factory phase identity. -// Experimental: FactoryCurrentPhase is part of an experimental API and may change or be -// removed. -type FactoryCurrentPhase struct { - // Current phase identifier. - ID string `json:"id"` - // Zero-based declared phase ordinal, or null for an undeclared phase. - Ordinal *int64 `json:"ordinal"` -} - -// Declared or approved factory resource ceilings. -// Experimental: FactoryDeclaredLimits is part of an experimental API and may change or be -// removed. -type FactoryDeclaredLimits struct { - // Maximum AI credits consumed by subagents and descendants. - MaxAiCredits *float64 `json:"maxAiCredits,omitempty"` - // Maximum concurrently active subagents. - MaxConcurrentSubagents *int64 `json:"maxConcurrentSubagents,omitempty"` - // Maximum total subagents spawned by the run. - MaxTotalSubagents *int64 `json:"maxTotalSubagents,omitempty"` - // Maximum accumulated active execution time in seconds. - TimeoutSeconds *float64 `json:"timeoutSeconds,omitempty"` -} - -// Parameters sent to the owning extension to execute a factory closure. -// Experimental: FactoryExecuteRequest is part of an experimental API and may change or be -// removed. -type FactoryExecuteRequest struct { - // Factory input value. - Args any `json:"args"` - // Opaque token identifying this factory execution attempt. - ExecutionToken string `json:"executionToken"` - // Registered factory name. - Name string `json:"name"` - // Factory run identifier. - RunID string `json:"runId"` - // Target session identifier - SessionID string `json:"sessionId"` -} - -// Result returned by an extension factory closure. -// Experimental: FactoryExecuteResult is part of an experimental API and may change or be -// removed. -type FactoryExecuteResult struct { - // Factory result value. - Result any `json:"result,omitempty"` -} - -// Parameters for paging factory progress. -// Experimental: FactoryGetRunProgressRequest is part of an experimental API and may change -// or be removed. -type FactoryGetRunProgressRequest struct { - // Exclusive forward cursor. - AfterSeq *int64 `json:"afterSeq,omitempty"` - // Exclusive backward cursor. - BeforeSeq *int64 `json:"beforeSeq,omitempty"` - // Maximum records to return. Defaults to 200 and is capped at 500. - Limit *int32 `json:"limit,omitempty"` - // Optional phase identifier used to scope records and cursors. - PhaseID *string `json:"phaseId,omitempty"` - // Factory run identifier. - RunID string `json:"runId"` -} - -// Parameters for retrieving a factory run. -// Experimental: FactoryGetRunRequest is part of an experimental API and may change or be -// removed. -type FactoryGetRunRequest struct { - // Factory run identifier. - RunID string `json:"runId"` -} - -// Parameters for reading a factory journal entry. -// Experimental: FactoryJournalGetRequest is part of an experimental API and may change or -// be removed. -type FactoryJournalGetRequest struct { - // Opaque token identifying the current factory execution attempt. - ExecutionToken string `json:"executionToken"` - // Namespaced journal key. - Key string `json:"key"` - // Factory run identifier. - RunID string `json:"runId"` -} - -// Result of reading a factory journal entry. -// Experimental: FactoryJournalGetResult is part of an experimental API and may change or be -// removed. -type FactoryJournalGetResult struct { - // Whether the journal contained the requested key. - Hit bool `json:"hit"` - // Cached JSON result. The hit field distinguishes a cached JSON null from a miss. - ResultJSON any `json:"resultJson,omitempty"` -} - -// Parameters for storing a factory journal entry. -// Experimental: FactoryJournalPutRequest is part of an experimental API and may change or -// be removed. -type FactoryJournalPutRequest struct { - // Opaque token identifying the current factory execution attempt. - ExecutionToken string `json:"executionToken"` - // Namespaced journal key. - Key string `json:"key"` - // JSON result to memoize. - ResultJSON any `json:"resultJson"` - // Factory run identifier. - RunID string `json:"runId"` -} - -// Parameters for paging factory runs. -// Experimental: FactoryListRunsRequest is part of an experimental API and may change or be -// removed. -type FactoryListRunsRequest struct { - // Exclusive forward cursor. - AfterSeq *int64 `json:"afterSeq,omitempty"` - // Exclusive backward cursor. - BeforeSeq *int64 `json:"beforeSeq,omitempty"` - // Maximum terminal runs to return. Defaults to 200 and is capped at 500. - Limit *int32 `json:"limit,omitempty"` -} - -// A page of factory runs in durable creation order. -// Experimental: FactoryListRunsResult is part of an experimental API and may change or be -// removed. -type FactoryListRunsResult struct { - // Whether terminal runs newer than this page exist. - HasMoreNewer *bool `json:"hasMoreNewer,omitempty"` - // Newest terminal-run cursor in this page, or null when the terminal window is empty. - NewestSeq *int64 `json:"newestSeq,omitempty"` - // Oldest terminal-run cursor in this page, or null when the terminal window is empty. - OldestSeq *int64 `json:"oldestSeq,omitempty"` - // Number of terminal runs older than this page. - OmittedOlder *int64 `json:"omittedOlder,omitempty"` - // Factory run summaries in durable creation order. - Runs []FactoryRunSummary `json:"runs"` -} - -// One ordered factory progress line. -// Experimental: FactoryLogLine is part of an experimental API and may change or be removed. -type FactoryLogLine struct { - // Progress line kind. - Kind FactoryLogLineKind `json:"kind"` - // Monotonic sequence number within the factory run. - Seq int64 `json:"seq"` - // Progress text. - Text string `json:"text"` -} - -// Parameters for recording factory progress. -// Experimental: FactoryLogRequest is part of an experimental API and may change or be -// removed. -type FactoryLogRequest struct { - // Opaque token identifying the current factory execution attempt. - ExecutionToken string `json:"executionToken"` - // Ordered progress lines to append. - Lines []FactoryLogLine `json:"lines"` - // Factory run identifier. - RunID string `json:"runId"` -} - -// Parameters for an owned durable pause checkpoint. -// Experimental: FactoryPauseCheckpointRequest is part of an experimental API and may change -// or be removed. -type FactoryPauseCheckpointRequest struct { - // Opaque token identifying the execution attempt that reached the checkpoint. - ExecutionToken string `json:"executionToken"` - // Stable author-defined checkpoint key. - Key string `json:"key"` - // Factory run identifier. - RunID string `json:"runId"` -} - -// Experimental: FactoryPauseCheckpointResult is part of an experimental API and may change -// or be removed. -type FactoryPauseCheckpointResult struct { - // Whether this execution attempt must pause or may continue. - Action FactoryPauseCheckpointAction `json:"action"` -} - -// Durable metadata describing who initiated a factory pause. -// Experimental: FactoryPauseInfo is part of an experimental API and may change or be -// removed. -type FactoryPauseInfo interface { - factoryPauseInfo() - Type() FactoryPauseInfoType -} - -type RawFactoryPauseInfoData struct { - Discriminator FactoryPauseInfoType - Raw json.RawMessage -} - -func (RawFactoryPauseInfoData) factoryPauseInfo() {} -func (r RawFactoryPauseInfoData) Type() FactoryPauseInfoType { - return r.Discriminator -} - -type FactoryPauseInfoCheckpoint struct { - // Stable author-defined checkpoint key that initiated the pause. - Key string `json:"key"` -} - -func (FactoryPauseInfoCheckpoint) factoryPauseInfo() {} -func (FactoryPauseInfoCheckpoint) Type() FactoryPauseInfoType { - return FactoryPauseInfoTypeCheckpoint -} - -type FactoryPauseInfoUser struct { -} - -func (FactoryPauseInfoUser) factoryPauseInfo() {} -func (FactoryPauseInfoUser) Type() FactoryPauseInfoType { - return FactoryPauseInfoTypeUser -} - -// Parameters for pausing a running factory. -// Experimental: FactoryPauseRequest is part of an experimental API and may change or be -// removed. -type FactoryPauseRequest struct { - // Factory run identifier. - RunID string `json:"runId"` -} - -// Durable lifecycle and timing for one factory phase. -// Experimental: FactoryPhaseObservation is part of an experimental API and may change or be -// removed. -type FactoryPhaseObservation struct { - // Completed active time accumulated by this phase in milliseconds. - AccumulatedActiveMs int64 `json:"accumulatedActiveMs"` - // Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip - // timestamp (equal to `startedAt`). - CompletedAt *int64 `json:"completedAt,omitempty"` - // Current live active time for this phase in milliseconds. - CurrentActiveMs int64 `json:"currentActiveMs"` - // Optional human-readable phase detail. - Detail *string `json:"detail,omitempty"` - // Number of times execution entered this phase. - EntryCount int64 `json:"entryCount"` - // Phase identifier. - ID string `json:"id"` - // Most recent run attempt that entered this phase, or `0` if the phase has never been - // entered. - LastEnteredRunAttempt int64 `json:"lastEnteredRunAttempt"` - // Direct agents in this phase that are currently live. - LiveAgentCount int64 `json:"liveAgentCount"` - // Zero-based declared phase ordinal, or null for an undeclared phase. - Ordinal *int64 `json:"ordinal"` - // Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip - // timestamp (equal to `completedAt`). - StartedAt *int64 `json:"startedAt,omitempty"` - // Derived lifecycle state of the phase. - Status FactoryPhaseStatus `json:"status"` - // Human-readable phase title. - Title string `json:"title"` - // Total direct agents associated with this phase. - TotalAgentCount int64 `json:"totalAgentCount"` -} - -// One durable factory progress record. -// Experimental: FactoryProgressLine is part of an experimental API and may change or be -// removed. -type FactoryProgressLine struct { - // Resume attempt that emitted this record. - Attempt int64 `json:"attempt"` - // Progress record kind. - Kind FactoryLogLineKind `json:"kind"` - // Phase active when the record was emitted, or null before any phase. - PhaseID *string `json:"phaseId"` - // Epoch milliseconds when the record was persisted. - RecordedAt int64 `json:"recordedAt"` - // Global monotonic sequence number within the run. - Seq int64 `json:"seq"` - // Prompt-safe progress text. - Text string `json:"text"` -} - -// A bidirectional page of factory progress. -// Experimental: FactoryProgressPage is part of an experimental API and may change or be -// removed. -type FactoryProgressPage struct { - // Whether progress records newer than this page exist. - HasMoreNewer bool `json:"hasMoreNewer"` - // Whether progress records older than this page exist. - HasMoreOlder bool `json:"hasMoreOlder"` - // Newest sequence number in this page, or null when empty. - NewestSeq *int64 `json:"newestSeq"` - // Oldest sequence number in this page, or null when empty. - OldestSeq *int64 `json:"oldestSeq"` - // Progress records in sequence order. - Records []FactoryProgressLine `json:"records"` - // Run revision reflected by this page. - Revision int64 `json:"revision"` -} - -// Parameters for resuming a factory run from its persisted identity. -// Experimental: FactoryResumeRequest is part of an experimental API and may change or be -// removed. -type FactoryResumeRequest struct { - // Optional per-invocation resource ceiling overrides. - Limits *FactoryRunLimits `json:"limits,omitempty"` - // Whether to emit factory phase names to the session transcript. - LogPhaseNames *bool `json:"logPhaseNames,omitempty"` - // Whether to notify the originating session when the factory completes. - NotifyOnComplete *bool `json:"notifyOnComplete,omitempty"` - // Factory run identifier. - RunID string `json:"runId"` -} - -// Resolved persisted factory identity and resumed run envelope. -// Experimental: FactoryResumeResult is part of an experimental API and may change or be -// removed. -type FactoryResumeResult struct { - // Persisted factory name resolved for the resumed run. - FactoryName string `json:"factoryName"` - // Terminal resumed run envelope. - Run FactoryRunResult `json:"run"` -} - -// Durable factory resource consumption. -// Experimental: FactoryRunConsumed is part of an experimental API and may change or be -// removed. -type FactoryRunConsumed struct { - // Accumulated active execution time in milliseconds. - ActiveMs int64 `json:"activeMs"` - // AI usage consumed by the run in nano-AIU. - NanoAiu int64 `json:"nanoAiu"` - // Total subagents spawned by the run. - Subagents int64 `json:"subagents"` -} - -// Full factory run observability detail. -// Experimental: FactoryRunDetail is part of an experimental API and may change or be -// removed. -type FactoryRunDetail struct { - // Epoch milliseconds when the current active segment started, or null while inactive. - ActiveSegmentStartedAt *int64 `json:"activeSegmentStartedAt"` - // Durable identities and live statuses for direct factory agents. - Agents []FactoryAgentSummary `json:"agents"` - // Approved effective resource ceilings, or null until approved. - Approved *FactoryDeclaredLimits `json:"approved"` - // Whether the durable run state currently passes runtime resume eligibility checks. - CanResume bool `json:"canResume"` - // Epoch milliseconds when the run completed, or null while nonterminal. - CompletedAt *int64 `json:"completedAt"` - // Durable resource consumption. - Consumed FactoryRunConsumed `json:"consumed"` - // Epoch milliseconds when the run was created. - CreatedAt int64 `json:"createdAt"` - // Current phase identity, or null before any phase is entered. - CurrentPhase *FactoryCurrentPhase `json:"currentPhase"` - // Resource ceilings declared by the factory. - DeclaredLimits FactoryDeclaredLimits `json:"declaredLimits"` - // Number of phases declared by the factory. - DeclaredPhaseCount int64 `json:"declaredPhaseCount"` - // Human-readable factory description. - Description string `json:"description"` - // Registered factory name. - FactoryName string `json:"factoryName"` - // Number of direct factory agents currently live. - LiveAgentCount int64 `json:"liveAgentCount"` - // Epoch milliseconds when this live-overlay snapshot was observed. - ObservedAt int64 `json:"observedAt"` - // Lifecycle and timing observations for each factory phase. - Phases []FactoryPhaseObservation `json:"phases"` - // Bidirectional page of durable factory progress. - Progress FactoryProgressPage `json:"progress"` - // Monotonic durable run revision. - Revision int64 `json:"revision"` - // Factory run identifier. - RunID string `json:"runId"` - // Epoch milliseconds when execution first started, or null before start. - StartedAt *int64 `json:"startedAt"` - // Current factory run status. - Status FactoryRunStatus `json:"status"` - // Terminal run outcome, or null while nonterminal. - Terminal *FactoryRunTerminal `json:"terminal"` - // Total direct factory agents spawned across all attempts. - TotalSpawnedAgentCount int64 `json:"totalSpawnedAgentCount"` - // Epoch milliseconds when the durable run was last updated. - UpdatedAt int64 `json:"updatedAt"` -} - -// Machine-readable factory run failure. -// Experimental: FactoryRunFailure is part of an experimental API and may change or be -// removed. -type FactoryRunFailure interface { - factoryRunFailure() - Type() FactoryRunFailureType -} - -type RawFactoryRunFailureData struct { - Discriminator FactoryRunFailureType - Raw json.RawMessage -} - -func (RawFactoryRunFailureData) factoryRunFailure() {} -func (r RawFactoryRunFailureData) Type() FactoryRunFailureType { - return r.Discriminator -} - -// The run stopped because its usage accounting could not be completed. -type FactoryRunFailureFactoryAccountingIncomplete struct { - // Confirmed usage in nano-AIU, representing the floor of what the run spent. - DrainedNanoAiu int64 `json:"drainedNanoAiu"` - // Factory run identifier. - RunID string `json:"runId"` -} - -func (FactoryRunFailureFactoryAccountingIncomplete) factoryRunFailure() {} -func (FactoryRunFailureFactoryAccountingIncomplete) Type() FactoryRunFailureType { - return FactoryRunFailureTypeFactoryAccountingIncomplete -} - -type FactoryRunFailureFactoryDurableFailure struct { - // Stable failure code. - Code string `json:"code"` - // Execution-critical durable operation that failed. - Operation FactoryDurableOperation `json:"operation"` - // Factory run identifier. - RunID string `json:"runId"` -} - -func (FactoryRunFailureFactoryDurableFailure) factoryRunFailure() {} -func (FactoryRunFailureFactoryDurableFailure) Type() FactoryRunFailureType { - return FactoryRunFailureTypeFactoryDurableFailure -} - -type FactoryRunFailureFactoryLimitReached struct { - // Resource ceiling that stopped the run. - Kind FactoryRunFailureKind `json:"kind"` - // Factory run identifier. - RunID string `json:"runId"` - // Suggested larger ceiling when the runtime can derive one safely. - SuggestedValue *float64 `json:"suggestedValue,omitempty"` - // Approved effective ceiling that was reached. - Value float64 `json:"value"` -} - -func (FactoryRunFailureFactoryLimitReached) factoryRunFailure() {} -func (FactoryRunFailureFactoryLimitReached) Type() FactoryRunFailureType { - return FactoryRunFailureTypeFactoryLimitReached -} - -// The extension that owns the factory 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. -type FactoryRunFailureFactoryProviderDisconnected struct { - // Factory run identifier. - RunID string `json:"runId"` -} - -func (FactoryRunFailureFactoryProviderDisconnected) factoryRunFailure() {} -func (FactoryRunFailureFactoryProviderDisconnected) Type() FactoryRunFailureType { - return FactoryRunFailureTypeFactoryProviderDisconnected -} - -type FactoryRunFailureFactoryResumeDeclined struct { - // Human-readable reason the resume did not proceed. - Reason string `json:"reason"` - // Factory run identifier whose changed limits were declined. - RunID string `json:"runId"` -} - -func (FactoryRunFailureFactoryResumeDeclined) factoryRunFailure() {} -func (FactoryRunFailureFactoryResumeDeclined) Type() FactoryRunFailureType { - return FactoryRunFailureTypeFactoryResumeDeclined -} - -// Wire-only per-invocation factory resource ceiling overrides. -// Experimental: FactoryRunLimits is part of an experimental API and may change or be -// removed. -type FactoryRunLimits struct { - // Maximum AI credits consumed by factory subagents and their descendants. The post-paid - // ceiling is soft: parallel turns can settle beyond it before the run stops. - MaxAiCredits *float64 `json:"maxAiCredits,omitempty"` - // Maximum number of factory subagents that may run concurrently. - MaxConcurrentSubagents *int64 `json:"maxConcurrentSubagents,omitempty"` - // Maximum total number of factory subagents that may be admitted. - MaxTotalSubagents *int64 `json:"maxTotalSubagents,omitempty"` - // 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. - TimeoutSeconds *float64 `json:"timeoutSeconds,omitempty"` -} - -// Parameters for invoking a registered factory. -// Experimental: FactoryRunRequest is part of an experimental API and may change or be -// removed. -type FactoryRunRequest struct { - // Factory input value. - Args any `json:"args"` - // Registered factory name. - Name string `json:"name"` - // Factory invocation options. - Options *RunOptions `json:"options,omitempty"` -} - -// Complete current or terminal factory run envelope. -// Experimental: FactoryRunResult is part of an experimental API and may change or be -// removed. -type FactoryRunResult struct { - // One-based execution attempt represented by this envelope. Absent before the first attempt - // starts or when returned by an older runtime. - Attempt *int64 `json:"attempt,omitempty"` - // Error message for an errored run. - Error *string `json:"error,omitempty"` - // Machine-readable failure details for a halted or errored run. - Failure FactoryRunFailure `json:"failure,omitempty"` - // Structured pause initiator metadata for a paused attempt. - PauseInfo FactoryPauseInfo `json:"pauseInfo,omitempty"` - // Reason for a halted or cancelled run. - Reason *string `json:"reason,omitempty"` - // Completed factory result. - Result any `json:"result,omitempty"` - // Factory run identifier. - RunID string `json:"runId"` - // Partial journal and progress snapshot for a halted, cancelled, or errored run. - Snapshot any `json:"snapshot,omitempty"` - // Current or terminal factory run status. - Status FactoryRunStatus `json:"status"` -} - -// Durable factory run summary with read-time live overlays. -// Experimental: FactoryRunSummary is part of an experimental API and may change or be -// removed. -type FactoryRunSummary struct { - // Epoch milliseconds when the current active segment started, or null while inactive. - ActiveSegmentStartedAt *int64 `json:"activeSegmentStartedAt"` - // Approved effective resource ceilings, or null until approved. - Approved *FactoryDeclaredLimits `json:"approved"` - // Whether the durable run state currently passes runtime resume eligibility checks. - CanResume bool `json:"canResume"` - // Epoch milliseconds when the run completed, or null while nonterminal. - CompletedAt *int64 `json:"completedAt"` - // Durable resource consumption. - Consumed FactoryRunConsumed `json:"consumed"` - // Epoch milliseconds when the run was created. - CreatedAt int64 `json:"createdAt"` - // Current phase identity, or null before any phase is entered. - CurrentPhase *FactoryCurrentPhase `json:"currentPhase"` - // Resource ceilings declared by the factory. - DeclaredLimits FactoryDeclaredLimits `json:"declaredLimits"` - // Number of phases declared by the factory. - DeclaredPhaseCount int64 `json:"declaredPhaseCount"` - // Human-readable factory description. - Description string `json:"description"` - // Registered factory name. - FactoryName string `json:"factoryName"` - // Number of direct factory agents currently live. - LiveAgentCount int64 `json:"liveAgentCount"` - // Epoch milliseconds when this live-overlay snapshot was observed. - ObservedAt int64 `json:"observedAt"` - // Monotonic durable run revision. - Revision int64 `json:"revision"` - // Factory run identifier. - RunID string `json:"runId"` - // Epoch milliseconds when execution first started, or null before start. - StartedAt *int64 `json:"startedAt"` - // Current factory run status. - Status FactoryRunStatus `json:"status"` - // Terminal run outcome, or null while nonterminal. - Terminal *FactoryRunTerminal `json:"terminal"` - // Total direct factory agents spawned across all attempts. - TotalSpawnedAgentCount int64 `json:"totalSpawnedAgentCount"` - // Epoch milliseconds when the durable run was last updated. - UpdatedAt int64 `json:"updatedAt"` -} - -// Prompt-safe terminal factory outcome. -// Experimental: FactoryRunTerminal is part of an experimental API and may change or be -// removed. -type FactoryRunTerminal struct { - // Human-readable terminal error. - Error *string `json:"error,omitempty"` - // Machine-readable terminal failure. - Failure FactoryRunFailure `json:"failure,omitempty"` - // Pause initiator metadata, or null when the run did not pause. - PauseInfo FactoryPauseInfo `json:"pauseInfo"` - // Human-readable terminal reason. - Reason *string `json:"reason,omitempty"` - // Prompt-safe preview of the completed result. - ResultPreview *string `json:"resultPreview,omitempty"` -} - -// Internal parameters for resuming a factory run from a tool. -// Experimental: FactoryToolResumeRequest is part of an experimental API and may change or -// be removed. -// Internal: FactoryToolResumeRequest is an internal SDK API and is not part of the public -// surface. -type FactoryToolResumeRequest struct { - // Optional per-invocation resource ceiling overrides. - Limits *FactoryRunLimits `json:"limits,omitempty"` - // Factory run identifier. - RunID string `json:"runId"` - // Opaque identifier of the originating tool call. - ToolCallID *string `json:"toolCallId,omitempty"` -} - -// Options for an internal tool-originated factory invocation. -// Experimental: FactoryToolRunOptions is part of an experimental API and may change or be -// removed. -// Internal: FactoryToolRunOptions is an internal SDK API and is not part of the public -// surface. -type FactoryToolRunOptions struct { - // Per-invocation resource ceiling overrides. - Limits *FactoryRunLimits `json:"limits,omitempty"` - // Run identifier whose journal and progress should seed this resumed run. - ResumeFromRunID *string `json:"resumeFromRunId,omitempty"` -} - -// Internal parameters for invoking a registered factory from a tool. -// Experimental: FactoryToolRunRequest is part of an experimental API and may change or be -// removed. -// Internal: FactoryToolRunRequest is an internal SDK API and is not part of the public -// surface. -type FactoryToolRunRequest struct { - // Factory input value. - Args any `json:"args"` - // Registered factory name. - Name string `json:"name"` - // Tool-originated factory invocation options. - Options *FactoryToolRunOptions `json:"options,omitempty"` - // Opaque identifier of the originating tool call. - ToolCallID *string `json:"toolCallId,omitempty"` -} - // Content filtering mode to apply to all tools, or a map of tool name to content filtering // mode. // Experimental: FilterMapping is part of an experimental API and may change or be removed. @@ -4847,6 +4972,137 @@ type FolderTrustCheckResult struct { Trusted bool `json:"trusted"` } +// The remote the checked-out branch tracks. +// Experimental: GitCurrentBranchRemoteResult is part of an experimental API and may change +// or be removed. +// Internal: GitCurrentBranchRemoteResult is an internal SDK API and is not part of the +// public surface. +type GitCurrentBranchRemoteResult struct { + // Name of the tracked remote. Reports `origin` whenever the working tree has no tracking + // configuration to read, including on a detached HEAD, so this is never null and never + // empty. + Remote string `json:"remote"` +} + +// Working-tree path a git query applies to. +// Experimental: GitCwdRequest is part of an experimental API and may change or be removed. +// Internal: GitCwdRequest is an internal SDK API and is not part of the public surface. +type GitCwdRequest struct { + // Absolute path to a directory inside the git working tree to query. + Cwd string `json:"cwd"` +} + +// Safe discovery information. Host-side relay bootstrap credentials are never included. +// Experimental: GitHubEnvironment is part of an experimental API and may change or be +// removed. +type GitHubEnvironment struct { + // Hosting capabilities advertised by the environment. + Capabilities *EnvironmentCapabilities `json:"capabilities,omitempty"` + // Identifier assigned by Mission Control. + ID string `json:"id"` + // Compute kind reported by Mission Control. + Kind EnvironmentKind `json:"kind"` + // Discovery labels attached to the environment. + Labels map[string]string `json:"labels,omitzero"` + // Timestamp of the last heartbeat received by Mission Control. + LastHeartbeatAt *string `json:"lastHeartbeatAt,omitempty"` + // Human-readable environment name. + Name string `json:"name"` + // Organization identifier, when the environment belongs to an organization. + OrgID *string `json:"orgId,omitempty"` + // Identifier of the environment owner. + OwnerID *string `json:"ownerId,omitempty"` + // Owner category reported by Mission Control. + OwnerType *string `json:"ownerType,omitempty"` + // Open-ended operational status vocabulary. + Status string `json:"status"` +} + +// A GitHub login the authenticated user may act as: their own account, or an organization +// they belong to. +// Experimental: GitHubOwnerOption is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnerOption is an internal SDK API and is not part of the public surface. +type GitHubOwnerOption struct { + // The owner's GitHub login. + Login string `json:"login"` + // Which kind of owner this is. The authenticated user's own account is always reported as + // `user`. + Type string `json:"type"` +} + +// The owner listing to abandon. +// Experimental: GitHubOwnersCancelRequest is part of an experimental API and may change or +// be removed. +// Internal: GitHubOwnersCancelRequest is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersCancelRequest struct { + // Request id the listing was started with. + RequestID int64 `json:"requestId"` +} + +// Whether the id named a running owner listing. +// Experimental: GitHubOwnersCancelResult is part of an experimental API and may change or +// be removed. +// Internal: GitHubOwnersCancelResult is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersCancelResult struct { + // True when a listing with the id was running and the cancel stopped it. False when the id + // was never registered, was registered but unused, was released after being abandoned, or + // its listing had ended. An unused id is released and cannot start a later listing. + Canceled bool `json:"canceled"` +} + +// Credential to list owners under, and the request id that makes the listing cancellable. +// Experimental: GitHubOwnersListRequest is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnersListRequest is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersListRequest struct { + // The credential the listing runs under, carried opaquely because its shape is the host's + // own and the runtime only resolves a token and a GitHub host from it. No credential + // travels: this selects one the runtime already holds. + AuthInfo any `json:"authInfo"` + // Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled + // before use, released after being abandoned, or already used is refused rather than + // silently running uncancellable. + RequestID int64 `json:"requestId"` +} + +// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except +// that `throwError` reports a failure the caller is expected to raise rather than render. +// Experimental: GitHubOwnersListResult is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnersListResult is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersListResult struct { + // Why no owners could be listed, phrased for a user. Present when the listing failed in a + // way the caller should render rather than raise. + Message *string `json:"message,omitempty"` + // The owners, on success: the authenticated user first, then the organizations they belong + // to. + // Internal: Owners is part of the SDK's internal API surface and is not intended for + // external use. + Owners []GitHubOwnerOption `json:"owners,omitzero"` + // A malformed request or an unreadable credential, which the caller raises instead of + // rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, + // which the caller renders. + ThrowError *string `json:"throwError,omitempty"` + // A line the caller should log. Present only alongside `message`, and only for failures + // worth recording. + Warning *string `json:"warning,omitempty"` +} + +// A freshly registered request id. Registering it before the listing starts is what lets a +// cancel that races the request still find the owner listing slot. The id serves one +// listing only. Long-abandoned unused ids can be released by later allocations. +// Experimental: GitHubOwnersRequestIDResult is part of an experimental API and may change +// or be removed. +type GitHubOwnersRequestIDResult struct { + // Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + RequestID int64 `json:"requestId"` +} + // Pointer to a GitHub repository. // Experimental: GitHubRepoRef is part of an experimental API and may change or be removed. type GitHubRepoRef struct { @@ -4858,6 +5114,39 @@ type GitHubRepoRef struct { Owner string `json:"owner"` } +// Working-tree path whose owning GitHub repository should be resolved. +// Experimental: GitHubRepositoryAtPathRequest is part of an experimental API and may change +// or be removed. +// Internal: GitHubRepositoryAtPathRequest is an internal SDK API and is not part of the +// public surface. +type GitHubRepositoryAtPathRequest struct { + // Absolute path to a directory inside the git working tree to resolve. + Path string `json:"path"` +} + +// The GitHub repository that owns the requested path, when the selected remote (`origin`, +// else the first) is on a GitHub host. +// Experimental: GitHubRepositoryAtPathResult is part of an experimental API and may change +// or be removed. +// Internal: GitHubRepositoryAtPathResult is an internal SDK API and is not part of the +// public surface. +type GitHubRepositoryAtPathResult struct { + // Resolved repository identity, or null when the selected remote resolves to no GitHub host. + Repository *GitHubRepositoryIdentity `json:"repository,omitempty"` +} + +// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. +// Experimental: GitHubRepositoryIdentity is part of an experimental API and may change or +// be removed. +type GitHubRepositoryIdentity struct { + // Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + Host string `json:"host"` + // Repository name, without the owner prefix or the `.git` suffix. + Name string `json:"name"` + // Repository owner login (user or organization). + Owner string `json:"owner"` +} + // Client environment metadata describing the process that produced a telemetry event. // Experimental: GitHubTelemetryClientInfo is part of an experimental API and may change or // be removed. @@ -4994,6 +5283,142 @@ func (GitHubTokenAcquireResultToken) Kind() GitHubTokenAcquireResultKind { return GitHubTokenAcquireResultKindToken } +// A GitHub repository one of a working tree's remotes points at. +// Experimental: GitRemoteRepository is part of an experimental API and may change or be +// removed. +// Internal: GitRemoteRepository is an internal SDK API and is not part of the public +// surface. +type GitRemoteRepository struct { + // GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise + // remote. + Host string `json:"host"` + // Repository name, without the owner. + Name string `json:"name"` + // Account or organization owning the repository. + Owner string `json:"owner"` + // Name of the first remote that produced this distinct repository entry, such as `origin` + // or `upstream`. + RemoteName string `json:"remoteName"` +} + +// Git working tree whose GitHub remotes should be listed. +// Experimental: GitReposFromRemotesRequest is part of an experimental API and may change or +// be removed. +// Internal: GitReposFromRemotesRequest is an internal SDK API and is not part of the public +// surface. +type GitReposFromRemotesRequest struct { + // Absolute path to the root of the git working tree. + GitRoot string `json:"gitRoot"` +} + +// The GitHub repositories a working tree's remotes point at. +// Experimental: GitReposFromRemotesResult is part of an experimental API and may change or +// be removed. +// Internal: GitReposFromRemotesResult is an internal SDK API and is not part of the public +// surface. +type GitReposFromRemotesResult struct { + // One entry per distinct GitHub repository, in the order git reports the first remote for + // each repository. Empty when no remote points at a GitHub host, which a caller should read + // as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + // Internal: Repositories is part of the SDK's internal API surface and is not intended for + // external use. + Repositories []GitRemoteRepository `json:"repositories"` +} + +// Selects the configuration directory whose machine-wide state to read. +// Experimental: GlobalStateLoadForConfigDirRequest is part of an experimental API and may +// change or be removed. +// Internal: GlobalStateLoadForConfigDirRequest is an internal SDK API and is not part of +// the public surface. +type GlobalStateLoadForConfigDirRequest struct { + // Copilot configuration directory to read the state document from, taking precedence over + // the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + // read the directory the server resolved for itself. + ConfigDir *string `json:"configDir,omitempty"` +} + +// The host's machine-wide state. Every field is optional because a fresh install has +// recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Experimental: GlobalStateLoadResult is part of an experimental API and may change or be +// removed. +// Internal: GlobalStateLoadResult is an internal SDK API and is not part of the public +// surface. +type GlobalStateLoadResult struct { + // Whether the user has answered the prompt suggesting they install the desktop app. + AppInstallNudgeResponded *bool `json:"appInstallNudgeResponded,omitempty"` + // Whether the app tip has been shown. + AppTipShown *bool `json:"appTipShown,omitempty"` + // Terminals the user has already been asked to set up, so the host does not ask twice. + AskedSetupTerminals []string `json:"askedSetupTerminals,omitzero"` + // When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the + // once-per-day cap for non-staff users across restarts. + AutoFeedbackLastPromptedAt *string `json:"autoFeedbackLastPromptedAt,omitempty"` + // When the host first ran on this machine. + FirstLaunchAt *string `json:"firstLaunchAt,omitempty"` + // Plugins installed on this machine. + InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` + // Account used for the most recent sign-in. + // Internal: LastLoggedInUser is part of the SDK's internal API surface and is not intended + // for external use. + LastLoggedInUser *LoggedInUser `json:"lastLoggedInUser,omitempty"` + // Every account the host has signed in to on this machine. + // Internal: LoggedInUsers is part of the SDK's internal API surface and is not intended for + // external use. + LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitzero"` + // Whether the one-off cleanup of stored reasoning summaries has run. + ReasoningSummariesCleanupDone *bool `json:"reasoningSummariesCleanupDone,omitempty"` + // Models the user selected recently, most recent first. + RecentModelIDs []string `json:"recentModelIds,omitzero"` + // Whether the user declined to trust the sandbox credential proxy CA. + SandboxCredentialProxyCaDeclined *bool `json:"sandboxCredentialProxyCaDeclined,omitempty"` + // Whether the sandbox onboarding has been shown. + SandboxOnboardingShown *bool `json:"sandboxOnboardingShown,omitempty"` + // Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only + // behavior. + Staff *bool `json:"staff,omitempty"` + // Whether the user was recognized as GitHub staff. + StaffGitHub *bool `json:"staffGithub,omitempty"` + // When the staff-only log level migration last ran. + StaffLogLevelMigrationAt *string `json:"staffLogLevelMigrationAt,omitempty"` + // Whether the user was recognized as Microsoft staff. + StaffMicrosoft *bool `json:"staffMicrosoft,omitempty"` + // When the staff-only model reset last ran. + StaffModelResetAt *string `json:"staffModelResetAt,omitempty"` + // When the staff-only update channel migration last ran. + StaffUpdateChannelMigrationAt *string `json:"staffUpdateChannelMigrationAt,omitempty"` + // Folders where the user declined the init prompt, so it stays hidden there. + SuppressInitFolders []string `json:"suppressInitFolders,omitzero"` + // Folders the user has marked as trusted. + TrustedFolders []string `json:"trustedFolders,omitzero"` +} + +// A single top-level key to record in the host's machine-wide state. The write replaces +// only that key and leaves the rest of the document untouched, so two writers recording +// different one-off flags do not overwrite each other. The stored credential keys cannot be +// written through this method. +// Experimental: GlobalStateWriteKeyRequest is part of an experimental API and may change or +// be removed. +// Internal: GlobalStateWriteKeyRequest is an internal SDK API and is not part of the public +// surface. +type GlobalStateWriteKeyRequest struct { + // Copilot configuration directory to write the state document in, taking precedence over + // the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + // write the directory the server resolved for itself. Mirrors + // `globalState.loadForConfigDir`, so a caller can read and write the same directory. + ConfigDir *string `json:"configDir,omitempty"` + // Top-level key to write, named as it appears in the result of `globalState.load`. It must + // be one of the writable keys that `globalState.writeKey` lists. + Key string `json:"key"` + // Value to store for the key. Omit it, or pass null, to remove the key instead. + Value any `json:"value,omitempty"` +} + +// Experimental: GlobalStateWriteKeyResult is part of an experimental API and may change or +// be removed. +type GlobalStateWriteKeyResult struct { +} + // Pending external tool call request ID, with the tool result or an error describing why it // failed. // Experimental: HandlePendingToolCallRequest is part of an experimental API and may change @@ -5325,6 +5750,423 @@ type HooksDiscoverResult struct { Warnings []string `json:"warnings"` } +// Normalized listener settings delivered only to the supervised hosting participant. +// Experimental: HostConfiguration is part of an experimental API and may change or be +// removed. +type HostConfiguration struct { + // Requested GitHub Mission Control registration. + GitHubEnvironment *HostGitHubEnvironmentOptions `json:"githubEnvironment,omitempty"` + // Normalized local listener settings, absent for relay-only hosts. + LocalServer *HostLocalServerConfiguration `json:"localServer,omitempty"` + // Whether app-owned durable sessions are resumed by the owning application. + ResumeFactory bool `json:"resumeFactory"` + // Whether session materialization is delegated to the owning application. + SessionFactory bool `json:"sessionFactory"` +} + +// The resident session the application has materialized on its own connection. +// Experimental: HostCreateSessionResult is part of an experimental API and may change or be +// removed. +type HostCreateSessionResult struct { + // Runtime session UUID materialized on the application's SDK connection. + SessionID string `json:"sessionId"` +} + +// Stops a connection-owned listener and joins its teardown. +// Experimental: HostDisposeRequest is part of an experimental API and may change or be +// removed. +type HostDisposeRequest struct { + // Listener UUID. Unknown or successfully stopped IDs are harmless. + HostID string `json:"hostId"` +} + +// Empty acknowledgement for a completed host lifecycle operation. +// Experimental: HostDisposeResult is part of an experimental API and may change or be +// removed. +type HostDisposeResult struct { +} + +// Empty acknowledgement for a completed host lifecycle operation. +// Experimental: HostEmptyResult is part of an experimental API and may change or be removed. +type HostEmptyResult struct { +} + +// Private credentials delivered only to a runtime-owned Mission Control hosting participant. +// Experimental: HostEnvironmentCredentials is part of an experimental API and may change or +// be removed. +type HostEnvironmentCredentials struct { + // GitHub API base URL for the authenticated service. + GitHubAPIURL string `json:"githubApiUrl"` + // Hostname of the authenticated GitHub service. + GitHubHost string `json:"githubHost"` + // Mission Control API origin for environment registration and management. + MissionControlURL string `json:"missionControlUrl"` + // Current bearer token for the authenticated GitHub identity. + Token string `json:"token"` +} + +// Reports a supervised listener's hosting-task termination and cleanup outcome. +// Experimental: HostExitedNotification is part of an experimental API and may change or be +// removed. +type HostExitedNotification struct { + // Explicit startup or teardown failure, when present. + Error *string `json:"error,omitempty"` + // Process exit status when available; absent for in-process listener tasks. + ExitCode *int64 `json:"exitCode,omitempty"` + // Listener UUID. + HostID string `json:"hostId"` + // Cause of termination. + Reason HostExitReason `json:"reason"` +} + +// Experimental: HostExitedResult is part of an experimental API and may change or be +// removed. +type HostExitedResult struct { +} + +// Normalized listener settings delivered only to the supervised hosting participant. +// Experimental: HostGetConfigurationResult is part of an experimental API and may change or +// be removed. +type HostGetConfigurationResult struct { + // Requested GitHub Mission Control registration. + GitHubEnvironment *HostGitHubEnvironmentOptions `json:"githubEnvironment,omitempty"` + // Normalized local listener settings, absent for relay-only hosts. + LocalServer *HostLocalServerConfiguration `json:"localServer,omitempty"` + // Whether app-owned durable sessions are resumed by the owning application. + ResumeFactory bool `json:"resumeFactory"` + // Whether session materialization is delegated to the owning application. + SessionFactory bool `json:"sessionFactory"` +} + +// Private credentials delivered only to a runtime-owned Mission Control hosting participant. +// Experimental: HostGetEnvironmentCredentialsResult is part of an experimental API and may +// change or be removed. +type HostGetEnvironmentCredentialsResult struct { + // GitHub API base URL for the authenticated service. + GitHubAPIURL string `json:"githubApiUrl"` + // Hostname of the authenticated GitHub service. + GitHubHost string `json:"githubHost"` + // Mission Control API origin for environment registration and management. + MissionControlURL string `json:"missionControlUrl"` + // Current bearer token for the authenticated GitHub identity. + Token string `json:"token"` +} + +// GitHub Mission Control registration options. The compute ID is application-owned and +// stable. +// Experimental: HostGitHubEnvironmentOptions is part of an experimental API and may change +// or be removed. +type HostGitHubEnvironmentOptions struct { + // Stable application installation identity, reused across host restarts. + ComputeID string `json:"computeId"` + // Human-readable environment display name. + Name string `json:"name"` +} + +// Normalized local WebSocket listener settings. +// Experimental: HostLocalServerConfiguration is part of an experimental API and may change +// or be removed. +type HostLocalServerConfiguration struct { + // Hostname or IP address to bind. + Hostname string `json:"hostname"` + // Port to bind, with zero requesting OS allocation. + Port int32 `json:"port"` + // Whether the listener requires token authentication. + RequireConnectionToken bool `json:"requireConnectionToken"` + // Secret connection token, absent when authentication is disabled. + Token *string `json:"token,omitempty"` +} + +// Local WebSocket transport options. +// Experimental: HostLocalServerOptions is part of an experimental API and may change or be +// removed. +type HostLocalServerOptions struct { + // Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. + Hostname *string `json:"hostname,omitempty"` + // Listener port. Omitted or zero requests an OS-allocated port. + Port *int32 `json:"port,omitempty"` + // Require token authentication (default true). Cannot be false with a token. + RequireConnectionToken *bool `json:"requireConnectionToken,omitempty"` + // Nonempty connection token. Generated randomly when required and omitted. + Token *string `json:"token,omitempty"` +} + +// The resident session the application has materialized on its own connection. +// Experimental: HostMaterializeSessionResult is part of an experimental API and may change +// or be removed. +type HostMaterializeSessionResult struct { + // Runtime session UUID materialized on the application's SDK connection. + SessionID string `json:"sessionId"` +} + +// Publishes a resident session attached to the listener's owning connection. +// Experimental: HostPublishSessionRequest is part of an experimental API and may change or +// be removed. +type HostPublishSessionRequest struct { + // Listener UUID returned by host.start. + HostID string `json:"hostId"` + // Canonical runtime session ID attached to the listener's owning connection. + SessionID string `json:"sessionId"` +} + +// The existing runtime identity and its resource on the listener. +// Experimental: HostPublishSessionResult is part of an experimental API and may change or +// be removed. +type HostPublishSessionResult struct { + // Canonical runtime ID of the published session. + SessionID string `json:"sessionId"` + // AHP resource URI for the session on this listener. + SessionURI string `json:"sessionUri"` +} + +// Readiness reported by the supervised hosting participant on its own SDK connection. +// Experimental: HostReadyRequest is part of an experimental API and may change or be +// removed. +type HostReadyRequest struct { + // Actual bound WebSocket URL. + Address *string `json:"address,omitempty"` + // Registered environment ID, reported only once the relay transport is connected. + EnvironmentID *string `json:"environmentId,omitempty"` + // Configured secret token, absent when authentication is disabled. + Token *string `json:"token,omitempty"` +} + +// Empty acknowledgement for a completed host lifecycle operation. +// Experimental: HostReadyResult is part of an experimental API and may change or be removed. +type HostReadyResult struct { +} + +// Listener-scoped registration, not a copy or durable adoption of a session. +// Experimental: HostRegisterSessionRequest is part of an experimental API and may change or +// be removed. +type HostRegisterSessionRequest struct { + // Additional directories already granted to the resident session. + AdditionalDirectories []string `json:"additionalDirectories,omitzero"` + // Session creation time in milliseconds since the Unix epoch, when available. + CreatedAtUnixMs *int64 `json:"createdAtUnixMs,omitempty"` + // Last session modification time in milliseconds since the Unix epoch, when available. + ModifiedAtUnixMs *int64 `json:"modifiedAtUnixMs,omitempty"` + // Canonical ID of the existing resident runtime session. + SessionID string `json:"sessionId"` + // Current display title of the resident session, when available. + Title *string `json:"title,omitempty"` + // Absolute working directory of the resident session. + WorkingDirectory string `json:"workingDirectory"` +} + +// The existing runtime identity and its resource on the listener. +type HostRegisterSessionResult struct { + // Canonical runtime ID of the published session. + SessionID string `json:"sessionId"` + // AHP resource URI for the session on this listener. + SessionURI string `json:"sessionUri"` +} + +// Empty acknowledgement for a completed host lifecycle operation. +// Experimental: HostReleaseSessionResult is part of an experimental API and may change or +// be removed. +type HostReleaseSessionResult struct { +} + +// Application callback routed over its existing SDK connection. +// Experimental: HostSessionCreateCallback is part of an experimental API and may change or +// be removed. +type HostSessionCreateCallback struct { + // Host-selected SDK creation or resume settings, without executable callbacks or tools. + Config map[string]any `json:"config"` + // Unique identity of the session participation being requested. + HandoffID string `json:"handoffId"` + // Listener UUID identifying the owning application's host. + HostID string `json:"hostId"` + // Resume an app-owned durable session instead of creating a new session. + Resume *bool `json:"resume,omitempty"` +} + +// One application-owned session handoff, requested by the supervised hosting participant. +// Experimental: HostSessionCreateRequest is part of an experimental API and may change or +// be removed. +type HostSessionCreateRequest struct { + // Host-selected SDK creation or resume settings, without executable callbacks or tools. + Config map[string]any `json:"config"` + // Unique identity for this participation, independent of the session lifetime. + HandoffID string `json:"handoffId"` + // Resume an app-owned durable session instead of creating a new session. + Resume *bool `json:"resume,omitempty"` +} + +// The resident session the application has materialized on its own connection. +// Experimental: HostSessionCreateResult is part of an experimental API and may change or be +// removed. +type HostSessionCreateResult struct { + // Runtime session UUID materialized on the application's SDK connection. + SessionID string `json:"sessionId"` +} + +// Releases the original application session object retained for one handoff. +// Experimental: HostSessionReleasedNotification is part of an experimental API and may +// change or be removed. +type HostSessionReleasedNotification struct { + // Identity of the handoff retaining the original application session object. + HandoffID string `json:"handoffId"` + // Listener UUID whose application session participation ended. + HostID string `json:"hostId"` +} + +// Experimental: HostSessionReleasedResult is part of an experimental API and may change or +// be removed. +type HostSessionReleasedResult struct { +} + +// Ends one participation, not the application's session lifetime. +// Experimental: HostSessionReleaseRequest is part of an experimental API and may change or +// be removed. +type HostSessionReleaseRequest struct { + // Identity of the participation to release without destroying the session. + HandoffID string `json:"handoffId"` +} + +// Empty acknowledgement for a completed host lifecycle operation. +// Experimental: HostShutdownResult is part of an experimental API and may change or be +// removed. +type HostShutdownResult struct { +} + +// Starts a supervised AHP host with at least one explicitly selected transport. +// Experimental: HostStartRequest is part of an experimental API and may change or be +// removed. +type HostStartRequest struct { + // Registers a GitHub Mission Control environment and enables its relay transport. + GitHubEnvironment *HostGitHubEnvironmentOptions `json:"githubEnvironment,omitempty"` + // Caller-generated UUID identifying this connection-owned listener. + HostID string `json:"hostId"` + // Enables a local WebSocket listener. + LocalServer *HostLocalServerOptions `json:"localServer,omitempty"` + // Ask the owning application to resume its durable AHP sessions. + ResumeFactory *bool `json:"resumeFactory,omitempty"` + // Ask the owning SDK application to materialize AHP sessions. + SessionFactory *bool `json:"sessionFactory,omitempty"` +} + +// Listener readiness, returned only after binding and the supervised participant's SDK +// handshake. +// Experimental: HostStartResult is part of an experimental API and may change or be removed. +type HostStartResult struct { + // GitHub Mission Control environment ID, present when its relay transport is ready. + EnvironmentID *string `json:"environmentId,omitempty"` + // Caller-generated listener UUID. + HostID string `json:"hostId"` + // Separate host process ID, when provided by a legacy runtime. Absent for in-process + // listeners. + Pid *int64 `json:"pid,omitempty"` + // Secret connection token, absent when authentication is disabled. + Token *string `json:"token,omitempty"` + // Actual bound WebSocket URL, including the allocated port. + URL *string `json:"url,omitempty"` +} + +// Catalogue identity retained from a bound candidate or plan at installation time. +// Experimental: InstallationCatalogueIdentity is part of an experimental API and may change +// or be removed. +type InstallationCatalogueIdentity struct { + // Catalogue description retained at install planning time. + Description *string `json:"description,omitempty"` + // Human display name retained from the catalogue candidate. + DisplayName string `json:"displayName"` + // Catalogue item URL when supplied by the authority. + ItemURL *string `json:"itemUrl,omitempty"` + // Catalogue publisher retained at install planning time. + Publisher *string `json:"publisher,omitempty"` + // Authority resource identifier when supplied by the catalogue. + ResourceID *string `json:"resourceId,omitempty"` + // Catalogue authority/source string that supplied the candidate. + Source string `json:"source"` + // Catalogue trust observation retained at install planning time. + TrustAtInstall CatalogTrustSnapshot `json:"trustAtInstall,omitempty"` + // Catalogue version retained at install planning time. + Version *string `json:"version,omitempty"` +} + +// One connection-owned, expiring request for a trusted host's explicit user decision. +// Experimental: InstallationConfirmationRequest is part of an experimental API and may +// change or be removed. +type InstallationConfirmationRequest struct { + // Opaque one-use challenge. Return unchanged; never log or persist. + ConfirmationID string `json:"confirmationId"` + // Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. + ExpiresAt string `json:"expiresAt"` + // Random identifier of this installation operation, not a plan handle. + OperationID string `json:"operationId"` + // 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. + PolicySessionID *string `json:"policySessionId,omitempty"` + // Resource-specific review to present before collecting the user's decision. + Review InstallationReview `json:"review"` + // Opaque commitment to the exact review and inputs. Return unchanged; never log. + ReviewFingerprint string `json:"reviewFingerprint"` +} + +// A response is meaningful only on the connection and request that issued its challenge. +// Experimental: InstallationConfirmationResponse is part of an experimental API and may +// change or be removed. +type InstallationConfirmationResponse struct { + // Exact challenge from the request. + ConfirmationID string `json:"confirmationId"` + // Fresh explicit user decision. There is no default. + Decision InstallationDecision `json:"decision"` + // Exact review commitment from the request. + ReviewFingerprint string `json:"reviewFingerprint"` +} + +// Only resource kinds with an implemented installation engine have a review variant. +// Experimental: InstallationReview is part of an experimental API and may change or be +// removed. +type InstallationReview interface { + installationReview() + Resource() InstallationReviewResource +} + +type RawInstallationReviewData struct { + Discriminator InstallationReviewResource + Raw json.RawMessage +} + +func (RawInstallationReviewData) installationReview() {} +func (r RawInstallationReviewData) Resource() InstallationReviewResource { + return r.Discriminator +} + +type InstallationReviewMCP struct { + // The exact MCP action and its reviewed changes. + Review MCPInstallationReview `json:"review"` +} + +func (InstallationReviewMCP) installationReview() {} +func (InstallationReviewMCP) Resource() InstallationReviewResource { + return InstallationReviewResourceMCP +} + +type InstallationReviewSkill struct { + // The exact verified Skill action and its reviewed files. + Review SkillInstallationReview `json:"review"` +} + +func (InstallationReviewSkill) installationReview() {} +func (InstallationReviewSkill) Resource() InstallationReviewResource { + return InstallationReviewResourceSkill +} + +// A response is meaningful only on the connection and request that issued its challenge. +// Experimental: InstallationsConfirmResult is part of an experimental API and may change or +// be removed. +type InstallationsConfirmResult struct { + // Exact challenge from the request. + ConfirmationID string `json:"confirmationId"` + // Fresh explicit user decision. There is no default. + Decision InstallationDecision `json:"decision"` + // Exact review commitment from the request. + ReviewFingerprint string `json:"reviewFingerprint"` +} + // Installed plugin record from global state, with marketplace, version, install time, // enabled state, cache path, and source. // Experimental: InstalledPlugin is part of an experimental API and may change or be removed. @@ -5777,6 +6619,22 @@ type LocalSessionMetadataValue struct { Summary *string `json:"summary,omitempty"` } +// An account the host has signed in to, identified by the server it lives on and the login +// it uses there. The same person can appear more than once when they use both github.com +// and an Enterprise server. +// Experimental: LoggedInUser is part of an experimental API and may change or be removed. +// Internal: LoggedInUser is an internal SDK API and is not part of the public surface. +type LoggedInUser struct { + // Source account this account was derived from, when one was recorded. + DerivedFrom *string `json:"derivedFrom,omitempty"` + // Host the account belongs to, such as `github.com` or an Enterprise server. + Host string `json:"host"` + // Account kind, when the host recorded one. Consumers must tolerate new strings. + Kind *string `json:"kind,omitempty"` + // Account login on that host. + Login string `json:"login"` +} + // Message text, optional severity level, persistence flag, optional follow-up URL, and // optional tip. // Experimental: LogRequest is part of an experimental API and may change or be removed. @@ -5836,11 +6694,106 @@ type ManagedMCPServerConfig struct { URL string `json:"url"` } +// Lock state and provenance of one managed setting. +// Experimental: ManagedSettingMeta is part of an experimental API and may change or be +// removed. +type ManagedSettingMeta struct { + // Whether users and repositories may choose a different value. `false` means policy locks + // the value. + Overridable bool `json:"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. + Source string `json:"source"` +} + // Experimental: ManagedSettingsClearCacheResult is part of an experimental API and may // change or be removed. type ManagedSettingsClearCacheResult struct { } +// One candidate channel; absent settings represents a channel that delivered no document. +// Experimental: ManagedSettingsComposeLayer is part of an experimental API and may change +// or be removed. +type ManagedSettingsComposeLayer struct { + // Candidate managed-settings document. Omit when the channel delivered none, as in resolve + // output. + Settings any `json:"settings,omitempty"` + // The channel whose candidate document is being supplied. + Source ManagedSettingsChannel `json:"source"` +} + +// Candidate managed-settings documents to merge without applying them. +// Experimental: ManagedSettingsComposeRequest is part of an experimental API and may change +// or be removed. +type ManagedSettingsComposeRequest struct { + // 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 `json:"layers"` +} + +// The effective managed settings the runtime would enforce for the given documents, in the +// same shape `managedSettings.resolve` returns. +// Experimental: ManagedSettingsComposeResult is part of an experimental API and may change +// or be removed. +type ManagedSettingsComposeResult struct { + // Warnings about ignored content, with paths prefixed by the channel name. + Diagnostics []ManagedSettingsDiagnostic `json:"diagnostics"` + // Only the supplied channels, strongest first, with canonical documents. Empty canonical + // documents are represented as absent settings, as in live resolution. + Layers []ManagedSettingsLayer `json:"layers"` + // Per-key lock state and provenance for `values`. + Meta *ManagedSettingsMeta `json:"meta,omitempty"` + // Effective managed settings, in the same shape as `session.managedSettings.get`. + Resolved ManagedSettingsResolvedData `json:"resolved"` + // Typed effective values, as in `managedSettings.resolve`. + Values *ManagedSettingsValues `json:"values,omitempty"` +} + +// One validation finding for a managed-settings document. +// Experimental: ManagedSettingsDiagnostic is part of an experimental API and may change or +// be removed. +type ManagedSettingsDiagnostic struct { + // Human-readable description of the finding. + Message string `json:"message"` + // Dot-separated path of the offending setting, such as `autoTier.overridable`. Empty for + // the document as a whole. + Path string `json:"path"` + // Whether the finding rejects the document. + Severity ManagedSettingsDiagnosticSeverity `json:"severity"` +} + +// One managed-settings channel and the document it delivered. +// Experimental: ManagedSettingsLayer is part of an experimental API and may change or be +// removed. +type ManagedSettingsLayer struct { + // Validated managed-settings document this channel delivered. Absent when the channel + // delivered none. + Settings any `json:"settings,omitempty"` + // 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 `json:"source"` +} + +// 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: ManagedSettingsMeta is part of an experimental API and may change or be +// removed. +type ManagedSettingsMeta struct { + // Lock state and provenance of `values.autoTier`. + AutoTier *ManagedSettingMeta `json:"autoTier,omitempty"` + // Lock state and provenance of `values.model`. + Model *ManagedSettingMeta `json:"model,omitempty"` +} + // Validated device-managed settings discovered before a session exists. // Experimental: ManagedSettingsReadResult is part of an experimental API and may change or // be removed. @@ -5852,16 +6805,12 @@ type ManagedSettingsReadResult struct { SettingsJSON any `json:"settingsJson,omitempty"` } -// 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: ManagedSettingsResolvedData is part of an experimental API and may change // or be removed. type ManagedSettingsResolvedData struct { @@ -5901,6 +6850,100 @@ type ManagedSettingsResolvedData struct { Source ManagedSettingsResolvedSource `json:"source"` } +// Experimental: ManagedSettingsResolveRequest is part of an experimental API and may change +// or be removed. +type ManagedSettingsResolveRequest struct { + // Embedding client identity for server policy requests, as in session creation. Omit for + // the CLI identity. + ClientName *string `json:"clientName,omitempty"` + // GitHub token to resolve instead of the current account. The call fails when the token + // cannot be resolved. + GitHubToken *string `json:"gitHubToken,omitempty"` + // 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 `json:"selectionId,omitempty"` +} + +// Effective enterprise managed settings for an account, resolved without a session. +// Experimental: ManagedSettingsResolveResult is part of an experimental API and may change +// or be removed. +type ManagedSettingsResolveResult struct { + // 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 `json:"account,omitempty"` + // 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 `json:"diagnostics"` + // 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 `json:"layers"` + // Per-key lock state and provenance for the entries in `values`, using the same key names. + Meta *ManagedSettingsMeta `json:"meta,omitempty"` + // Effective managed settings from the device and account (server) channels, in the same + // shape as `session.managedSettings.get`, excluding session-local injection. + Resolved ManagedSettingsResolvedData `json:"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`. + Values *ManagedSettingsValues `json:"values,omitempty"` +} + +// The authoring JSON schema for managed settings recognized by this runtime. +// Experimental: ManagedSettingsSchemaResult is part of an experimental API and may change +// or be removed. +type ManagedSettingsSchemaResult struct { + // Version of the runtime that owns this schema. + RuntimeVersion string `json:"runtimeVersion"` + // 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 any `json:"schema"` +} + +// A candidate managed-settings document to validate without applying it. +// Experimental: ManagedSettingsValidateRequest is part of an experimental API and may +// change or be removed. +type ManagedSettingsValidateRequest struct { + // 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 any `json:"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. + Layer *string `json:"layer,omitempty"` +} + +// Result of validating a managed-settings document. +// Experimental: ManagedSettingsValidateResult is part of an experimental API and may change +// or be removed. +type ManagedSettingsValidateResult struct { + // Errors that reject the document and warnings about content the runtime ignores. + Diagnostics []ManagedSettingsDiagnostic `json:"diagnostics"` + // Canonical form of the document the runtime would apply, with unrecognized keys removed. + // Absent when the document is invalid. + Settings any `json:"settings,omitempty"` + // 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 bool `json:"valid"` +} + +// 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: ManagedSettingsValues is part of an experimental API and may change or be +// removed. +type ManagedSettingsValues struct { + // Managed Auto routing preference, used when the selected model is `auto`. + AutoTier *AutoTier `json:"autoTier,omitempty"` + // 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 `json:"model,omitempty"` +} + // Result of registering a new marketplace. // Experimental: MarketplaceAddResult is part of an experimental API and may change or be // removed. @@ -5996,6 +7039,30 @@ type MCPAllowedServer struct { RedactedNote *string `json:"redactedNote,omitempty"` } +// Applies exactly one previously prepared operation on its original connection. +// Experimental: MCPApplyInstallRequest is part of an experimental API and may change or be +// removed. +type MCPApplyInstallRequest struct { + // Capabilities required by the original prepared operation. + Contract CatalogClientContract `json:"contract"` + // Runtime-issued ID already returned by prepareInstall, never reused or rebound. + OperationID string `json:"operationId"` + // Same existing attached or privately borrowed session as preparation. + PolicySessionID string `json:"policySessionId"` +} + +// One-use application of the exact retained removal plan. +// Experimental: MCPApplyUninstallRequest is part of an experimental API and may change or +// be removed. +type MCPApplyUninstallRequest struct { + // Required authenticated bound installation capabilities. + Contract CatalogClientContract `json:"contract"` + // Opaque original removal plan, consumed once. + PlanHandle string `json:"planHandle"` + // Same existing selected session as removal preparation. + PolicySessionID string `json:"policySessionId"` +} + // MCP server, tool name, and arguments to invoke from an MCP App view. // Experimental: MCPAppsCallToolRequest is part of an experimental API and may change or be // removed. @@ -6272,6 +7339,48 @@ type MCPConfigUpdateRequest struct { type MCPConfigUpdateResult struct { } +// Effective MCP configuration entry. Configuration enablement is distinct from the optional +// live observation. +// Experimental: MCPConfiguredServer is part of an experimental API and may change or be +// removed. +type MCPConfiguredServer struct { + // Human-readable display name supplied by configuration. + DisplayName *string `json:"displayName,omitempty"` + // Whether this configured server is enabled after session configuration and policy + // filtering. + Enabled bool `json:"enabled"` + // Observed state from an already materialized matching server. Omitted when no live graph + // has this configured server; it never determines configuration enablement. + Live *MCPConfiguredServerState `json:"live,omitempty"` + // Server name (config key) + Name string `json:"name"` + // Configuration provenance: user, workspace, plugin, builtin, or managed. + Source *MCPServerSource `json:"source,omitempty"` + // Plugin name that provided this server, when source is plugin. + SourcePlugin *string `json:"sourcePlugin,omitempty"` + // Plugin version that provided this server, when source is plugin. + SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` +} + +// Effective MCP configuration with optional live observations from matching already +// materialized servers. +// Experimental: MCPConfiguredServerList is part of an experimental API and may change or be +// removed. +type MCPConfiguredServerList struct { + // Effective configured MCP servers. + Servers []MCPConfiguredServer `json:"servers"` +} + +// Observational state for a matching already materialized MCP server. +// Experimental: MCPConfiguredServerState is part of an experimental API and may change or +// be removed. +type MCPConfiguredServerState struct { + // Observed connection error, when the materialized server failed. + Error *string `json:"error,omitempty"` + // Observed connection status. This is not a configuration or readiness guarantee. + Status MCPServerStatus `json:"status"` +} + // Credential-free authentication identity used to configure GitHub MCP. // Experimental: MCPConfigureGitHubRequest is part of an experimental API and may change or // be removed. @@ -6319,6 +7428,8 @@ type MCPDiagnosticSourceConfiguration struct { // Experimental: MCPDisableRequest is part of an experimental API and may change or be // removed. type MCPDisableRequest struct { + // Required for an owned installation; omission preserves only manual-server behaviour. + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` // Name of the MCP server to disable ServerName string `json:"serverName"` } @@ -6346,6 +7457,8 @@ type MCPDiscoverResult struct { // Experimental: MCPEnableRequest is part of an experimental API and may change or be // removed. type MCPEnableRequest struct { + // Exact receipt identity for explicit owned activation in this session. + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` // Name of the MCP server to enable ServerName string `json:"serverName"` } @@ -6502,6 +7615,526 @@ type MCPHostState struct { PendingConnections []string `json:"pendingConnections"` } +// One Registry string-valued configuration entry for the selected transport. +// Experimental: MCPInstallationInput is part of an experimental API and may change or be +// removed. +type MCPInstallationInput struct { + // Exact category declared by the selected choice. + Category MCPPlanValueCategory `json:"category"` + // Exact key declared by the selected choice. + Key string `json:"key"` + // Explicit non-secret value. Secret placeholders use a separate input channel. + Value string `json:"value"` +} + +// Read-only or recovery management result, never permission to activate or replay. +// Experimental: MCPInstallationManagementOutcome is part of an experimental API and may +// change or be removed. +type MCPInstallationManagementOutcome interface { + mcpInstallationManagementOutcome() + Kind() MCPInstallationManagementOutcomeKind +} + +type RawMCPInstallationManagementOutcomeData struct { + Discriminator MCPInstallationManagementOutcomeKind + Raw json.RawMessage +} + +func (RawMCPInstallationManagementOutcomeData) mcpInstallationManagementOutcome() {} +func (r RawMCPInstallationManagementOutcomeData) Kind() MCPInstallationManagementOutcomeKind { + return r.Discriminator +} + +type MCPInstallationManagementOutcomeInstallPrepared struct { + // Known original operation identity, returned before callback or effects. + Operation MCPPreparedInstall `json:"operation"` +} + +func (MCPInstallationManagementOutcomeInstallPrepared) mcpInstallationManagementOutcome() {} +func (MCPInstallationManagementOutcomeInstallPrepared) Kind() MCPInstallationManagementOutcomeKind { + return MCPInstallationManagementOutcomeKindInstallPrepared +} + +type MCPInstallationManagementOutcomeListed struct { + // Owned receipts visible to the selected account and host. + Installations []MCPInstallationSummary `json:"installations"` +} + +func (MCPInstallationManagementOutcomeListed) mcpInstallationManagementOutcome() {} +func (MCPInstallationManagementOutcomeListed) Kind() MCPInstallationManagementOutcomeKind { + return MCPInstallationManagementOutcomeKindListed +} + +type MCPInstallationManagementOutcomeOperation struct { + // Original-connection operation snapshot. + Operation MCPInstallationOperationStatus `json:"operation"` +} + +func (MCPInstallationManagementOutcomeOperation) mcpInstallationManagementOutcome() {} +func (MCPInstallationManagementOutcomeOperation) Kind() MCPInstallationManagementOutcomeKind { + return MCPInstallationManagementOutcomeKindOperation +} + +type MCPInstallationManagementOutcomeRecovered struct { + // Freshly inspected receipts after successful durable reconciliation. + Installations []MCPInstallationSummary `json:"installations"` +} + +func (MCPInstallationManagementOutcomeRecovered) mcpInstallationManagementOutcome() {} +func (MCPInstallationManagementOutcomeRecovered) Kind() MCPInstallationManagementOutcomeKind { + return MCPInstallationManagementOutcomeKindRecovered +} + +// Already-confirmed durable work must be reconciled before new mutations or inventory. +type MCPInstallationManagementOutcomeRecoveryRequired struct { +} + +func (MCPInstallationManagementOutcomeRecoveryRequired) mcpInstallationManagementOutcome() {} +func (MCPInstallationManagementOutcomeRecoveryRequired) Kind() MCPInstallationManagementOutcomeKind { + return MCPInstallationManagementOutcomeKindRecoveryRequired +} + +type MCPInstallationManagementOutcomeRefused struct { + // Specific bounded refusal. + Reason MCPInstallationFailureReason `json:"reason"` +} + +func (MCPInstallationManagementOutcomeRefused) mcpInstallationManagementOutcome() {} +func (MCPInstallationManagementOutcomeRefused) Kind() MCPInstallationManagementOutcomeKind { + return MCPInstallationManagementOutcomeKindRefused +} + +type MCPInstallationManagementOutcomeUninstallPlanned struct { + // Original owned removal plan and operation. + Plan MCPUninstallPlan `json:"plan"` +} + +func (MCPInstallationManagementOutcomeUninstallPlanned) mcpInstallationManagementOutcome() {} +func (MCPInstallationManagementOutcomeUninstallPlanned) Kind() MCPInstallationManagementOutcomeKind { + return MCPInstallationManagementOutcomeKindUninstallPlanned +} + +// Management result with contract receipt, or a typed request/negotiation refusal. +// Experimental: MCPInstallationManagementResult is part of an experimental API and may +// change or be removed. +type MCPInstallationManagementResult interface { + mcpInstallationManagementResult() + mcpInstallationManagementResultKind() MCPInstallationManagementResultKind +} + +type RawMCPInstallationManagementResultData struct { + Discriminator MCPInstallationManagementResultKind + Raw json.RawMessage +} + +func (RawMCPInstallationManagementResultData) mcpInstallationManagementResult() {} +func (r RawMCPInstallationManagementResultData) mcpInstallationManagementResultKind() MCPInstallationManagementResultKind { + return r.Discriminator +} +func (CatalogInvalidRequestError) mcpInstallationManagementResult() {} +func (CatalogInvalidRequestError) mcpInstallationManagementResultKind() MCPInstallationManagementResultKind { + return MCPInstallationManagementResultKindInvalidRequest +} +func (CatalogNegotiationRefusedError) mcpInstallationManagementResult() {} +func (CatalogNegotiationRefusedError) mcpInstallationManagementResultKind() MCPInstallationManagementResultKind { + return MCPInstallationManagementResultKindNegotiationRefused +} + +type MCPInstallationManagementResultOutcome struct { + // Capabilities actually honoured for this request. + Negotiated CatalogNegotiatedContract `json:"negotiated"` + // Observed management outcome. + Outcome MCPInstallationManagementOutcome `json:"outcome"` +} + +func (MCPInstallationManagementResultOutcome) mcpInstallationManagementResult() {} +func (MCPInstallationManagementResultOutcome) mcpInstallationManagementResultKind() MCPInstallationManagementResultKind { + return MCPInstallationManagementResultKindOutcome +} + +// Existing-operation control. A new session selector is deliberately not accepted. +// Experimental: MCPInstallationOperationRequest is part of an experimental API and may +// change or be removed. +type MCPInstallationOperationRequest struct { + // Original installation wire capability; new-work authentication is not reacquired. + Contract CatalogClientContract `json:"contract"` + // Exact runtime-issued operation ID on the original connection. + OperationID string `json:"operationId"` +} + +// Status snapshot from the original connection, independent of new-work account +// availability. +// Experimental: MCPInstallationOperationStatus is part of an experimental API and may +// change or be removed. +type MCPInstallationOperationStatus interface { + mcpInstallationOperationStatus() + Phase() MCPInstallationOperationStatusPhase +} + +type RawMCPInstallationOperationStatusData struct { + Discriminator MCPInstallationOperationStatusPhase + Raw json.RawMessage +} + +func (RawMCPInstallationOperationStatusData) mcpInstallationOperationStatus() {} +func (r RawMCPInstallationOperationStatusData) Phase() MCPInstallationOperationStatusPhase { + return r.Discriminator +} + +type MCPInstallationOperationStatusApplying struct { + // Whether applying was asked to cancel; already-started effects retain their lease. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOperationStatusApplying) mcpInstallationOperationStatus() {} +func (MCPInstallationOperationStatusApplying) Phase() MCPInstallationOperationStatusPhase { + return MCPInstallationOperationStatusPhaseApplying +} + +type MCPInstallationOperationStatusAwaitingConfirmation struct { + // Whether the pending human callback was asked to cancel. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOperationStatusAwaitingConfirmation) mcpInstallationOperationStatus() {} +func (MCPInstallationOperationStatusAwaitingConfirmation) Phase() MCPInstallationOperationStatusPhase { + return MCPInstallationOperationStatusPhaseAwaitingConfirmation +} + +type MCPInstallationOperationStatusCompleted struct { + // Whether cancellation was requested before the terminal result. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` + // Immutable terminal receipt. + Outcome MCPInstallationOutcome `json:"outcome"` +} + +func (MCPInstallationOperationStatusCompleted) mcpInstallationOperationStatus() {} +func (MCPInstallationOperationStatusCompleted) Phase() MCPInstallationOperationStatusPhase { + return MCPInstallationOperationStatusPhaseCompleted +} + +type MCPInstallationOperationStatusPrepared struct { + // Whether the inert prepared operation was asked to cancel. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOperationStatusPrepared) mcpInstallationOperationStatus() {} +func (MCPInstallationOperationStatusPrepared) Phase() MCPInstallationOperationStatusPhase { + return MCPInstallationOperationStatusPhasePrepared +} + +type MCPInstallationOperationStatusPreparing struct { + // Whether cancellation has been requested, not proof that a write was undone. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOperationStatusPreparing) mcpInstallationOperationStatus() {} +func (MCPInstallationOperationStatusPreparing) Phase() MCPInstallationOperationStatusPhase { + return MCPInstallationOperationStatusPhasePreparing +} + +type MCPInstallationOperationStatusRevalidating struct { + // Whether source or authority revalidation was asked to cancel. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOperationStatusRevalidating) mcpInstallationOperationStatus() {} +func (MCPInstallationOperationStatusRevalidating) Phase() MCPInstallationOperationStatusPhase { + return MCPInstallationOperationStatusPhaseRevalidating +} + +// Terminal mutation result. Uncertainty is not approval, rollback or permission to replay. +// Experimental: MCPInstallationOutcome is part of an experimental API and may change or be +// removed. +type MCPInstallationOutcome interface { + mcpInstallationOutcome() + Kind() MCPInstallationOutcomeKind +} + +type RawMCPInstallationOutcomeData struct { + Discriminator MCPInstallationOutcomeKind + Raw json.RawMessage +} + +func (RawMCPInstallationOutcomeData) mcpInstallationOutcome() {} +func (r RawMCPInstallationOutcomeData) Kind() MCPInstallationOutcomeKind { + return r.Discriminator +} + +type MCPInstallationOutcomeCancelled struct { + // Original operation cancelled before a terminal application result. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOutcomeCancelled) mcpInstallationOutcome() {} +func (MCPInstallationOutcomeCancelled) Kind() MCPInstallationOutcomeKind { + return MCPInstallationOutcomeKindCancelled +} + +type MCPInstallationOutcomeDeclined struct { + // Original operation explicitly declined by the user. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOutcomeDeclined) mcpInstallationOutcome() {} +func (MCPInstallationOutcomeDeclined) Kind() MCPInstallationOutcomeKind { + return MCPInstallationOutcomeKindDeclined +} + +type MCPInstallationOutcomeInstalled struct { + // Durable installation succeeded but final transaction cleanup remains. + CleanupPending bool `json:"cleanupPending"` + // Receipt identity produced by the confirmed transaction. + Installation MCPInstallationSummary `json:"installation"` +} + +func (MCPInstallationOutcomeInstalled) mcpInstallationOutcome() {} +func (MCPInstallationOutcomeInstalled) Kind() MCPInstallationOutcomeKind { + return MCPInstallationOutcomeKindInstalled +} + +// A write may have completed. Recover and inspect durable state before retrying. +type MCPInstallationOutcomeRecoveryRequired struct { + // Operation whose durable result must be recovered and inspected. + OperationID string `json:"operationId"` +} + +func (MCPInstallationOutcomeRecoveryRequired) mcpInstallationOutcome() {} +func (MCPInstallationOutcomeRecoveryRequired) Kind() MCPInstallationOutcomeKind { + return MCPInstallationOutcomeKindRecoveryRequired +} + +type MCPInstallationOutcomeRefused struct { + // Present once an operation has been allocated; never a plan handle. + OperationID *string `json:"operationId,omitempty"` + // Specific bounded refusal, never a success-shaped fallback. + Reason MCPInstallationFailureReason `json:"reason"` +} + +func (MCPInstallationOutcomeRefused) mcpInstallationOutcome() {} +func (MCPInstallationOutcomeRefused) Kind() MCPInstallationOutcomeKind { + return MCPInstallationOutcomeKindRefused +} + +// The durable transaction was aborted or fully compensated. +type MCPInstallationOutcomeRolledBack struct { + // Original connection-owned operation. + OperationID string `json:"operationId"` + // Cause of the fully aborted or compensated operation. + Reason MCPInstallationFailureReason `json:"reason"` +} + +func (MCPInstallationOutcomeRolledBack) mcpInstallationOutcome() {} +func (MCPInstallationOutcomeRolledBack) Kind() MCPInstallationOutcomeKind { + return MCPInstallationOutcomeKindRolledBack +} + +type MCPInstallationOutcomeUninstalled struct { + // Durable removal succeeded but final cleanup remains. + CleanupPending bool `json:"cleanupPending"` + // Exact removed receipt identity. + InstallationID string `json:"installationId"` + // Original removal operation. + OperationID string `json:"operationId"` + // Any grants in the incumbent shared OAuth store remain unowned and retained. + PreservedSharedAuthentication bool `json:"preservedSharedAuthentication"` + // Exact owned input slots removed, excluding shared OAuth credentials. + RemovedOwnedSecrets int64 `json:"removedOwnedSecrets"` + // Whether protected pre-install configuration was restored. + RestoredPreviousConfiguration bool `json:"restoredPreviousConfiguration"` +} + +func (MCPInstallationOutcomeUninstalled) mcpInstallationOutcome() {} +func (MCPInstallationOutcomeUninstalled) Kind() MCPInstallationOutcomeKind { + return MCPInstallationOutcomeKindUninstalled +} + +// Final remote configuration, not a template. The producer refuses external-value +// expansion before presenting this review. Receipt-owned secrets appear only as +// `${installation-secret:}` references whose `` matches a reviewed +// `${secret:}` placeholder; values are never included. +// Experimental: MCPInstallationRemoteConfiguration is part of an experimental API and may +// change or be removed. +type MCPInstallationRemoteConfiguration struct { + // Configured headers, excluding separately authorised OAuth tokens. Values may + // contain owned secret references, never secret values. + Headers map[string]string `json:"headers"` + // Configured tool selection, not permission to invoke those tools. + Tools []string `json:"tools"` + // Transport in the effective persisted remote configuration. + Transport MCPPlanRemoteTransport `json:"transport"` + // Exact resolved endpoint, without templates or secret placeholders. + URL string `json:"url"` +} + +// An installation result together with the exact honoured contract, or a negotiation +// refusal. +// Experimental: MCPInstallationResult is part of an experimental API and may change or be +// removed. +type MCPInstallationResult interface { + mcpInstallationResult() + mcpInstallationResultKind() MCPInstallationResultKind +} + +type RawMCPInstallationResultData struct { + Discriminator MCPInstallationResultKind + Raw json.RawMessage +} + +func (RawMCPInstallationResultData) mcpInstallationResult() {} +func (r RawMCPInstallationResultData) mcpInstallationResultKind() MCPInstallationResultKind { + return r.Discriminator +} +func (CatalogInvalidRequestError) mcpInstallationResult() {} +func (CatalogInvalidRequestError) mcpInstallationResultKind() MCPInstallationResultKind { + return MCPInstallationResultKindInvalidRequest +} +func (CatalogNegotiationRefusedError) mcpInstallationResult() {} +func (CatalogNegotiationRefusedError) mcpInstallationResultKind() MCPInstallationResultKind { + return MCPInstallationResultKindNegotiationRefused +} + +type MCPInstallationResultOutcome struct { + // Capabilities actually honoured for this request. + Negotiated CatalogNegotiatedContract `json:"negotiated"` + // Terminal result of the original operation. + Outcome MCPInstallationOutcome `json:"outcome"` +} + +func (MCPInstallationResultOutcome) mcpInstallationResult() {} +func (MCPInstallationResultOutcome) mcpInstallationResultKind() MCPInstallationResultKind { + return MCPInstallationResultKindOutcome +} + +// Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. +// Experimental: MCPInstallationReview is part of an experimental API and may change or be +// removed. +type MCPInstallationReview interface { + mcpInstallationReview() + Action() MCPInstallationReviewAction +} + +type RawMCPInstallationReviewData struct { + Discriminator MCPInstallationReviewAction + Raw json.RawMessage +} + +func (RawMCPInstallationReviewData) mcpInstallationReview() {} +func (r RawMCPInstallationReviewData) Action() MCPInstallationReviewAction { + return r.Discriminator +} + +type MCPInstallationReviewInstall struct { + // Catalogue identity retained from the bound candidate when available. + Catalogue *InstallationCatalogueIdentity `json:"catalogue,omitempty"` + // Original catalogue trust metadata, not a verification claim. + CatalogueTrust CatalogTrustSnapshot `json:"catalogueTrust,omitempty"` + // The configuration change for the selected alternative only. + ConfigurationChange MCPPlanConfigurationChange `json:"configurationChange"` + // 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. + EffectiveConfiguration *MCPInstallationRemoteConfiguration `json:"effectiveConfiguration,omitempty"` + // Identity from the retained plan, not caller display text. + Identity MCPPlanResourceIdentity `json:"identity"` + // Non-secret values supplied for this selected alternative. + Inputs []MCPInstallationInput `json:"inputs"` + // Policy decision bound to this plan. + Policy MCPPlanPolicyResult `json:"policy"` + // Original source identity and content commitment. + Provenance MCPPlanProvenance `json:"provenance"` + // Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. + SecretStorage MCPInstallationSecretStorage `json:"secretStorage"` + // Only the selected alternative is applied. + SelectedChoice MCPPlanTransportChoice `json:"selectedChoice"` + // Exact reviewed placeholders supplied separately. Never secret values. + SuppliedSecrets []string `json:"suppliedSecrets"` + // Exact reviewed user-scope destination. + Target MCPPlanTarget `json:"target"` +} + +func (MCPInstallationReviewInstall) mcpInstallationReview() {} +func (MCPInstallationReviewInstall) Action() MCPInstallationReviewAction { + return MCPInstallationReviewActionInstall +} + +type MCPInstallationReviewUninstall struct { + // Identity from the installed receipt. + Identity MCPPlanResourceIdentity `json:"identity"` + // Receipt-owned installation being removed. + InstallationID string `json:"installationId"` + // Exact planner-owned secret slots to remove, excluding shared OAuth grants. + OwnedSecretCount int64 `json:"ownedSecretCount"` + // Current removal policy, independent of permission to activate the server. + Policy MCPPlanPolicyResult `json:"policy"` + // Shared profile authentication is deliberately retained, not pending cleanup. + PreservesSharedAuthentication bool `json:"preservesSharedAuthentication"` + // Source identity and content commitment retained by the installed receipt. + Provenance MCPPlanProvenance `json:"provenance"` + // Whether uninstall restores a protected pre-install configuration. + RestoresPreviousConfiguration bool `json:"restoresPreviousConfiguration"` + // Exact destination, checked for intervening changes before mutation. + Target MCPPlanTarget `json:"target"` +} + +func (MCPInstallationReviewUninstall) mcpInstallationReview() {} +func (MCPInstallationReviewUninstall) Action() MCPInstallationReviewAction { + return MCPInstallationReviewActionUninstall +} + +// A request-local value for one exact reviewed placeholder. Never logged or persisted in a +// plan. +// Experimental: MCPInstallationSecret is part of an experimental API and may change or be +// removed. +type MCPInstallationSecret struct { + // Exact placeholder from the selected choice, not a caller-chosen backend identifier. + Placeholder string `json:"placeholder"` + // Fresh explicit secret value. It is omitted from confirmation reviews and telemetry. + Value string `json:"value"` +} + +// New-work inventory or recovery request under an explicitly selected existing session. +// Experimental: MCPInstallationsRequest is part of an experimental API and may change or be +// removed. +type MCPInstallationsRequest struct { + // Required authenticated bound installation contract. + Contract CatalogClientContract `json:"contract"` + // Existing selected local session on this connection. + PolicySessionID string `json:"policySessionId"` +} + +// Durable configuration ownership is distinct from session-specific usability. +// Experimental: MCPInstallationSummary is part of an experimental API and may change or be +// removed. +type MCPInstallationSummary struct { + // Catalogue identity retained from the installed plan when available. + Catalogue *InstallationCatalogueIdentity `json:"catalogue,omitempty"` + // Exact alternative retained in the installing receipt. + ChoiceID string `json:"choiceId"` + // Identity retained from the validated original plan. + Identity MCPPlanResourceIdentity `json:"identity"` + // Exact durable installation receipt identity. + InstallationID string `json:"installationId"` + // ISO 8601 wall-clock installation time when available. + InstalledAt *string `json:"installedAt,omitempty"` + // Original installing operation, not a fresh management operation. + OperationID string `json:"operationId"` + // Ownership or setup state, never inferred proof of tool usability. + State MCPInstallationState `json:"state"` +} + // A normalised, inert description of what installing an MCP server would involve. Carries // no raw card, no install specification, and no secret value. // Experimental: MCPInstallPlan is part of an experimental API and may change or be removed. @@ -6587,6 +8220,37 @@ type MCPOauthAuthenticationStateChangedRequest struct { ServerName *string `json:"serverName,omitempty"` } +// Targets only the original prepared/applying owned login on this exact session requester. +// Experimental: MCPOauthCancelLoginRequest is part of an experimental API and may change or +// be removed. +type MCPOauthCancelLoginRequest struct { + // The same authoritative installation identity supplied during preparation. + ExpectedInstallationID string `json:"expectedInstallationId"` + // Runtime-issued login handle known before the effectful login request begins. + LoginID string `json:"loginId"` +} + +// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. +// Experimental: MCPOauthCancelLoginResult is part of an experimental API and may change or +// be removed. +type MCPOauthCancelLoginResult struct { + // True after cancellation settles, false when the original login already connected + // successfully. + Cancelled bool `json:"cancelled"` +} + +// Host-delivered callback for a runtime-managed MCP OAuth login. +// Experimental: MCPOauthCompleteRequest is part of an experimental API and may change or be +// removed. +type MCPOauthCompleteRequest struct { + // Opaque identifier returned by session.mcp.oauth.login for the pending external callback. + AuthorizationID string `json:"authorizationId"` + // Full externally visible HTTPS callback URL received by the host, including the + // authorization response query parameters. Applications behind a reverse proxy must + // reconstruct the public URL rather than passing an internal proxy URL. + CallbackURL string `json:"callbackUrl"` +} + // Pending MCP OAuth request ID and host-provided token or cancellation response. // Experimental: MCPOauthHandlePendingRequest is part of an experimental API and may change // or be removed. @@ -6607,7 +8271,7 @@ type MCPOauthHandlePendingResult struct { } // Remote MCP server name and optional overrides controlling reauthentication, OAuth client -// display name, callback success-page copy, and static OAuth client selection. +// display name, callback handling, and static OAuth client selection. // Experimental: MCPOauthLoginRequest is part of an experimental API and may change or be // removed. type MCPOauthLoginRequest struct { @@ -6628,6 +8292,8 @@ type MCPOauthLoginRequest struct { // ephemeral host-owned secret, uses it for this authentication attempt and does not persist // it. ClientSecret *string `json:"clientSecret,omitempty"` + // Exact owned receipt identity. Owned login never uses an implicit helper session. + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` // 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. @@ -6635,10 +8301,19 @@ type MCPOauthLoginRequest struct { // Optional OAuth grant type override for this login. Defaults to the server configuration, // or authorization_code when no grant type is specified. GrantType *MCPOauthLoginGrantType `json:"grantType,omitempty"` + // Required for owned login. Consumes the exact prepareLogin handle once. + // Set forceReauth and display options during preparation, not consumption. + LoginID *string `json:"loginId,omitempty"` // 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. PublicClient *bool `json:"publicClient,omitempty"` + // Optional externally visible HTTPS redirect URI for a host-managed callback endpoint. When + // supplied, the runtime still owns discovery, PKCE, token exchange, persistence, and + // reconnect, but does not bind a loopback listener or terminate HTTPS. The URI must not + // contain query parameters or a fragment and must be registered for the selected CIMD, DCR, + // or static OAuth client. + RedirectURI *string `json:"redirectUri,omitempty"` // Name of the remote MCP server to authenticate ServerName string `json:"serverName"` } @@ -6648,12 +8323,21 @@ type MCPOauthLoginRequest struct { // Experimental: MCPOauthLoginResult is part of an experimental API and may change or be // removed. type MCPOauthLoginResult struct { + // Opaque authorization identifier returned only for a host-managed redirect URI. The + // runtime also sends it as the OAuth state value, so the callback endpoint can read state + // and pass it with the full callback URL to session.mcp.oauth.complete. + AuthorizationID *string `json:"authorizationId,omitempty"` // URL the caller should open in a browser to complete OAuth. Omitted when cached tokens // were still valid and no browser interaction was needed — the server is already - // reconnected in that case. When present, the runtime starts the callback listener before - // returning and continues the flow in the background; completion is signaled via - // session.mcp_server_status_changed. + // reconnected in that case. For the default loopback flow, the runtime starts its listener + // before returning. With redirectUri, the host receives the callback and completes it + // through session.mcp.oauth.complete. The runtime continues the flow in the background and + // signals completion via session.mcp_server_status_changed. AuthorizationURL *string `json:"authorizationUrl,omitempty"` + // Runtime-issued owned flow identity; never a server name or installation operation ID. + LoginID *string `json:"loginId,omitempty"` + // Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. + Status *MCPOwnedOauthLoginStatus `json:"status,omitempty"` } // Host response to the pending OAuth request. @@ -6696,10 +8380,40 @@ func (MCPOauthPendingRequestResponseToken) Kind() MCPOauthPendingRequestResponse return MCPOauthPendingRequestResponseKindToken } +// Effect-free preparation bound to the existing local session, requester and installation, +// with frozen options. +// Experimental: MCPOauthPrepareLoginRequest is part of an experimental API and may change +// or be removed. +type MCPOauthPrepareLoginRequest struct { + // Text shown on the loopback callback page after successful authorisation. + CallbackSuccessMessage *string `json:"callbackSuccessMessage,omitempty"` + // Display name used by the incumbent OAuth client-registration flow. + ClientName *string `json:"clientName,omitempty"` + // Exact installation identity from owned inventory, never a server-name alias. + ExpectedInstallationID string `json:"expectedInstallationId"` + // Request a new authorisation rather than accepting a usable cached grant. + ForceReauth *bool `json:"forceReauth,omitempty"` + // Name recorded by the authoritative owned installation receipt. + ServerName string `json:"serverName"` +} + +// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth +// work. +// Experimental: MCPOauthPrepareLoginResult is part of an experimental API and may change or +// be removed. +type MCPOauthPrepareLoginResult struct { + // Original expiry, not extended by consumption, retries or cancellation. + ExpiresAt time.Time `json:"expiresAt"` + // Retain with the original requester and use for one login or cancellation. + LoginID string `json:"loginId"` +} + // Remote MCP server name for a passive OAuth status probe. // Experimental: MCPOauthProbeRequest is part of an experimental API and may change or be // removed. type MCPOauthProbeRequest struct { + // Exact owned receipt identity; probing never activates a dormant installation. + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` // Name of the configured remote MCP server to probe. ServerName string `json:"serverName"` } @@ -6816,6 +8530,8 @@ type MCPPlanConfigurationChange struct { type MCPPlanInstallRequest struct { // Protocol version and capabilities the caller requires. Contract CatalogClientContract `json:"contract"` + // The same existing attached session that owns the original catalogue candidate. + PolicySessionID *string `json:"policySessionId,omitempty"` // Configuration scope the plan targets. Defaults to user scope when omitted. Scope *MCPPlanScope `json:"scope,omitempty"` // What to plan: either a candidate handle from a previous search, or a card supplied @@ -7223,6 +8939,168 @@ func (r MCPPlanTransportChoiceRemote) Transport() MCPPlanTransportChoiceTranspor return MCPPlanTransportChoiceTransport(r.Discriminator) } +// Read-only preparation of one owned removal under fresh selected-session authority. +// Experimental: MCPPlanUninstallRequest is part of an experimental API and may change or be +// removed. +type MCPPlanUninstallRequest struct { + // Required authenticated bound installation capabilities. + Contract CatalogClientContract `json:"contract"` + // Exact receipt to inspect, not a server-name guess. + InstallationID string `json:"installationId"` + // Existing selected session on the original connection. + PolicySessionID string `json:"policySessionId"` +} + +// Inert, runtime-owned admission. The operation ID is known before confirmation or effects. +// Experimental: MCPPreparedInstall is part of an experimental API and may change or be +// removed. +type MCPPreparedInstall struct { + // Original plan expiry in Unix epoch milliseconds; preparation does not extend it. + ExpiresAtEpochMs int64 `json:"expiresAtEpochMs"` + // Original connection-owned operation, known before the first confirmation callback. + OperationID string `json:"operationId"` +} + +// Side-effect-free preparation of one original bound remote MCP choice. +// Experimental: MCPPrepareInstallRequest is part of an experimental API and may change or +// be removed. +type MCPPrepareInstallRequest struct { + // Exact selected alternative from that plan. + ChoiceID string `json:"choiceId"` + // Required bound catalogue and confirmed remote installation capabilities. + Contract CatalogClientContract `json:"contract"` + // Declared non-secret values. Non-empty only when the caller requires + // `mcp-configured-remote-installation`; omitted values use the card default. + Inputs []MCPInstallationInput `json:"inputs"` + // Original single-use bound plan, never a client-authored configuration. + PlanHandle string `json:"planHandle"` + // An existing local session attached to this connection, not permission to attach one. + PolicySessionID string `json:"policySessionId"` + // One entry per declared secret placeholder of the selected choice. Non-empty + // only when the caller requires `mcp-configured-remote-installation`. + Secrets []MCPInstallationSecret `json:"secrets"` + // The trusted host presents this choice alongside the exact secret placeholders. + SecretStorage MCPInstallationSecretStorage `json:"secretStorage"` + // The exact original source, used transiently only after confirmation. + Source MCPServerCardReference `json:"source"` +} + +// An MCP prompt descriptor. Server-provided non-standard fields are exposed under +// `additionalProperties`. +// Experimental: MCPPrompt is part of an experimental API and may change or be removed. +type MCPPrompt struct { + // Server-provided non-standard descriptor fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Arguments accepted by the prompt + Arguments []MCPPromptArgument `json:"arguments,omitzero"` + // Description of what this prompt provides + Description *string `json:"description,omitempty"` + // Icons associated with this prompt + Icons []MCPPromptIcon `json:"icons,omitzero"` + // Prompt-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // The programmatic name of the prompt + Name string `json:"name"` + // Human-readable display title + Title *string `json:"title,omitempty"` +} + +// An argument accepted by an MCP prompt. +// Experimental: MCPPromptArgument is part of an experimental API and may change or be +// removed. +type MCPPromptArgument struct { + // Server-provided non-standard argument fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Description of the argument + Description *string `json:"description,omitempty"` + // Argument-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // Name of the argument + Name string `json:"name"` + // Whether the argument is required; omission is distinct from false + Required *bool `json:"required,omitempty"` +} + +// An MCP prompt icon with standard size hints and preserved non-standard fields. +// Experimental: MCPPromptIcon is part of an experimental API and may change or be removed. +type MCPPromptIcon struct { + // Server-provided non-standard icon fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Icon MIME type, when known + MIMEType *string `json:"mimeType,omitempty"` + // Icon sizes, such as `48x48` or `any` + Sizes []string `json:"sizes,omitzero"` + // Icon URI + Src string `json:"src"` + // Theme hint for this icon + Theme *string `json:"theme,omitempty"` +} + +// An MCP prompt message with opaque JSON content preserved without flattening or +// content-type filtering. +// Experimental: MCPPromptMessage is part of an experimental API and may change or be +// removed. +type MCPPromptMessage struct { + // Server-provided non-standard message fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // The original MCP content block, including nested metadata and unfamiliar content types + Content any `json:"content"` + // Message-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // The role of the message sender + Role MCPPromptRole `json:"role"` +} + +// MCP server, prompt name, and optional string-valued arguments. +// Experimental: MCPPromptsGetRequest is part of an experimental API and may change or be +// removed. +type MCPPromptsGetRequest struct { + // String-valued arguments to pass to the prompt + Arguments map[string]string `json:"arguments,omitzero"` + // The programmatic name of the prompt + PromptName string `json:"promptName"` + // Name of the MCP server hosting the prompt + ServerName string `json:"serverName"` +} + +// Prompt messages returned by the MCP server without sending them to the model. +// Experimental: MCPPromptsGetResult is part of an experimental API and may change or be +// removed. +type MCPPromptsGetResult struct { + // Server-provided non-standard result fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Description of the prompt + Description *string `json:"description,omitempty"` + // Ordered prompt messages + Messages []MCPPromptMessage `json:"messages"` + // MCP result metadata + Meta map[string]any `json:"_meta,omitzero"` +} + +// MCP server whose prompts to enumerate. +// Experimental: MCPPromptsListRequest is part of an experimental API and may change or be +// removed. +type MCPPromptsListRequest struct { + // Opaque MCP pagination cursor from a prior `nextCursor` value + Cursor *string `json:"cursor,omitempty"` + // Name of the MCP server whose prompts to enumerate + ServerName string `json:"serverName"` +} + +// One page of prompts advertised by the named MCP server. +// Experimental: MCPPromptsListResult is part of an experimental API and may change or be +// removed. +type MCPPromptsListResult struct { + // Server-provided non-standard result fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // MCP result metadata + Meta map[string]any `json:"_meta,omitzero"` + // Opaque cursor for the next page, if the server has more prompts + NextCursor *string `json:"nextCursor,omitempty"` + // Prompts advertised by the server + Prompts []MCPPrompt `json:"prompts"` +} + // Registration parameters for an external MCP client. // Experimental: MCPRegisterExternalClientRequest is part of an experimental API and may // change or be removed. @@ -7453,6 +9331,8 @@ type MCPRestartServerRequest struct { // 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). Config MCPSerializableServerConfig `json:"config,omitempty"` + // Exact receipt identity for an explicit owned restart; configuration overrides are refused. + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` // Name of the MCP server to restart ServerName string `json:"serverName"` } @@ -7659,6 +9539,10 @@ type MCPServer struct { Error *string `json:"error,omitempty"` // Server name (config key) Name string `json:"name"` + // 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. + Owned *MCPServerOwnership `json:"owned,omitempty"` // 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. @@ -7672,6 +9556,9 @@ type MCPServer struct { // Connection status: connected, failed, needs-auth, pending, disabled, stopped, or // not_configured Status MCPServerStatus `json:"status"` + // Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for + // local and in-memory servers. + URL *string `json:"url,omitempty"` } // Set to `true` to use defaults, or provide an object with additional auth or OIDC settings. @@ -7856,6 +9743,14 @@ type MCPServerNeedsAuthInfo struct { Timestamp int64 `json:"timestamp"` } +// Owned installation that a listed MCP server's live configuration came from. +// Experimental: MCPServerOwnership is part of an experimental API and may change or be +// removed. +type MCPServerOwnership struct { + // Stable installation identifier from the owned installation receipt. + InstallationID string `json:"installationId"` +} + // Mode controlling how MCP server env values are resolved (`direct` or `indirect`). // Experimental: MCPSetEnvValueModeParams is part of an experimental API and may change or // be removed. @@ -7921,6 +9816,8 @@ type MCPStartServerRequest struct { // MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server // with its already-registered configuration (config-free start-by-name). Config MCPSerializableServerConfig `json:"config,omitempty"` + // Exact receipt identity for explicit owned activation in this session. + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` // Name of the MCP server to start ServerName string `json:"serverName"` } @@ -7941,6 +9838,8 @@ type MCPStartServersResult struct { // Experimental: MCPStopServerRequest is part of an experimental API and may change or be // removed. type MCPStopServerRequest struct { + // Exact owned receipt identity. Stop also forgets this session's durable activation. + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` // Name of the MCP server to stop ServerName string `json:"serverName"` } @@ -7975,6 +9874,26 @@ type MCPToolUI struct { Visibility []MCPToolUIVisibility `json:"visibility,omitzero"` } +// Exact inert removal plan. No configuration or credentials have changed. +// Experimental: MCPUninstallPlan is part of an experimental API and may change or be +// removed. +type MCPUninstallPlan struct { + // Original wall-clock expiry in milliseconds. Applying never renews it. + ExpiresAtEpochMs int64 `json:"expiresAtEpochMs"` + // Original owned receipt being removed. + Installation MCPInstallationSummary `json:"installation"` + // The original operation, inspectable and cancellable on this same connection. + OperationID string `json:"operationId"` + // Exact configured input slots owned by this installation, never shared OAuth tokens. + OwnedSecretCount int64 `json:"ownedSecretCount"` + // One-use original connection and authority-bound plan handle. + PlanHandle string `json:"planHandle"` + // Shared authentication is deliberately retained; revocation is a separate action. + PreservesSharedAuthentication bool `json:"preservesSharedAuthentication"` + // Whether removal restores a protected earlier configuration. + RestoresPreviousConfiguration bool `json:"restoresPreviousConfiguration"` +} + // Server name identifying the external client to remove. // Experimental: MCPUnregisterExternalClientRequest is part of an experimental API and may // change or be removed. @@ -8024,9 +9943,11 @@ type MetadataContextHeaviestMessagesResult struct { // Experimental: MetadataContextInfoRequest is part of an experimental API and may change or // be removed. type MetadataContextInfoRequest struct { - // Maximum output tokens allowed by the target model. Pass 0 if unknown. + // Requested output allowance to reserve against the combined context ceiling. Pass 0 to + // resolve the session's request cap, falling back to the model's advertised output limit. OutputTokenLimit int64 `json:"outputTokenLimit"` - // Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + // Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from + // the session. PromptTokenLimit int64 `json:"promptTokenLimit"` // Model identifier used for tokenization. Omit to use the session default. Used both for // token counting and to compute display values. @@ -8203,6 +10124,12 @@ type Model struct { Name string `json:"name"` // Policy state (if applicable) Policy *ModelPolicy `json:"policy,omitempty"` + // The model provider that produced this model, as a neutral reference (opaque id, + // human-readable label, and provider kind). Present on models returned by + // `session.model.list`, which resolves provider attribution from the session's account + // roster; absent on the flat `server.models.list`, which does not resolve a session roster. + // The model picker groups by this reference. + Provider *ModelProviderRef `json:"provider,omitempty"` // Context-window tiers this model offers, when the provider advertises them independently // of tiered token pricing. Copilot models carry their tiers in `billing.tokenPrices`; a // provider that has no pricing to publish (an agent host reached over AHP, for example) @@ -8210,6 +10137,11 @@ type Model struct { SupportedContextTiers []string `json:"supportedContextTiers,omitzero"` // Supported reasoning effort levels (only present if model supports reasoning effort) SupportedReasoningEfforts []string `json:"supportedReasoningEfforts,omitzero"` + // Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". + // Open vocabulary, passed through unchanged. It can name the vendor that serves the model + // instead of the one that built it, or a label that is not a vendor, such as + // "Experimental". Absent when the Copilot API reports no vendor. + Vendor *string `json:"vendor,omitempty"` // Warnings the service published for this model, such as a deprecated client version. // Present only when the service published at least one warning. The model remains usable; // hosts should surface these as advisory rather than blocking. @@ -8232,6 +10164,14 @@ type ModelApplyStartupOverlayRequest struct { DeferredResume *bool `json:"deferredResume,omitempty"` // Model required by device-managed policy, when configured. DeviceManagedModel *string `json:"deviceManagedModel,omitempty"` + // 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. + ManagedContextTier *string `json:"managedContextTier,omitempty"` + // 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. + ManagedReasoningEffort *string `json:"managedReasoningEffort,omitempty"` // 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. @@ -8249,6 +10189,26 @@ type ModelApplyStartupOverlayRequest struct { ServerManagedModel *string `json:"serverManagedModel,omitempty"` } +// Provider-reported model artifact metadata. +// Experimental: ModelArtifactDetails is part of an experimental API and may change or be +// removed. +type ModelArtifactDetails struct { + // Provider-reported model architecture. + Architecture *string `json:"architecture,omitempty"` + // Provider-reported model families. + Families []string `json:"families,omitzero"` + // Primary model family. + Family *string `json:"family,omitempty"` + // Artifact format, such as `gguf`. + Format *string `json:"format,omitempty"` + // Provider-reported parameter count label. + ParameterSize *string `json:"parameterSize,omitempty"` + // Provider-reported quantization label. + Quantization *string `json:"quantization,omitempty"` + // Provider-reported tokenizer. + Tokenizer *string `json:"tokenizer,omitempty"` +} + // Billing information // Experimental: ModelBilling is part of an experimental API and may change or be removed. type ModelBilling struct { @@ -8444,6 +10404,9 @@ type ModelCapabilitiesSupports struct { AdaptiveThinking *AdaptiveThinkingSupport `json:"adaptive_thinking,omitempty"` // Whether this model supports reasoning effort configuration ReasoningEffort *bool `json:"reasoningEffort,omitempty"` + // Whether the model supports provider-native thinking. Independent of configurable + // reasoning effort; omission means unknown. + Thinking *bool `json:"thinking,omitempty"` // Whether this model supports canonical tool calling ToolCalls *bool `json:"toolCalls,omitempty"` // Whether this model supports vision/image input @@ -8507,6 +10470,282 @@ type ModelPolicy struct { Terms *string `json:"terms,omitempty"` } +// Normalized model-provider adapter definitions available to the session, not discovered +// instances. +// Experimental: ModelProviderAdapterCatalog is part of an experimental API and may change +// or be removed. +type ModelProviderAdapterCatalog struct { + // Available provider adapters ordered by adapterId. + Providers []ModelProviderAdapterDescriptor `json:"providers"` +} + +// A normalized model-provider adapter in the session's effective catalog. +// Experimental: ModelProviderAdapterDescriptor is part of an experimental API and may +// change or be removed. +type ModelProviderAdapterDescriptor struct { + // Stable opaque identity for routing to this adapter. Unique in the effective catalog, + // independent of live registration generations. + AdapterID string `json:"adapterId"` + // Adapter-declared policy for passive and automatic discovery. + AutomaticDiscovery ModelProviderAutomaticDiscoveryPolicy `json:"automaticDiscovery"` + // Human-readable provider name. + DisplayName string `json:"displayName"` + // Operations supported by this provider adapter. + Operations []ModelProviderAdapterOperationDescriptor `json:"operations"` + // Attribution for the adapter itself. + Provenance ModelProviderAttribution `json:"provenance"` + // Descriptive provider family, such as `ollama`. Different adapters may have the same + // family; use adapterId for routing. + ProviderKind string `json:"providerKind"` +} + +// An operation supported by a model-provider adapter. +// Experimental: ModelProviderAdapterOperationDescriptor is part of an experimental API and +// may change or be removed. +type ModelProviderAdapterOperationDescriptor struct { + // Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported + // on discover. No external references are resolved. Omitted or null input selects defaults + // when requiresInput is false. Without a schema, the adapter validates supplied input. + InputSchema any `json:"inputSchema,omitempty"` + // Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and + // duplicate declarations are rejected. + Name string `json:"name"` +} + +// Contributor attribution, independent of routing identity and authorization. +// Experimental: ModelProviderAttribution is part of an experimental API and may change or +// be removed. +type ModelProviderAttribution struct { + // Human-readable contributor name, not the adapter display name. + OwnerDisplayName *string `json:"ownerDisplayName,omitempty"` + // Stable contributor identifier. Required and nonblank for extension and custom sources; + // optional for built-in and configured sources. Does not grant authority. + OwnerID *string `json:"ownerId,omitempty"` + // Kind of component that supplied the adapter. Attribution does not confer authority. + Source ModelProviderProvenanceSource `json:"source"` +} + +// Adapter-declared policy that tells clients whether discovery may run automatically. +// Experimental: ModelProviderAutomaticDiscoveryPolicy is part of an experimental API and +// may change or be removed. +type ModelProviderAutomaticDiscoveryPolicy struct { + // Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + Mode ModelProviderAutomaticDiscoveryMode `json:"mode"` + // Maximum network scope used by this adapter during discovery. + NetworkScope ModelProviderDiscoveryNetworkScope `json:"networkScope"` + // True when discovery requires non-null caller input. Omission or null is rejected before + // adapter execution. When false, omitted or null input selects adapter defaults without + // schema validation. + RequiresInput bool `json:"requiresInput"` + // True when the adapter must be enabled by a trusted owner, such as a trusted extension, + // before automatic discovery may run. + RequiresTrust bool `json:"requiresTrust"` +} + +// Provider configuration prepared from a discovered model. Preparing a plan changes +// nothing: it neither registers the model with the session nor writes durable +// configuration. To apply it, pass `provider` and `model` to `session.provider.add`, +// omitting whichever the dispositions report as already configured. +// Experimental: ModelProviderConfigurationPlan is part of an experimental API and may +// change or be removed. +type ModelProviderConfigurationPlan struct { + // Model definition prepared from the discovered model. Capability fields the provider did + // not report stay omitted rather than being asserted false. + Model ProviderModelConfig `json:"model"` + // Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is + // already registered and the caller can select it without adding anything. + ModelDisposition ModelProviderConfigurationDisposition `json:"modelDisposition"` + // Provider connection prepared from the instance's inference metadata. Carries no + // credential; supply one if the endpoint requires it. + Provider NamedProviderConfig `json:"provider"` + // Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider + // with the same endpoint is already registered and `provider` restates it under its + // existing name; adding it again is rejected as a duplicate. + ProviderDisposition ModelProviderConfigurationDisposition `json:"providerDisposition"` + // Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is + // applied. + SelectionID string `json:"selectionId"` + // Non-fatal warnings carried over from the discovered model, such as capabilities the + // provider did not report. + Warnings []ModelProviderWarning `json:"warnings"` +} + +// One model provider available to the session — the model analog of the account +// `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never +// branches on kind. +// Experimental: ModelProviderDescriptor is part of an experimental API and may change or be +// removed. +type ModelProviderDescriptor struct { + // Opaque, stable provider id, stamped onto every model this provider returns. + ID string `json:"id"` + // The neutral provider kind. + Kind ModelProviderKind `json:"kind"` + // Human-readable menu label, owned by the runtime so every consumer renders identical text. + Label string `json:"label"` + // Stable ordering key for presenting providers in a deterministic sequence. + Ordering int64 `json:"ordering"` +} + +// Provider discovery parameters. +// Experimental: ModelProviderDiscoverRequest is part of an experimental API and may change +// or be removed. +type ModelProviderDiscoverRequest struct { + // Opaque adapter identity returned by `session.providers.getCatalog`. + AdapterID string `json:"adapterId"` + // Provider-specific JSON input. Omission or null selects adapter defaults unless + // requiresInput is true. Non-null input is validated against the advertised Draft 7 schema + // when present; otherwise validation belongs to the adapter. + Input any `json:"input,omitempty"` +} + +// Provider instances found by a discovery operation. +// Experimental: ModelProviderDiscoverResult is part of an experimental API and may change +// or be removed. +type ModelProviderDiscoverResult struct { + // Discovered provider instances. Empty when passive default discovery finds no reachable + // provider. + Instances []ModelProviderInstance `json:"instances"` + // Typed operation outcome. Passive discovery can return `absent` with an empty instance + // list. + Outcome ModelProviderOperationOutcome `json:"outcome"` +} + +// Provider status request parameters. +// Experimental: ModelProviderGetStatusRequest is part of an experimental API and may change +// or be removed. +type ModelProviderGetStatusRequest struct { + // Provider instance reference returned by discovery. + Instance ModelProviderInstanceReference `json:"instance"` +} + +// A normalized model-provider instance discovered by the runtime. +// Experimental: ModelProviderInstance is part of an experimental API and may change or be +// removed. +type ModelProviderInstance struct { + // Human-readable instance name. + DisplayName string `json:"displayName"` + // Inference API endpoint when the provider exposes one separately from its management + // endpoint. + InferenceEndpoint *string `json:"inferenceEndpoint,omitempty"` + // Transport to use for inference against this instance. + InferenceTransport *ProviderEndpointTransport `json:"inferenceTransport,omitempty"` + // Provider family to use for inference against this instance. + InferenceType *ProviderEndpointType `json:"inferenceType,omitempty"` + // Wire API to use for inference against this instance, when required by the provider family. + InferenceWireAPI *ProviderEndpointWireAPI `json:"inferenceWireApi,omitempty"` + // Attribution for the adapter that produced this instance. + Provenance ModelProviderProvenance `json:"provenance"` + // Self-contained reference for subsequent provider operations. + Reference ModelProviderInstanceReference `json:"reference"` +} + +// Serializable reference to a discovered provider instance. +// Experimental: ModelProviderInstanceReference is part of an experimental API and may +// change or be removed. +type ModelProviderInstanceReference struct { + // Stable opaque identity of the adapter that owns this reference. Must be present in the + // target session's effective catalog. + AdapterID string `json:"adapterId"` + // Stable instance identifier derived by the provider adapter, such as + // `ollama:{normalizedEndpoint}`. + ID string `json:"id"` + // Absolute provider management URI. The adapter validates normalization, supported schemes, + // and permission to access it against its bound configuration; a reference does not grant + // authority. + ManagementEndpoint string `json:"managementEndpoint"` + // Descriptive provider family. Must match the selected adapter; not a routing key. + ProviderKind string `json:"providerKind"` +} + +// Provider model inventory request parameters. +// Experimental: ModelProviderModelsListRequest is part of an experimental API and may +// change or be removed. +type ModelProviderModelsListRequest struct { + // Provider instance reference returned by discovery. + Instance ModelProviderInstanceReference `json:"instance"` +} + +// Typed provider-operation outcome. Use the code for control flow and the optional message +// for display. +// Experimental: ModelProviderOperationOutcome is part of an experimental API and may change +// or be removed. +type ModelProviderOperationOutcome struct { + // Machine-readable operation outcome. + Code ModelProviderOperationOutcomeCode `json:"code"` + // Human-readable detail for non-success outcomes. + Message *string `json:"message,omitempty"` +} + +// A discovered instance and one of its models to translate into provider configuration. +// Pass back the instance and model as returned by `session.providers.discover` and +// `session.providers.models.list`. +// Experimental: ModelProviderPrepareConfigurationRequest is part of an experimental API and +// may change or be removed. +type ModelProviderPrepareConfigurationRequest struct { + // The discovered instance that serves the model. + Instance ModelProviderInstance `json:"instance"` + // The discovered model to configure. + Model DiscoveredModel `json:"model"` +} + +// Attribution for the adapter that produced a provider row. +// Experimental: ModelProviderProvenance is part of an experimental API and may change or be +// removed. +type ModelProviderProvenance struct { + // Stable opaque adapter identity from the effective catalog. Treat this as a whole + // identifier, not a parseable owner or kind. + AdapterID string `json:"adapterId"` + // Human-readable contributor name, not the adapter display name. + OwnerDisplayName *string `json:"ownerDisplayName,omitempty"` + // Stable contributor identifier when the adapter has an owner outside the runtime. + // Independent of the contribution mechanism and not a routing key. + OwnerID *string `json:"ownerId,omitempty"` + // Descriptive provider family that produced this row; not a routing key. + ProviderKind string `json:"providerKind"` + // Kind of component that supplied the adapter. + Source ModelProviderProvenanceSource `json:"source"` +} + +// A neutral reference to the model provider that produced a model: an opaque id, a +// human-readable label, and the provider kind. Carried on each enumerated Model so +// consumers can group by provider without reaching into a provider-shaped internal type. +// Experimental: ModelProviderRef is part of an experimental API and may change or be +// removed. +type ModelProviderRef struct { + // Opaque, stable id of the provider that produced this model. Matches the enumerated + // `ModelProviderDescriptor.id`. + ID string `json:"id"` + // The provider kind. + Kind ModelProviderKind `json:"kind"` + // Human-readable provider label, owned by the runtime so every consumer renders identical + // text. + Label string `json:"label"` +} + +// Current health information for a provider instance. +// Experimental: ModelProviderStatus is part of an experimental API and may change or be +// removed. +type ModelProviderStatus struct { + // Normalized provider instance. + Instance ModelProviderInstance `json:"instance"` + // Typed operation outcome. + Outcome ModelProviderOperationOutcome `json:"outcome"` + // Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + Status string `json:"status"` + // Provider-reported version. + Version *string `json:"version,omitempty"` +} + +// A non-fatal provider observation warning. +// Experimental: ModelProviderWarning is part of an experimental API and may change or be +// removed. +type ModelProviderWarning struct { + // Machine-readable warning code. + Code string `json:"code"` + // Human-readable warning message. + Message string `json:"message"` +} + // Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are // intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs // remain exempt from repository-only policy but are restricted by this host list. Omit or @@ -8784,6 +11023,9 @@ type NamedProviderConfig struct { HasBearerTokenProvider *bool `json:"hasBearerTokenProvider,omitempty"` // Additional HTTP headers included with provider requests. Headers map[string]string `json:"headers,omitzero"` + // The product serving the provider's models, reported in telemetry as `model_provider`. + // Only affects telemetry. + ModelProvider *ProviderConfigModelProvider `json:"modelProvider,omitempty"` // Unique provider name used to qualify model selection IDs. Name string `json:"name"` // Transport used to communicate with the provider. @@ -9034,6 +11276,20 @@ func (PermissionDecisionApprovePermanently) Kind() PermissionDecisionKind { return PermissionDecisionKindApprovePermanently } +// Approve file-tool read access to specific directories for this logical session, including +// continuation or resume. +// Experimental: PermissionDecisionApproveReadOnlyForSession is part of an experimental API +// and may change or be removed. +type PermissionDecisionApproveReadOnlyForSession struct { + // Canonical directories covered by the read-only grant + Directories []string `json:"directories"` +} + +func (PermissionDecisionApproveReadOnlyForSession) permissionDecision() {} +func (PermissionDecisionApproveReadOnlyForSession) Kind() PermissionDecisionKind { + return PermissionDecisionKindApproveReadOnlyForSession +} + // Permission-decision variant indicating the request was cancelled before use, with an // optional reason. // Experimental: PermissionDecisionCancelled is part of an experimental API and may change @@ -9243,21 +11499,6 @@ func (PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess) Kin return PermissionDecisionApproveForLocationApprovalKindExtensionPermissionAccess } -// Location-scoped factory approval, optionally narrowed by approval key. -// Experimental: PermissionDecisionApproveForLocationApprovalFactory is part of an -// experimental API and may change or be removed. -type PermissionDecisionApproveForLocationApprovalFactory struct { - // Optional factory operation name or canonical approval key; when omitted, the approval - // covers all factory operations. - ApprovalKey *string `json:"approvalKey,omitempty"` -} - -func (PermissionDecisionApproveForLocationApprovalFactory) permissionDecisionApproveForLocationApproval() { -} -func (PermissionDecisionApproveForLocationApprovalFactory) Kind() PermissionDecisionApproveForLocationApprovalKind { - return PermissionDecisionApproveForLocationApprovalKindFactory -} - // Location-scoped approval details for an MCP server tool, or all tools on the server when // `toolName` is null. // Experimental: PermissionDecisionApproveForLocationApprovalMCP is part of an experimental @@ -9313,6 +11554,21 @@ func (PermissionDecisionApproveForLocationApprovalRead) Kind() PermissionDecisio return PermissionDecisionApproveForLocationApprovalKindRead } +// Location-scoped workflow approval, optionally narrowed by approval key. +// Experimental: PermissionDecisionApproveForLocationApprovalWorkflow is part of an +// experimental API and may change or be removed. +type PermissionDecisionApproveForLocationApprovalWorkflow struct { + // Optional workflow operation name or canonical approval key; when omitted, the approval + // covers all workflow operations. + ApprovalKey *string `json:"approvalKey,omitempty"` +} + +func (PermissionDecisionApproveForLocationApprovalWorkflow) permissionDecisionApproveForLocationApproval() { +} +func (PermissionDecisionApproveForLocationApprovalWorkflow) Kind() PermissionDecisionApproveForLocationApprovalKind { + return PermissionDecisionApproveForLocationApprovalKindWorkflow +} + // Location-scoped approval details for filesystem write operations. // Experimental: PermissionDecisionApproveForLocationApprovalWrite is part of an // experimental API and may change or be removed. @@ -9421,21 +11677,6 @@ func (PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess) Kind return PermissionDecisionApproveForSessionApprovalKindExtensionPermissionAccess } -// Session-scoped factory approval, optionally narrowed by approval key. -// Experimental: PermissionDecisionApproveForSessionApprovalFactory is part of an -// experimental API and may change or be removed. -type PermissionDecisionApproveForSessionApprovalFactory struct { - // Optional factory operation name or canonical approval key; when omitted, the approval - // covers all factory operations. - ApprovalKey *string `json:"approvalKey,omitempty"` -} - -func (PermissionDecisionApproveForSessionApprovalFactory) permissionDecisionApproveForSessionApproval() { -} -func (PermissionDecisionApproveForSessionApprovalFactory) Kind() PermissionDecisionApproveForSessionApprovalKind { - return PermissionDecisionApproveForSessionApprovalKindFactory -} - // Session-scoped approval details for an MCP server tool, or all tools on the server when // `toolName` is null. // Experimental: PermissionDecisionApproveForSessionApprovalMCP is part of an experimental @@ -9490,6 +11731,21 @@ func (PermissionDecisionApproveForSessionApprovalRead) Kind() PermissionDecision return PermissionDecisionApproveForSessionApprovalKindRead } +// Session-scoped workflow approval, optionally narrowed by approval key. +// Experimental: PermissionDecisionApproveForSessionApprovalWorkflow is part of an +// experimental API and may change or be removed. +type PermissionDecisionApproveForSessionApprovalWorkflow struct { + // Optional workflow operation name or canonical approval key; when omitted, the approval + // covers all workflow operations. + ApprovalKey *string `json:"approvalKey,omitempty"` +} + +func (PermissionDecisionApproveForSessionApprovalWorkflow) permissionDecisionApproveForSessionApproval() { +} +func (PermissionDecisionApproveForSessionApprovalWorkflow) Kind() PermissionDecisionApproveForSessionApprovalKind { + return PermissionDecisionApproveForSessionApprovalKindWorkflow +} + // Session-scoped approval details for filesystem write operations. // Experimental: PermissionDecisionApproveForSessionApprovalWrite is part of an experimental // API and may change or be removed. @@ -9949,21 +12205,6 @@ func (PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess) Kind( return PermissionsLocationsAddToolApprovalDetailsKindExtensionPermissionAccess } -// Location-persisted factory approval, optionally narrowed by approval key. -// Experimental: PermissionsLocationsAddToolApprovalDetailsFactory is part of an -// experimental API and may change or be removed. -type PermissionsLocationsAddToolApprovalDetailsFactory struct { - // Optional factory operation name or canonical approval key; when omitted, the approval - // covers all factory operations. - ApprovalKey *string `json:"approvalKey,omitempty"` -} - -func (PermissionsLocationsAddToolApprovalDetailsFactory) permissionsLocationsAddToolApprovalDetails() { -} -func (PermissionsLocationsAddToolApprovalDetailsFactory) Kind() PermissionsLocationsAddToolApprovalDetailsKind { - return PermissionsLocationsAddToolApprovalDetailsKindFactory -} - // Location-persisted tool approval details for an MCP server tool, or all tools when // `toolName` is null. // Experimental: PermissionsLocationsAddToolApprovalDetailsMCP is part of an experimental @@ -10017,6 +12258,21 @@ func (PermissionsLocationsAddToolApprovalDetailsRead) Kind() PermissionsLocation return PermissionsLocationsAddToolApprovalDetailsKindRead } +// Location-persisted workflow approval, optionally narrowed by approval key. +// Experimental: PermissionsLocationsAddToolApprovalDetailsWorkflow is part of an +// experimental API and may change or be removed. +type PermissionsLocationsAddToolApprovalDetailsWorkflow struct { + // Optional workflow operation name or canonical approval key; when omitted, the approval + // covers all workflow operations. + ApprovalKey *string `json:"approvalKey,omitempty"` +} + +func (PermissionsLocationsAddToolApprovalDetailsWorkflow) permissionsLocationsAddToolApprovalDetails() { +} +func (PermissionsLocationsAddToolApprovalDetailsWorkflow) Kind() PermissionsLocationsAddToolApprovalDetailsKind { + return PermissionsLocationsAddToolApprovalDetailsKindWorkflow +} + // Location-persisted tool approval details for filesystem write operations. // Experimental: PermissionsLocationsAddToolApprovalDetailsWrite is part of an experimental // API and may change or be removed. @@ -10144,8 +12400,9 @@ type PermissionsSetModeRequest struct { AssistedApprovalModel *string `json:"assistedApprovalModel,omitempty"` // Permission mode to apply Mode PermissionMode `json:"mode"` - // Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK - // callers. + // 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. Source *PermissionModeSource `json:"source,omitempty"` } @@ -10730,6 +12987,10 @@ type ProviderConfig struct { // Well-known model ID used for capability lookup. When set, agent behavior config and token // limits are inferred from this model. ModelID *string `json:"modelId,omitempty"` + // The product serving the model, reported in telemetry as `model_provider`. Set it when + // `type` alone cannot identify the product, such as Ollama or LM Studio behind an + // OpenAI-compatible endpoint. Only affects telemetry. + ModelProvider *ProviderConfigModelProvider `json:"modelProvider,omitempty"` // Provider name used for model and telemetry attribution. ProviderName *string `json:"providerName,omitempty"` // Provider transport. Defaults to "http". @@ -10752,6 +13013,18 @@ type ProviderConfigAzure struct { APIVersion *string `json:"apiVersion,omitempty"` } +// A provider offered for interactive login. +// Experimental: ProviderDescriptor is part of an experimental API and may change or be +// removed. +type ProviderDescriptor struct { + // Whether this provider is currently available to sign in with. + Available bool `json:"available"` + // The neutral provider kind. + Kind LoginProviderKind `json:"kind"` + // Human-readable menu label, owned by the runtime so every consumer renders identical text. + Label string `json:"label"` +} + // A snapshot of the provider endpoint the session is currently configured to talk to. // Experimental: ProviderEndpoint is part of an experimental API and may change or be // removed. @@ -10882,6 +13155,32 @@ type ProviderTokenAcquireResult struct { Token string `json:"token"` } +// Host-managed model selection ids to withdraw from the session's BYOK registry. +// Experimental: ProviderWithdrawRequest is part of an experimental API and may change or be +// removed. +type ProviderWithdrawRequest struct { + // 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 `json:"models"` +} + +// What the withdrawal actually removed from the registry. +// Experimental: ProviderWithdrawResult is part of an experimental API and may change or be +// removed. +type ProviderWithdrawResult struct { + // 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 *bool `json:"modelDeselected,omitempty"` + // 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 `json:"providersRemoved"` + // Selection ids that were registered and are now withdrawn. Excludes requested ids that + // were not present. + Withdrawn []string `json:"withdrawn"` +} + // Attachment union accepted by push input, covering files, directories, GitHub objects, // blobs, snippets, and extension context. // Experimental: PushAttachment is part of an experimental API and may change or be removed. @@ -11894,19 +14193,6 @@ type ResponseFormat struct { Type ResponseFormatType `json:"type"` } -// Options controlling factory invocation. -// Experimental: RunOptions is part of an experimental API and may change or be removed. -type RunOptions struct { - // Per-invocation resource ceiling overrides. - Limits *FactoryRunLimits `json:"limits,omitempty"` - // Whether to emit factory phase names to the session transcript. - LogPhaseNames *bool `json:"logPhaseNames,omitempty"` - // Whether to notify the originating session when the factory completes. - NotifyOnComplete *bool `json:"notifyOnComplete,omitempty"` - // Run identifier whose journal and progress should seed this resumed run. - ResumeFromRunID *string `json:"resumeFromRunId,omitempty"` -} - // Experimental: RuntimeShutdownResult is part of an experimental API and may change or be // removed. type RuntimeShutdownResult struct { @@ -11937,6 +14223,12 @@ type SandboxConfig struct { AllowDevToolAccess *bool `json:"allowDevToolAccess,omitempty"` // Credential-injection capability flags. Auth *SandboxConfigAuth `json:"auth,omitempty"` + // 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. + Credentials *SandboxCredentialsConfig `json:"credentials,omitempty"` // Whether sandboxing is enabled for the session. Enabled bool `json:"enabled"` // The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. @@ -11970,14 +14262,17 @@ type SandboxConfig struct { // Experimental: SandboxConfigAuth is part of an experimental API and may change or be // removed. type SandboxConfigAuth struct { - // Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the - // OS keyring the sandbox blocks. Default: false (opt-in). + // 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). Gh *bool `json:"gh,omitempty"` - // Whether to inject git credentials as an `http..extraheader` so authenticated HTTPS - // git works inside the sandbox without the shell-based credential helper the sandbox - // blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, - // GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's - // own helper before the sandbox is applied. Default: false (opt-in). + // Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child + // receives a fake `http..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). Git *bool `json:"git,omitempty"` } @@ -12083,6 +14378,15 @@ type SandboxConfigUserPolicySeatbelt struct { KeychainAccess *bool `json:"keychainAccess,omitempty"` } +// Whole-value environment credential masking for sandboxed children. +// Experimental: SandboxCredentialsConfig is part of an experimental API and may change or +// be removed. +type SandboxCredentialsConfig struct { + // Environment variable names and their HTTPS injection destinations. Absent variables stay + // absent. No real values or sentinels are stored in this map. + EnvVars map[string]SandboxMaskedEnvVar `json:"envVars"` +} + // Request to disable sandboxing for the current session while resolving an active // sandbox-bypass permission prompt. // Experimental: SandboxDisableForSessionRequest is part of an experimental API and may @@ -12117,8 +14421,31 @@ type SandboxEnforcementStatus struct { Required bool `json:"required"` } +// Request to accept the sandbox path grant offered on an active sandbox escalation +// permission prompt. +// Experimental: SandboxGrantPathForRequestRequest is part of an experimental API and may +// change or be removed. +type SandboxGrantPathForRequestRequest struct { + // Optional attribution for the permission decision. + DecisionContext *PermissionDecisionContext `json:"decisionContext,omitempty"` + // Identifier of the exact pending sandbox escalation permission request whose + // sandboxPathGrant to accept. + RequestID string `json:"requestId"` +} + +// Result of accepting a sandbox path grant. +// Experimental: SandboxGrantPathForRequestResult is part of an experimental API and may +// change or be removed. +type SandboxGrantPathForRequestResult struct { + // Whether this call resolved the pending request and added the path to the session's + // sandbox policy. + Success bool `json:"success"` +} + // 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. // Experimental: SandboxHostCapability is part of an experimental API and may change or be // removed. type SandboxHostCapability struct { @@ -12128,9 +14455,11 @@ type SandboxHostCapability struct { // `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). Name string `json:"name"` // 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. @@ -12151,8 +14480,12 @@ type SandboxHostCapability struct { // 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. +// 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. // Experimental: SandboxHostCapabilityName is part of an experimental API and may change or // be removed. type SandboxHostCapabilityName string @@ -12175,6 +14508,51 @@ type SandboxHostSupport struct { Supported bool `json:"supported"` } +// Destinations authorized to receive one masked environment credential. +// Experimental: SandboxMaskedEnvVar is part of an experimental API and may change or be +// removed. +type SandboxMaskedEnvVar struct { + // 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. + InjectHosts []string `json:"injectHosts"` +} + +// Result of creating the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaCreateResult is part of an experimental API and may change or +// be removed. +type SandboxProxyCaCreateResult struct { + // Absolute path of the public certificate of the certificate authority, in PEM format. + CertificatePath string `json:"certificatePath"` +} + +// Identifies the credential hosts that the persistent certificate authority of the sandbox +// credential proxy must cover. The runtime always adds the hosts from the saved user +// settings. +// Experimental: SandboxProxyCaRequest is part of an experimental API and may change or be +// removed. +type SandboxProxyCaRequest struct { + // The sandbox configuration that the host gives its sessions. The runtime reads the + // credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` +} + +// Status of the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaStatus is part of an experimental API and may change or be +// removed. +type SandboxProxyCaStatus struct { + // Whether this process can add the certificate authority to OS trust without credentials + // from a different user. False where OS trust is unsupported, and on Windows when the + // process cannot elevate itself to write the machine trust store. When false, do not offer + // to set up the certificate authority. + CanInstall bool `json:"canInstall"` + // Human-readable reason for the state. On `installed` or `notInstalled`, present only when + // the certificate authority must be rotated, and then says why. + Detail *string `json:"detail,omitempty"` + // The state of the certificate authority. + State SandboxProxyCaState `json:"state"` +} + // Register an absolute-time scheduled prompt. // Experimental: ScheduleAddAtRequest is part of an experimental API and may change or be // removed. @@ -12548,6 +14926,11 @@ type ServerSkillList struct { Skills []ServerSkill `json:"skills"` } +// Experimental: SessionAccountsLoginCancelResult is part of an experimental API and may +// change or be removed. +type SessionAccountsLoginCancelResult struct { +} + // Current activity flags for the session. // Experimental: SessionActivity is part of an experimental API and may change or be removed. type SessionActivity struct { @@ -12581,24 +14964,10 @@ type SessionAgentListRequest struct { type SessionAgentSetPromptResult struct { } -// Credential-free authentication identity safe to expose to hosts and user interfaces. +// Current authentication information, or null when no authentication is active. // Experimental: SessionAuthInfoResult is part of an experimental API and may change or be // removed. -type SessionAuthInfoResult struct { - // Snapshot of the authenticated user's Copilot subscription info, if known - CopilotUser *CopilotUserResponse `json:"copilotUser,omitempty"` - // Name of the environment variable that supplied the credential, when applicable - EnvVar *string `json:"envVar,omitempty"` - // Authentication host - Host string `json:"host"` - // Authenticated login, when available - Login *string `json:"login,omitempty"` - // Opaque SDK GitHub credential registration backing this identity. Routing metadata only; - // never a credential. - RegistrationID *string `json:"registrationId,omitempty"` - // Authentication type - Type AuthInfoType `json:"type"` -} +type SessionAuthInfoResult = *AuthIdentity // Internal GitHub login parameters. // Experimental: SessionAuthLoginRequest is part of an experimental API and may change or be @@ -12710,6 +15079,16 @@ type SessionCompletionItem struct { RangeStart *int64 `json:"rangeStart,omitempty"` } +// The IDE a host is connected to, as reported to the session. +// Experimental: SessionConnectedIdeInfo is part of an experimental API and may change or be +// removed. +type SessionConnectedIdeInfo struct { + // Display name of the connected IDE, for example `VS Code`. + IdeName string `json:"ideName"` + // Absolute path of the workspace folder the IDE has open. + WorkspaceFolder string `json:"workspaceFolder"` +} + // Pre-resolved working-directory context for session startup. // Experimental: SessionContext is part of an experimental API and may change or be removed. type SessionContext struct { @@ -12730,7 +15109,8 @@ type SessionContext struct { // Experimental: SessionContextAttribution is part of an experimental API and may change or // be removed. type SessionContextAttribution struct { - // Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors + // Output reservation overlapping the displayed prompt allowance plus the tokens past the + // effective input budget's buffer-exhaustion blocking threshold. Mirrors // `SessionContextInfo.bufferTokens`. BufferTokens int64 `json:"bufferTokens"` // The six normalized `/context` header buckets, computed from the same tokenization as @@ -12746,9 +15126,9 @@ type SessionContextAttribution struct { // Flat list of per-source attribution entries. Group by `kind` and render unrecognized // kinds generically. Nesting and rollups are expressed via `parentId`. Entries []SessionContextAttributionEntriesItem `json:"entries"` - // Prompt limit plus the model's output reserve: the full context window - // `categories.freeSpace` and `categories.buffer` are measured against. Mirrors - // `SessionContextInfo.limit`. + // Advertised prompt allowance for the selected context tier: the denominator for + // context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. + // Mirrors `SessionContextInfo.limit`. Limit int64 `json:"limit"` // The concrete model id the entire breakdown was tokenized against (feeds the per-model // token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the @@ -12759,8 +15139,8 @@ type SessionContextAttribution struct { // `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected // model), `default` (a fallback before any model is known). ModelSource string `json:"modelSource"` - // Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` - // context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + // Effective input budget after reserving requested output against the combined context + // ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. PromptTokenLimit int64 `json:"promptTokenLimit"` // Total token count of the current context window the entries are measured against (system // message + conversation messages + tool definitions — the same total reported by @@ -12773,7 +15153,7 @@ type SessionContextAttribution struct { // describe window capacity rather than occupied context, so the values do not sum to // `totalTokens`. type SessionContextAttributionCategories struct { - // Output reserve plus post-blocking-threshold buffer. + // Overlapping output reservation plus post-blocking-threshold buffer. Buffer int64 `json:"buffer"` // Custom-instructions tokens (0 when none are configured). CustomInstructions int64 `json:"customInstructions"` @@ -12823,21 +15203,25 @@ type SessionContextAttributionEntriesItem struct { // Experimental: SessionContextInfo is part of an experimental API and may change or be // removed. type SessionContextInfo struct { - // Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + // Output reservation overlapping the displayed prompt allowance plus tokens after the + // effective input budget's buffer-exhaustion blocking threshold (default 95%). BufferTokens int64 `json:"bufferTokens"` // Token count at which background compaction starts (configurable percentage of // promptTokenLimit) CompactionThreshold int64 `json:"compactionThreshold"` // Tokens consumed by user/assistant/tool messages ConversationTokens int64 `json:"conversationTokens"` - // Prompt token limit plus the model's full output token limit. + // Advertised prompt allowance for the selected context tier, without adding output tokens. + // The denominator for context-usage displays. Limit int64 `json:"limit"` // Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes // deferred tools) MCPToolsTokens int64 `json:"mcpToolsTokens"` // The model used for token counting ModelName string `json:"modelName"` - // Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + // Effective input budget: the selected tier's prompt allowance bounded by the combined + // context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when + // limits are unspecified. PromptTokenLimit int64 `json:"promptTokenLimit"` // Tokens consumed by the system prompt SystemTokens int64 `json:"systemTokens"` @@ -12877,13 +15261,6 @@ type SessionExtensionsReloadResult struct { type SessionExtensionsSendAttachmentsToMessageResult struct { } -// Experimental: SessionFactoryPauseAtCheckpointResult is part of an experimental API and -// may change or be removed. -type SessionFactoryPauseAtCheckpointResult struct { - // Whether this execution attempt must pause or may continue. - Action FactoryPauseCheckpointAction `json:"action"` -} - // File path, content to append, and optional mode for the client-provided session // filesystem. Implementations create parent directories as needed. // Experimental: SessionFSAppendFileRequest is part of an experimental API and may change or @@ -12906,6 +15283,9 @@ type SessionFSError struct { Code SessionFSErrorCode `json:"code"` // Free-form detail about the error, for logging/diagnostics Message *string `json:"message,omitempty"` + // For failed writeFile requests only: true if the provider changed the target before + // failing. Omit when unknown or unchanged. + WriteChanged *bool `json:"writeChanged,omitempty"` } // Path to test for existence in the client-provided session filesystem. @@ -12994,6 +15374,26 @@ type SessionFSReaddirWithTypesResult struct { Error *SessionFSError `json:"error,omitempty"` } +// Path of the binary file to read from the client-provided session filesystem. +// Experimental: SessionFSReadFileBytesRequest is part of an experimental API and may change +// or be removed. +type SessionFSReadFileBytesRequest struct { + // Path using SessionFs conventions + Path string `json:"path"` + // Target session identifier + SessionID string `json:"sessionId"` +} + +// File bytes as standard base64, or a filesystem error if the read failed. +// Experimental: SessionFSReadFileBytesResult is part of an experimental API and may change +// or be removed. +type SessionFSReadFileBytesResult struct { + // Exact file bytes encoded as standard base64 + Content string `json:"content"` + // Describes a filesystem error. + Error *SessionFSError `json:"error,omitempty"` +} + // Path of the file to read from the client-provided session filesystem. // Experimental: SessionFSReadFileRequest is part of an experimental API and may change or // be removed. @@ -13046,6 +15446,9 @@ type SessionFSRmRequest struct { // Experimental: SessionFSSetProviderCapabilities is part of an experimental API and may // change or be removed. type SessionFSSetProviderCapabilities struct { + // Whether the provider supports binary reads and writes through sessionFs.readFileBytes and + // sessionFs.writeFileBytes + Binary *bool `json:"binary,omitempty"` // Whether the provider supports SQLite query/exists operations Sqlite *bool `json:"sqlite,omitempty"` } @@ -13200,6 +15603,21 @@ type SessionFSStatResult struct { Size int64 `json:"size"` } +// File path, standard-base64-encoded bytes to write, and optional mode for the +// client-provided session filesystem. +// Experimental: SessionFSWriteFileBytesRequest is part of an experimental API and may +// change or be removed. +type SessionFSWriteFileBytesRequest struct { + // Exact file bytes encoded as standard base64 + Content string `json:"content"` + // Optional POSIX-style mode for newly created files + Mode *int64 `json:"mode,omitempty"` + // Path using SessionFs conventions + Path string `json:"path"` + // Target session identifier + SessionID string `json:"sessionId"` +} + // File path, content to write, and optional mode for the client-provided session filesystem. // Experimental: SessionFSWriteFileRequest is part of an experimental API and may change or // be removed. @@ -13340,11 +15758,6 @@ type SessionInstalledPluginSourceURL struct { URL string `json:"url"` } -// Experimental: SessionInstructionsReloadResult is part of an experimental API and may -// change or be removed. -type SessionInstructionsReloadResult struct { -} - // Baseline data provenance for a prediction. // Experimental: SessionLimitPredictionBaselineData is part of an experimental API and may // change or be removed. @@ -13568,6 +15981,11 @@ type SessionManagedPermissions struct { // restrict something, so a mode this runtime cannot interpret fails closed to the most // restrictive one it knows. Omit the key entirely to impose no restriction. DisableBypassPermissionsMode *string `json:"disableBypassPermissionsMode,omitempty"` + // Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or + // `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected + // because every network request must be enforceable at host-level egress. Multiple managed + // sources intersect their lists; an empty list denies all hosts. + LimitTo []string `json:"limitTo,omitzero"` } // Managed settings an SDK host may inject at session startup. Only permissions are accepted @@ -13604,6 +16022,58 @@ type SessionMCPEnableResult struct { type SessionMCPOauthAuthenticationStateChangedResult struct { } +// Targets only the original prepared/applying owned login on this exact session requester. +// Experimental: SessionMCPOauthCancelLoginRequest is part of an experimental API and may +// change or be removed. +type SessionMCPOauthCancelLoginRequest struct { + // The same authoritative installation identity supplied during preparation. + ExpectedInstallationID string `json:"expectedInstallationId"` + // Runtime-issued login handle known before the effectful login request begins. + LoginID string `json:"loginId"` +} + +// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. +// Experimental: SessionMCPOauthCancelLoginResult is part of an experimental API and may +// change or be removed. +type SessionMCPOauthCancelLoginResult struct { + // True after cancellation settles, false when the original login already connected + // successfully. + Cancelled bool `json:"cancelled"` +} + +// Experimental: SessionMCPOauthCompleteResult is part of an experimental API and may change +// or be removed. +type SessionMCPOauthCompleteResult struct { +} + +// Effect-free preparation bound to the existing local session, requester and installation, +// with frozen options. +// Experimental: SessionMCPOauthPrepareLoginRequest is part of an experimental API and may +// change or be removed. +type SessionMCPOauthPrepareLoginRequest struct { + // Text shown on the loopback callback page after successful authorisation. + CallbackSuccessMessage *string `json:"callbackSuccessMessage,omitempty"` + // Display name used by the incumbent OAuth client-registration flow. + ClientName *string `json:"clientName,omitempty"` + // Exact installation identity from owned inventory, never a server-name alias. + ExpectedInstallationID string `json:"expectedInstallationId"` + // Request a new authorisation rather than accepting a usable cached grant. + ForceReauth *bool `json:"forceReauth,omitempty"` + // Name recorded by the authoritative owned installation receipt. + ServerName string `json:"serverName"` +} + +// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth +// work. +// Experimental: SessionMCPOauthPrepareLoginResult is part of an experimental API and may +// change or be removed. +type SessionMCPOauthPrepareLoginResult struct { + // Original expiry, not extended by consumption, retries or cancellation. + ExpiresAt time.Time `json:"expiresAt"` + // Retain with the original requester and use for one login or cancellation. + LoginID string `json:"loginId"` +} + // Experimental: SessionMCPRegisterExternalClientResult is part of an experimental API and // may change or be removed. type SessionMCPRegisterExternalClientResult struct { @@ -13619,6 +16089,20 @@ type SessionMCPReloadResult struct { type SessionMCPRestartServerResult struct { } +// Records which IDE the host is connected to, or clears it. +// Experimental: SessionMCPSetConnectedIdeInfoParams is part of an experimental API and may +// change or be removed. +type SessionMCPSetConnectedIdeInfoParams struct { + // The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports + // that it is disconnected. + Ide *SessionConnectedIdeInfo `json:"ide,omitempty"` +} + +// Experimental: SessionMCPSetConnectedIdeInfoResult is part of an experimental API and may +// change or be removed. +type SessionMCPSetConnectedIdeInfoResult struct { +} + // Experimental: SessionMCPStartServerResult is part of an experimental API and may change // or be removed. type SessionMCPStartServerResult struct { @@ -13695,6 +16179,12 @@ type SessionModelList struct { // 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. ModelPriceCategories []SessionModelPriceCategory `json:"modelPriceCategories,omitzero"` + // 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. + Providers []ModelProviderDescriptor `json:"providers,omitzero"` // Per-quota snapshots returned alongside the model list, keyed by quota type. QuotaSnapshots map[string]any `json:"quotaSnapshots,omitzero"` } @@ -13920,10 +16410,12 @@ type SessionOpenOptions struct { RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Resolved sandbox configuration. SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` - // Origin of the sandbox choice. The runtime uses this only for internal telemetry - // provenance; managed policy is derived independently. - // Internal: SandboxConfigSource is part of the SDK's internal API surface and is not - // intended for external use. + // 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: SandboxConfigSource is part of an experimental API and may change or be + // removed. SandboxConfigSource *SandboxConfigSource `json:"sandboxConfigSource,omitempty"` // Capabilities enabled for this session. SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` @@ -14439,6 +16931,35 @@ type SessionsCloseResult struct { type SessionsConfigureSessionExtensionsResult struct { } +// Identity, state location and starting context for a workspace record. +// Experimental: SessionsCreateWorkspaceRequest is part of an experimental API and may +// change or be removed. +type SessionsCreateWorkspaceRequest struct { + // Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, + // `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, + // `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + Context *SessionWorkingDirectoryContextWithClient `json:"context,omitempty"` + // `windows` (any letter case) selects Windows path rules. Any other value selects POSIX + // path rules. + Convention string `json:"convention"` + // User-supplied display name for the workspace + Name *string `json:"name,omitempty"` + // Session ID the workspace record belongs to + SessionID string `json:"sessionId"` + // Directory the session's state is written under when no session filesystem provider is + // configured. Ignored when a provider is configured; the provider's session state path is + // used instead. + SessionStatePath string `json:"sessionStatePath"` +} + +// The workspace record that was written. +// Experimental: SessionsCreateWorkspaceResult is part of an experimental API and may change +// or be removed. +type SessionsCreateWorkspaceResult struct { + // The created workspace record, as JSON + WorkspaceJSON string `json:"workspaceJson"` +} + // Session ID to delete from disk. // Experimental: SessionsDeleteRequest is part of an experimental API and may change or be // removed. @@ -14875,6 +17396,25 @@ type SessionsLoadDeferredRepoHooksRequest struct { SessionID string `json:"sessionId"` } +// Where the session's state lives, as a root directory and the session ID under it. +// Experimental: SessionsLoadWorkspaceRequest is part of an experimental API and may change +// or be removed. +type SessionsLoadWorkspaceRequest struct { + // Session ID naming the state directory under the sessions home. Rejected when it is + // absolute or contains a parent component, so it cannot escape the sessions home. + SessionID string `json:"sessionId"` + // Root directory every session's state directory sits under + SessionsHome string `json:"sessionsHome"` +} + +// The workspace record on disk, omitted when the session has none. +// Experimental: SessionsLoadWorkspaceResult is part of an experimental API and may change +// or be removed. +type SessionsLoadWorkspaceResult struct { + // The workspace record, as JSON. Omitted when the record does not exist. + WorkspaceJSON *string `json:"workspaceJson,omitempty"` +} + // `sessions.open` handoff progress update with step, status, and optional message. // Experimental: SessionsOpenProgress is part of an experimental API and may change or be // removed. @@ -15030,6 +17570,30 @@ type SessionsTransferRemoteControlRequest struct { ToSessionID string `json:"toSessionId"` } +// Where the session's state lives, plus workspace-schema fields to merge into its workspace +// record. Stored keys outside the schema are not preserved, and a stored `fork_count` is +// never replaced. +// Experimental: SessionsUpdateWorkspaceFieldsRequest is part of an experimental API and may +// change or be removed. +type SessionsUpdateWorkspaceFieldsRequest struct { + // Workspace-schema fields to merge into the record, as a JSON object. Fields the object + // omits keep their stored values, except stored keys outside the schema are not preserved + // and a stored `fork_count` is never replaced. + FieldsJSON string `json:"fieldsJson"` + // Session ID naming the state directory under the sessions home. Rejected when it is + // absolute or contains a parent component, so it cannot escape the sessions home. + SessionID string `json:"sessionId"` + // Root directory every session's state directory sits under + SessionsHome string `json:"sessionsHome"` +} + +// The merge completed. The record carries the supplied workspace-schema fields, but a +// stored `fork_count` stays. +// Experimental: SessionsUpdateWorkspaceFieldsResult is part of an experimental API and may +// change or be removed. +type SessionsUpdateWorkspaceFieldsResult struct { +} + // Experimental: SessionSuspendResult is part of an experimental API and may change or be // removed. type SessionSuspendResult struct { @@ -15172,10 +17736,12 @@ type SessionUpdateOptionsParams struct { RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Resolved sandbox configuration. SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` - // Origin of the sandbox choice. The runtime uses this only for internal telemetry - // provenance; managed policy is derived independently. - // Internal: SandboxConfigSource is part of the SDK's internal API surface and is not - // intended for external use. + // 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: SandboxConfigSource is part of an experimental API and may change or be + // removed. SandboxConfigSource *SandboxConfigSource `json:"sandboxConfigSource,omitempty"` // Replaces the session's capability set with the given list. Use to enable or disable // capabilities mid-session (e.g., remove `memory` for reproducible scripted runs). Omit the @@ -15252,6 +17818,30 @@ type SessionWorkingDirectoryContext struct { RepositoryHost *string `json:"repositoryHost,omitempty"` } +// A working-directory context together with the client that produced it. +// Experimental: SessionWorkingDirectoryContextWithClient is part of an experimental API and +// may change or be removed. +type SessionWorkingDirectoryContextWithClient struct { + // Merge-base commit SHA + BaseCommit *string `json:"baseCommit,omitempty"` + // Current git branch name + Branch *string `json:"branch,omitempty"` + // Name of the client that created the session + ClientName *string `json:"clientName,omitempty"` + // Current working directory path + Cwd string `json:"cwd"` + // Root directory of the git repository + GitRoot *string `json:"gitRoot,omitempty"` + // Head commit of the current git branch + HeadCommit *string `json:"headCommit,omitempty"` + // Hosting platform type of the repository + HostType *string `json:"hostType,omitempty"` + // Repository identifier derived from the git remote URL + Repository *string `json:"repository,omitempty"` + // Raw host string from the git remote URL + RepositoryHost *string `json:"repositoryHost,omitempty"` +} + // Experimental: SessionWorkspacesCreateDirectoryResult is part of an experimental API and // may change or be removed. type SessionWorkspacesCreateDirectoryResult struct { @@ -15290,6 +17880,10 @@ func (RawSettableAuthInfoData) settableAuthInfo() {} func (r RawSettableAuthInfoData) settableAuthInfoType() SettableAuthInfoType { return r.Discriminator } +func (AccountAuthInfo) settableAuthInfo() {} +func (AccountAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeAccount +} func (APIKeyAuthInfo) settableAuthInfo() {} func (APIKeyAuthInfo) settableAuthInfoType() SettableAuthInfoType { return SettableAuthInfoTypeAPIKey @@ -15391,7 +17985,8 @@ type ShellCredentials struct { Git *bool `json:"git,omitempty"` } -// Shell command to run, with optional working directory and timeout in milliseconds. +// Shell command to run, with optional working directory and timeout in milliseconds. Spawn +// failures return an RPC error. // Experimental: ShellExecRequest is part of an experimental API and may change or be // removed. type ShellExecRequest struct { @@ -15403,11 +17998,11 @@ type ShellExecRequest struct { Timeout *int64 `json:"timeout,omitempty"` } -// Identifier of the spawned process, used to correlate streamed output and exit -// notifications. +// Identifier of the spawned shell process, usable with shell.kill while the process is +// running. // Experimental: ShellExecResult is part of an experimental API and may change or be removed. type ShellExecResult struct { - // Unique identifier for tracking streamed output + // Identifier usable with shell.kill while the process is running ProcessID string `json:"processId"` } @@ -15520,6 +18115,30 @@ type Skill struct { UserInvocable bool `json:"userInvocable"` } +// Applies exactly one retained verified Skill installation plan. +// Experimental: SkillApplyInstallRequest is part of an experimental API and may change or +// be removed. +type SkillApplyInstallRequest struct { + // Required authenticated bound catalogue and Skill installation capabilities. + Contract CatalogClientContract `json:"contract"` + // Opaque original plan, consumed once. + PlanHandle string `json:"planHandle"` + // Same existing selected session as planning. + PolicySessionID string `json:"policySessionId"` +} + +// One-use application of the exact retained Skill removal plan. +// Experimental: SkillApplyUninstallRequest is part of an experimental API and may change or +// be removed. +type SkillApplyUninstallRequest struct { + // Required authenticated bound installation contract. + Contract CatalogClientContract `json:"contract"` + // Opaque original removal plan, consumed once. + PlanHandle string `json:"planHandle"` + // Same existing selected session as removal preparation. + PolicySessionID string `json:"policySessionId"` +} + // Canonical directory where skills can be discovered or created, with scope, preference, // and optional project path. // Experimental: SkillDiscoveryPath is part of an experimental API and may change or be @@ -15545,6 +18164,581 @@ type SkillDiscoveryPathList struct { Paths []SkillDiscoveryPath `json:"paths"` } +// One reviewed Skill file. +// Experimental: SkillInstallationFileReview is part of an experimental API and may change +// or be removed. +type SkillInstallationFileReview struct { + // SHA-256 digest of the exact file bytes. + Digest string `json:"digest"` + // Whether the file is installed with executable permissions. + Executable bool `json:"executable"` + // Declared media type for the file. + MediaType string `json:"mediaType"` + // Relative file path within the Skill root. + Path string `json:"path"` + // Exact reviewed file size in bytes. + SizeBytes int64 `json:"sizeBytes"` +} + +// A user-facing personal Skill installation location without absolute host paths. +// Experimental: SkillInstallationLocation is part of an experimental API and may change or +// be removed. +type SkillInstallationLocation struct { + // Diagnostics-only absolute host path. Hosts must not display it by default. + DiagnosticsAbsolutePath *string `json:"diagnosticsAbsolutePath,omitempty"` + // Safe display label, for example ~/.copilot/skills/run-checks. + DisplayLabel string `json:"displayLabel"` + // Path relative to the Copilot home. + RelativePath string `json:"relativePath"` + // Installation scope. Agent Finder Skills are installed in the user's personal Copilot home. + Scope SkillInstallationScope `json:"scope"` +} + +// Management outcome for verified Skill inventory, planning and removal. +// Experimental: SkillInstallationManagementOutcome is part of an experimental API and may +// change or be removed. +type SkillInstallationManagementOutcome interface { + skillInstallationManagementOutcome() + Kind() SkillInstallationManagementOutcomeKind +} + +type RawSkillInstallationManagementOutcomeData struct { + Discriminator SkillInstallationManagementOutcomeKind + Raw json.RawMessage +} + +func (RawSkillInstallationManagementOutcomeData) skillInstallationManagementOutcome() {} +func (r RawSkillInstallationManagementOutcomeData) Kind() SkillInstallationManagementOutcomeKind { + return r.Discriminator +} + +// Enablement changed and the selected session was reconciled. +type SkillInstallationManagementOutcomeEnabledChanged struct { + // Safe reload or reconciliation diagnostics. + Diagnostics []string `json:"diagnostics"` + // Updated installation summary. + Installation SkillInstallationSummary `json:"installation"` +} + +func (SkillInstallationManagementOutcomeEnabledChanged) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeEnabledChanged) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindEnabledChanged +} + +// A verified Skill installation plan was prepared. +type SkillInstallationManagementOutcomeInstallPlanned struct { + // Prepared install plan. + Plan SkillInstallPlan `json:"plan"` +} + +func (SkillInstallationManagementOutcomeInstallPlanned) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeInstallPlanned) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindInstallPlanned +} + +// Owned Skill installations were listed. +type SkillInstallationManagementOutcomeListed struct { + // Owned Skill installation summaries. + Installations []SkillInstallationSummary `json:"installations"` +} + +func (SkillInstallationManagementOutcomeListed) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeListed) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindListed +} + +// Original-connection operation snapshot. +type SkillInstallationManagementOutcomeOperation struct { + // Operation status. + Operation SkillInstallationOperationStatus `json:"operation"` +} + +func (SkillInstallationManagementOutcomeOperation) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeOperation) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindOperation +} + +// Recovery completed and inventory was inspected. +type SkillInstallationManagementOutcomeRecovered struct { + // Owned Skill installation summaries after recovery. + Installations []SkillInstallationSummary `json:"installations"` +} + +func (SkillInstallationManagementOutcomeRecovered) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeRecovered) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindRecovered +} + +// Already-confirmed durable work must be reconciled before new mutations or inventory. +type SkillInstallationManagementOutcomeRecoveryRequired struct { +} + +func (SkillInstallationManagementOutcomeRecoveryRequired) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeRecoveryRequired) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindRecoveryRequired +} + +// The management request was refused. +type SkillInstallationManagementOutcomeRefused struct { + // Bounded refusal reason. + Reason SkillInstallationFailureReason `json:"reason"` +} + +func (SkillInstallationManagementOutcomeRefused) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeRefused) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindRefused +} + +// Interrupted work was safely compensated and the pending marker was cleared. +type SkillInstallationManagementOutcomeRolledBack struct { + // Original operation identity. + OperationID string `json:"operation_id"` + // Cause of the compensation. + Reason SkillInstallationFailureReason `json:"reason"` +} + +func (SkillInstallationManagementOutcomeRolledBack) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeRolledBack) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindRolledBack +} + +// An owned Skill uninstall plan was prepared. +type SkillInstallationManagementOutcomeUninstallPlanned struct { + // Prepared uninstall plan. + Plan SkillUninstallPlan `json:"plan"` +} + +func (SkillInstallationManagementOutcomeUninstallPlanned) skillInstallationManagementOutcome() {} +func (SkillInstallationManagementOutcomeUninstallPlanned) Kind() SkillInstallationManagementOutcomeKind { + return SkillInstallationManagementOutcomeKindUninstallPlanned +} + +// Skill installation management result with the honoured contract, or a typed refusal. +// Experimental: SkillInstallationManagementResult is part of an experimental API and may +// change or be removed. +type SkillInstallationManagementResult interface { + skillInstallationManagementResult() + skillInstallationManagementResultKind() SkillInstallationManagementResultKind +} + +type RawSkillInstallationManagementResultData struct { + Discriminator SkillInstallationManagementResultKind + Raw json.RawMessage +} + +func (RawSkillInstallationManagementResultData) skillInstallationManagementResult() {} +func (r RawSkillInstallationManagementResultData) skillInstallationManagementResultKind() SkillInstallationManagementResultKind { + return r.Discriminator +} +func (CatalogInvalidRequestError) skillInstallationManagementResult() {} +func (CatalogInvalidRequestError) skillInstallationManagementResultKind() SkillInstallationManagementResultKind { + return SkillInstallationManagementResultKindInvalidRequest +} +func (CatalogNegotiationRefusedError) skillInstallationManagementResult() {} +func (CatalogNegotiationRefusedError) skillInstallationManagementResultKind() SkillInstallationManagementResultKind { + return SkillInstallationManagementResultKindNegotiationRefused +} + +type SkillInstallationManagementResultOutcome struct { + // Capabilities honoured for this request. + Negotiated CatalogNegotiatedContract `json:"negotiated"` + // Observed management outcome. + Outcome SkillInstallationManagementOutcome `json:"outcome"` +} + +func (SkillInstallationManagementResultOutcome) skillInstallationManagementResult() {} +func (SkillInstallationManagementResultOutcome) skillInstallationManagementResultKind() SkillInstallationManagementResultKind { + return SkillInstallationManagementResultKindOutcome +} + +// Existing-operation control. A new session selector is deliberately not accepted. +// Experimental: SkillInstallationOperationRequest is part of an experimental API and may +// change or be removed. +type SkillInstallationOperationRequest struct { + // Required authenticated bound Skill installation capability. + Contract CatalogClientContract `json:"contract"` + // Exact runtime-issued operation ID on the original connection. + OperationID string `json:"operationId"` +} + +// Status snapshot from the original connection, independent of new-work account +// availability. +// Experimental: SkillInstallationOperationStatus is part of an experimental API and may +// change or be removed. +type SkillInstallationOperationStatus interface { + skillInstallationOperationStatus() + Phase() SkillInstallationOperationStatusPhase +} + +type RawSkillInstallationOperationStatusData struct { + Discriminator SkillInstallationOperationStatusPhase + Raw json.RawMessage +} + +func (RawSkillInstallationOperationStatusData) skillInstallationOperationStatus() {} +func (r RawSkillInstallationOperationStatusData) Phase() SkillInstallationOperationStatusPhase { + return r.Discriminator +} + +// Original operation progress discriminator. +type SkillInstallationOperationStatusApplying struct { + // Whether cancellation has been requested; already-started effects require recovery. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOperationStatusApplying) skillInstallationOperationStatus() {} +func (SkillInstallationOperationStatusApplying) Phase() SkillInstallationOperationStatusPhase { + return SkillInstallationOperationStatusPhaseApplying +} + +// Original operation progress discriminator. +type SkillInstallationOperationStatusAwaitingConfirmation struct { + // Whether cancellation has been requested. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOperationStatusAwaitingConfirmation) skillInstallationOperationStatus() {} +func (SkillInstallationOperationStatusAwaitingConfirmation) Phase() SkillInstallationOperationStatusPhase { + return SkillInstallationOperationStatusPhaseAwaitingConfirmation +} + +// Original operation progress discriminator. +type SkillInstallationOperationStatusCompleted struct { + // Whether cancellation was requested before the terminal result. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` + // Immutable terminal receipt. + Outcome SkillInstallationOutcome `json:"outcome"` +} + +func (SkillInstallationOperationStatusCompleted) skillInstallationOperationStatus() {} +func (SkillInstallationOperationStatusCompleted) Phase() SkillInstallationOperationStatusPhase { + return SkillInstallationOperationStatusPhaseCompleted +} + +// Original operation progress discriminator. +type SkillInstallationOperationStatusPrepared struct { + // Whether cancellation has been requested. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOperationStatusPrepared) skillInstallationOperationStatus() {} +func (SkillInstallationOperationStatusPrepared) Phase() SkillInstallationOperationStatusPhase { + return SkillInstallationOperationStatusPhasePrepared +} + +// Original operation progress discriminator. +type SkillInstallationOperationStatusPreparing struct { + // Whether cancellation has been requested. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOperationStatusPreparing) skillInstallationOperationStatus() {} +func (SkillInstallationOperationStatusPreparing) Phase() SkillInstallationOperationStatusPhase { + return SkillInstallationOperationStatusPhasePreparing +} + +// Original operation progress discriminator. +type SkillInstallationOperationStatusRevalidating struct { + // Whether cancellation has been requested. + CancellationRequested bool `json:"cancellationRequested"` + // Original runtime-issued operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOperationStatusRevalidating) skillInstallationOperationStatus() {} +func (SkillInstallationOperationStatusRevalidating) Phase() SkillInstallationOperationStatusPhase { + return SkillInstallationOperationStatusPhaseRevalidating +} + +// Terminal verified Skill mutation result. +// Experimental: SkillInstallationOutcome is part of an experimental API and may change or +// be removed. +type SkillInstallationOutcome interface { + skillInstallationOutcome() + Kind() SkillInstallationOutcomeKind +} + +type RawSkillInstallationOutcomeData struct { + Discriminator SkillInstallationOutcomeKind + Raw json.RawMessage +} + +func (RawSkillInstallationOutcomeData) skillInstallationOutcome() {} +func (r RawSkillInstallationOutcomeData) Kind() SkillInstallationOutcomeKind { + return r.Discriminator +} + +// The operation was cancelled before a terminal mutation. +type SkillInstallationOutcomeCancelled struct { + // Original operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOutcomeCancelled) skillInstallationOutcome() {} +func (SkillInstallationOutcomeCancelled) Kind() SkillInstallationOutcomeKind { + return SkillInstallationOutcomeKindCancelled +} + +// The user declined the confirmation request. +type SkillInstallationOutcomeDeclined struct { + // Original operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOutcomeDeclined) skillInstallationOutcome() {} +func (SkillInstallationOutcomeDeclined) Kind() SkillInstallationOutcomeKind { + return SkillInstallationOutcomeKindDeclined +} + +// The verified Skill was installed disabled. +type SkillInstallationOutcomeInstalled struct { + // Durable installed Skill summary. + Installation SkillInstallationSummary `json:"installation"` +} + +func (SkillInstallationOutcomeInstalled) skillInstallationOutcome() {} +func (SkillInstallationOutcomeInstalled) Kind() SkillInstallationOutcomeKind { + return SkillInstallationOutcomeKindInstalled +} + +// A write may have completed. Recover and inspect durable state before retrying. +type SkillInstallationOutcomeRecoveryRequired struct { + // Operation whose durable result must be recovered and inspected. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOutcomeRecoveryRequired) skillInstallationOutcome() {} +func (SkillInstallationOutcomeRecoveryRequired) Kind() SkillInstallationOutcomeKind { + return SkillInstallationOutcomeKindRecoveryRequired +} + +// The operation was refused without applying changes. +type SkillInstallationOutcomeRefused struct { + // Present once an operation has been allocated. + OperationID *string `json:"operationId,omitempty"` + // Bounded refusal reason. + Reason SkillInstallationFailureReason `json:"reason"` +} + +func (SkillInstallationOutcomeRefused) skillInstallationOutcome() {} +func (SkillInstallationOutcomeRefused) Kind() SkillInstallationOutcomeKind { + return SkillInstallationOutcomeKindRefused +} + +// The durable transaction was aborted or fully compensated. +type SkillInstallationOutcomeRolledBack struct { + // Original operation identity. + OperationID string `json:"operationId"` + // Cause of the fully aborted or compensated operation. + Reason SkillInstallationFailureReason `json:"reason"` +} + +func (SkillInstallationOutcomeRolledBack) skillInstallationOutcome() {} +func (SkillInstallationOutcomeRolledBack) Kind() SkillInstallationOutcomeKind { + return SkillInstallationOutcomeKindRolledBack +} + +// The owned Skill was removed. +type SkillInstallationOutcomeUninstalled struct { + // Removed installation identity. + InstallationID string `json:"installationId"` + // Original removal operation identity. + OperationID string `json:"operationId"` +} + +func (SkillInstallationOutcomeUninstalled) skillInstallationOutcome() {} +func (SkillInstallationOutcomeUninstalled) Kind() SkillInstallationOutcomeKind { + return SkillInstallationOutcomeKindUninstalled +} + +// Skill installation result with the honoured contract, or a typed request/negotiation +// refusal. +// Experimental: SkillInstallationResult is part of an experimental API and may change or be +// removed. +type SkillInstallationResult interface { + skillInstallationResult() + skillInstallationResultKind() SkillInstallationResultKind +} + +type RawSkillInstallationResultData struct { + Discriminator SkillInstallationResultKind + Raw json.RawMessage +} + +func (RawSkillInstallationResultData) skillInstallationResult() {} +func (r RawSkillInstallationResultData) skillInstallationResultKind() SkillInstallationResultKind { + return r.Discriminator +} +func (CatalogInvalidRequestError) skillInstallationResult() {} +func (CatalogInvalidRequestError) skillInstallationResultKind() SkillInstallationResultKind { + return SkillInstallationResultKindInvalidRequest +} +func (CatalogNegotiationRefusedError) skillInstallationResult() {} +func (CatalogNegotiationRefusedError) skillInstallationResultKind() SkillInstallationResultKind { + return SkillInstallationResultKindNegotiationRefused +} + +type SkillInstallationResultOutcome struct { + // Capabilities honoured for this request. + Negotiated CatalogNegotiatedContract `json:"negotiated"` + // Terminal operation outcome. + Outcome SkillInstallationOutcome `json:"outcome"` +} + +func (SkillInstallationResultOutcome) skillInstallationResult() {} +func (SkillInstallationResultOutcome) skillInstallationResultKind() SkillInstallationResultKind { + return SkillInstallationResultKindOutcome +} + +// Safe verified Skill review fields. No raw credential, candidate handle or plan handle. +// Experimental: SkillInstallationReview is part of an experimental API and may change or be +// removed. +type SkillInstallationReview interface { + skillInstallationReview() + Action() SkillInstallationReviewAction +} + +type RawSkillInstallationReviewData struct { + Discriminator SkillInstallationReviewAction + Raw json.RawMessage +} + +func (RawSkillInstallationReviewData) skillInstallationReview() {} +func (r RawSkillInstallationReviewData) Action() SkillInstallationReviewAction { + return r.Discriminator +} + +// Review for installing a verified Skill. +type SkillInstallationReviewInstall struct { + // Catalogue identity retained from the bound candidate before consent. + Catalogue InstallationCatalogueIdentity `json:"catalogue"` + // Skill description from SKILL.md when present. + Description *string `json:"description,omitempty"` + // Complete verified SKILL.md content. Planning refuses with review-too-large + // when this exceeds 262144 UTF-8 bytes; it is never truncated. + EntrypointContent string `json:"entrypointContent"` + // Relative path of the verified Skill entrypoint. + EntrypointPath string `json:"entrypointPath"` + // Reviewed files and digests. + Files []SkillInstallationFileReview `json:"files"` + // Installing never grants immediate use; the Skill is written disabled. + InstallsDisabled bool `json:"installsDisabled"` + // Skill invocation name from SKILL.md. + Name string `json:"name"` + // Exact verified source identity. + Source SkillInstallationSource `json:"source"` + // Exact user-scope target location without an absolute host path. + Target SkillInstallationLocation `json:"target"` + // Total reviewed payload size in bytes. + TotalBytes int64 `json:"totalBytes"` +} + +func (SkillInstallationReviewInstall) skillInstallationReview() {} +func (SkillInstallationReviewInstall) Action() SkillInstallationReviewAction { + return SkillInstallationReviewActionInstall +} + +// Review for uninstalling an owned verified Skill. +type SkillInstallationReviewUninstall struct { + // Catalogue identity retained at install time. + Catalogue InstallationCatalogueIdentity `json:"catalogue"` + // Files recorded by the installation receipt. + Files []SkillInstallationFileReview `json:"files"` + // Whether current files differ from the receipt. Apply refuses drift. + FilesModified bool `json:"filesModified"` + // Owned installation being removed. + Installation SkillInstallationSummary `json:"installation"` + // Total receipt-owned payload size in bytes. + TotalBytes int64 `json:"totalBytes"` +} + +func (SkillInstallationReviewUninstall) skillInstallationReview() {} +func (SkillInstallationReviewUninstall) Action() SkillInstallationReviewAction { + return SkillInstallationReviewActionUninstall +} + +// Source identity retained from Agent Finder and the pinned GitHub descriptor. +// Experimental: SkillInstallationSource is part of an experimental API and may change or be +// removed. +type SkillInstallationSource struct { + // Digest of the descriptor's bundle manifest. + BundleDigest string `json:"bundleDigest"` + // Agent Finder materialisation revision identifier. + CatalogRevisionID string `json:"catalogRevisionId"` + // Digest of the canonical materialisation descriptor. + DescriptorDigest string `json:"descriptorDigest"` + // Repository full name, for example owner/name. + Repository string `json:"repository"` + // GitHub repository database identifier. + RepositoryID string `json:"repositoryId"` + // Agent Finder resource identifier. + ResourceID string `json:"resourceId"` + // Pinned Git commit revision. + Revision string `json:"revision"` + // Root path within the pinned repository. + Root string `json:"root"` +} + +// Inventory request under an explicitly selected existing session. +// Experimental: SkillInstallationsRequest is part of an experimental API and may change or +// be removed. +type SkillInstallationsRequest struct { + // Required authenticated bound installation contract. + Contract CatalogClientContract `json:"contract"` + // Existing selected local session on this connection. + PolicySessionID string `json:"policySessionId"` +} + +// Durable verified Skill ownership summary. +// Experimental: SkillInstallationSummary is part of an experimental API and may change or +// be removed. +type SkillInstallationSummary struct { + // Catalogue identity retained at install time. + Catalogue InstallationCatalogueIdentity `json:"catalogue"` + // Persisted enablement requested for this installation. + ConfiguredEnabled bool `json:"configuredEnabled"` + // Exact durable installation receipt identity. + InstallationID string `json:"installationId"` + // ISO 8601 wall-clock installation time. + InstalledAt string `json:"installedAt"` + // Skill invocation name. + Name string `json:"name"` + // Operation that installed this Skill. + OperationID string `json:"operationId"` + // Ownership state observed from files and receipts. + OwnershipState SkillInstallationOwnershipState `json:"ownershipState"` + // Bound-session load observation. + SessionState SkillInstallationSessionState `json:"sessionState"` + // Exact retained verified source identity. + Source SkillInstallationSource `json:"source"` + // User-facing installation location without an absolute host path. + Target SkillInstallationLocation `json:"target"` +} + +// A computed Skill install plan. Nothing has been applied. +// Experimental: SkillInstallPlan is part of an experimental API and may change or be +// removed. +type SkillInstallPlan struct { + // Original wall-clock expiry as an ISO 8601 timestamp. + ExpiresAt string `json:"expiresAt"` + // Original operation identifier returned before confirmation. + OperationID string `json:"operationId"` + // One-use plan handle, bound to the original candidate authority. + PlanHandle string `json:"planHandle"` + // Safe review to present before applying the plan. + Review SkillInstallationReview `json:"review"` +} + // Skills available to the session, with their enabled state. // Experimental: SkillList is part of an experimental API and may change or be removed. type SkillList struct { @@ -15552,8 +18746,32 @@ type SkillList struct { Skills []Skill `json:"skills"` } -// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched -// separately and lazily. +// Side-effect-free planning of one verified Agent Finder Skill candidate. +// Experimental: SkillPlanInstallRequest is part of an experimental API and may change or be +// removed. +type SkillPlanInstallRequest struct { + // Fresh single-use AI skill candidate handle returned by a bound catalogue search. + CandidateHandle string `json:"candidateHandle"` + // Required authenticated bound catalogue and Skill installation capabilities. + Contract CatalogClientContract `json:"contract"` + // Existing local session attached to this connection. + PolicySessionID string `json:"policySessionId"` +} + +// Read-only preparation of one owned Skill removal under fresh selected-session authority. +// Experimental: SkillPlanUninstallRequest is part of an experimental API and may change or +// be removed. +type SkillPlanUninstallRequest struct { + // Required authenticated bound installation contract. + Contract CatalogClientContract `json:"contract"` + // Exact receipt to inspect. + InstallationID string `json:"installationId"` + // Existing selected local session on this connection. + PolicySessionID string `json:"policySessionId"` +} + +// Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is +// fetched separately and lazily. // Experimental: SkillProviderDescriptor is part of an experimental API and may change or be // removed. type SkillProviderDescriptor struct { @@ -15601,15 +18819,18 @@ type SkillProviderReadRequest struct { SessionID string `json:"sessionId"` } -// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related -// files and assets are not supported. +// Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter +// is optional: fields it omits come from the catalog descriptor, fields it declares must +// match the descriptor, and `allowed-tools` is read only from frontmatter. Related files +// and assets are not supported. // Experimental: SkillProviderReadResult is part of an experimental API and may change or be // removed. // Internal: SkillProviderReadResult is an internal SDK API and is not part of the public // surface. type SkillProviderReadResult struct { - // Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. - Markdown string `json:"markdown"` + // SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill + // with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. + Markdown *string `json:"markdown"` } // Skill names to mark as disabled in global configuration, replacing any previous list. @@ -15672,6 +18893,20 @@ type SkillsEnableRequest struct { Name string `json:"name"` } +// Persisted enablement update for one owned Skill installation. +// Experimental: SkillSetEnabledRequest is part of an experimental API and may change or be +// removed. +type SkillSetEnabledRequest struct { + // Required authenticated bound Skill installation capability. + Contract CatalogClientContract `json:"contract"` + // Persisted enablement value. + Enabled bool `json:"enabled"` + // Exact receipt identity to update. + InstallationID string `json:"installationId"` + // Existing selected local session to reconcile after persistence. + PolicySessionID string `json:"policySessionId"` +} + // Optional project paths to enumerate. // Experimental: SkillsGetDiscoveryPathsRequest is part of an experimental API and may // change or be removed. @@ -15723,6 +18958,22 @@ type SkillsLoadDiagnostics struct { Warnings []string `json:"warnings"` } +// A computed Skill uninstall plan. Nothing has been removed. +// Experimental: SkillUninstallPlan is part of an experimental API and may change or be +// removed. +type SkillUninstallPlan struct { + // Original wall-clock expiry as an ISO 8601 timestamp. + ExpiresAt string `json:"expiresAt"` + // Owned Skill installation being removed. + Installation SkillInstallationSummary `json:"installation"` + // Original removal operation identifier returned before confirmation. + OperationID string `json:"operationId"` + // One-use uninstall plan handle. + PlanHandle string `json:"planHandle"` + // Safe review to present before applying removal. + Review SkillInstallationReview `json:"review"` +} + // Slash-command metadata with name, aliases, description, kind, input hint, execution // allowance, and schedulability. // Experimental: SlashCommandInfo is part of an experimental API and may change or be @@ -15879,6 +19130,8 @@ func (SlashCommandSelectSubcommandResult) Kind() SlashCommandInvocationResultKin // Experimental: SlashCommandSetModelResult is part of an experimental API and may change or // be removed. type SlashCommandSetModelResult struct { + // Auto routing profile selected by the command, when the model is Auto. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Model selected by the command. Model string `json:"model"` // Reasoning effort selected for the model. @@ -17498,9 +20751,8 @@ type UserSettingMetadata struct { Value any `json:"value"` } -// Per-key metadata for every known user setting (settings.json overlaid with the legacy -// config.json, config.json wins), including settings left at their default. Excludes -// repository- and enterprise-managed overrides. +// Per-key metadata for every known user setting in settings.json, including settings left +// at their default. Excludes repository- and enterprise-managed overrides. // Experimental: UserSettingsGetResult is part of an experimental API and may change or be // removed. type UserSettingsGetResult struct { @@ -17509,11 +20761,6 @@ type UserSettingsGetResult struct { Settings map[string]UserSettingMetadata `json:"settings"` } -// Experimental: UserSettingsReloadResult is part of an experimental API and may change or -// be removed. -type UserSettingsReloadResult struct { -} - // Partial user settings to write to settings.json. Each top-level key is written // individually, replacing the existing value; a key whose value is null is removed. // Experimental: UserSettingsSetRequest is part of an experimental API and may change or be @@ -17523,14 +20770,9 @@ type UserSettingsSetRequest struct { Settings any `json:"settings"` } -// Outcome of writing user settings. // Experimental: UserSettingsSetResult is part of an experimental API and may change or be // removed. type UserSettingsSetResult struct { - // Top-level keys whose write landed in settings.json but is shadowed by a value still - // present in the legacy config.json (config.json wins on read). The write does not take - // effect until the legacy value is removed. - ShadowedKeys []string `json:"shadowedKeys"` } // The approval to add as a session-scoped rule @@ -17622,19 +20864,6 @@ func (UserToolSessionApprovalExtensionPermissionAccess) Kind() UserToolSessionAp return UserToolSessionApprovalKindExtensionPermissionAccess } -// Session-scoped factory approval, optionally narrowed by approval key. -// Experimental: UserToolSessionApprovalFactory is part of an experimental API and may -// change or be removed. -type UserToolSessionApprovalFactory struct { - // Optional factory operation name or canonical approval key - ApprovalKey *string `json:"approvalKey,omitempty"` -} - -func (UserToolSessionApprovalFactory) userToolSessionApproval() {} -func (UserToolSessionApprovalFactory) Kind() UserToolSessionApprovalKind { - return UserToolSessionApprovalKindFactory -} - // Session-scoped tool-approval rule for an MCP server tool, or all tools on the server when // `toolName` is null. // Experimental: UserToolSessionApprovalMCP is part of an experimental API and may change or @@ -17673,6 +20902,19 @@ func (UserToolSessionApprovalRead) Kind() UserToolSessionApprovalKind { return UserToolSessionApprovalKindRead } +// Session-scoped workflow approval, optionally narrowed by approval key. +// Experimental: UserToolSessionApprovalWorkflow is part of an experimental API and may +// change or be removed. +type UserToolSessionApprovalWorkflow struct { + // Optional workflow operation name or canonical approval key + ApprovalKey *string `json:"approvalKey,omitempty"` +} + +func (UserToolSessionApprovalWorkflow) userToolSessionApproval() {} +func (UserToolSessionApprovalWorkflow) Kind() UserToolSessionApprovalKind { + return UserToolSessionApprovalKindWorkflow +} + // Session-scoped tool-approval rule for filesystem write operations. // Experimental: UserToolSessionApprovalWrite is part of an experimental API and may change // or be removed. @@ -18846,6 +22088,26 @@ const ( AbortReasonUserInitiated AbortReason = "user_initiated" ) +// The provider kind stamped on a signed-in account. +// Experimental: AccountKind is part of an experimental API and may change or be removed. +type AccountKind string + +const ( + // A base Microsoft Entra identity. + AccountKindEntra AccountKind = "entra" + // A GitHub (EMU) account derived from a base Entra identity. + AccountKindEntraEmu AccountKind = "entraEmu" + // An OAuth github.com account. + AccountKindGitHubDotCom AccountKind = "githubDotCom" + // A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra + // identity as an EMU account; its bearer is a Loki-scoped inference token consumed through + // the model-provider path, not the GitHub switcher. + AccountKindLoki AccountKind = "loki" + // A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. + // *.ghe.com. + AccountKindProxima AccountKind = "proxima" +) + // Resolved Anthropic adaptive-thinking capability for a model. // Experimental: AdaptiveThinkingSupport is part of an experimental API and may change or be // removed. @@ -19079,11 +22341,32 @@ const ( AttachmentTypeSelection AttachmentType = "selection" ) +// Kind discriminator for AuthEnumerateQuery. +// Experimental: AuthEnumerateQueryKind is part of an experimental API and may change or be +// removed. +type AuthEnumerateQueryKind string + +const ( + AuthEnumerateQueryKindAccounts AuthEnumerateQueryKind = "accounts" + AuthEnumerateQueryKindProviders AuthEnumerateQueryKind = "providers" +) + +// Kind discriminator for AuthEnumerateValue. +// Experimental: AuthEnumerateValueKind is part of an experimental API and may change or be +// removed. +type AuthEnumerateValueKind string + +const ( + AuthEnumerateValueKindAccounts AuthEnumerateValueKind = "accounts" + AuthEnumerateValueKindProviders AuthEnumerateValueKind = "providers" +) + // Type discriminator for AuthInfo. // Experimental: AuthInfoType is part of an experimental API and may change or be removed. type AuthInfoType string const ( + AuthInfoTypeAccount AuthInfoType = "account" AuthInfoTypeAPIKey AuthInfoType = "api-key" AuthInfoTypeCopilotAPIToken AuthInfoType = "copilot-api-token" AuthInfoTypeEnv AuthInfoType = "env" @@ -19094,6 +22377,69 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) +// Disposition of a login attempt, including pending user decisions. +// Experimental: AuthLoginResultStatus is part of an experimental API and may change or be +// removed. +type AuthLoginResultStatus string + +const ( + // The credential was persisted and the selected account is signed in. + AuthLoginResultStatusCompleted AuthLoginResultStatus = "completed" + // The user declined plaintext persistence. + AuthLoginResultStatusDeclined AuthLoginResultStatus = "declined" + // Credentials are saved; select an account using a returned selectionId as advance input to + // complete sign-in. + AuthLoginResultStatusNeedsAccountSelection AuthLoginResultStatus = "needs-account-selection" + // Persistence needs explicit consent to store the token in plaintext. + AuthLoginResultStatusNeedsPlaintextConsent AuthLoginResultStatus = "needs-plaintext-consent" +) + +// Kind discriminator for AuthLoginStep. +// Experimental: AuthLoginStepKind is part of an experimental API and may change or be +// removed. +type AuthLoginStepKind string + +const ( + AuthLoginStepKindAwaiting AuthLoginStepKind = "awaiting" + AuthLoginStepKindCompleted AuthLoginStepKind = "completed" + AuthLoginStepKindError AuthLoginStepKind = "error" + AuthLoginStepKindInputRequired AuthLoginStepKind = "input-required" + AuthLoginStepKindNeedsInteraction AuthLoginStepKind = "needs-interaction" + AuthLoginStepKindOpenURL AuthLoginStepKind = "open-url" +) + +// Kind discriminator for AuthReadQuery. +// Experimental: AuthReadQueryKind is part of an experimental API and may change or be +// removed. +type AuthReadQueryKind string + +const ( + AuthReadQueryKindActiveAccount AuthReadQueryKind = "activeAccount" + AuthReadQueryKindLastErrors AuthReadQueryKind = "lastErrors" + AuthReadQueryKindStatus AuthReadQueryKind = "status" +) + +// Kind discriminator for AuthReadValue. +// Experimental: AuthReadValueKind is part of an experimental API and may change or be +// removed. +type AuthReadValueKind string + +const ( + AuthReadValueKindActiveAccount AuthReadValueKind = "activeAccount" + AuthReadValueKindLastErrors AuthReadValueKind = "lastErrors" + AuthReadValueKindStatus AuthReadValueKind = "status" +) + +// Kind discriminator for AuthWrite. +// Experimental: AuthWriteKind is part of an experimental API and may change or be removed. +type AuthWriteKind string + +const ( + AuthWriteKindLogout AuthWriteKind = "logout" + AuthWriteKindSetCredentials AuthWriteKind = "setCredentials" + AuthWriteKindSwitchActive AuthWriteKind = "switchActive" +) + // Current normalized autopilot objective lifecycle status. // Experimental: AutopilotObjectiveStatus is part of an experimental API and may change or // be removed. @@ -19199,14 +22545,23 @@ const ( CatalogAiSkillCandidateKindAiSkill CatalogAiSkillCandidateKind = "ai-skill" ) -// Typed non-installable state for an AI skill candidate +// Typed installability state for an AI skill candidate // Experimental: CatalogAiSkillInstallability is part of an experimental API and may change // or be removed. type CatalogAiSkillInstallability string const ( - // AI skills are discovery-only on this surface. + // Skill installation is understood but disabled for the selected session. + CatalogAiSkillInstallabilityFeatureDisabled CatalogAiSkillInstallability = "feature-disabled" + // This AI skill candidate carries verified materialisation metadata and the selected + // session may plan installation. + CatalogAiSkillInstallabilityInstallable CatalogAiSkillInstallability = "installable" + // The candidate lacks verified materialisation metadata required for installation. + CatalogAiSkillInstallabilityMaterialisationUnavailable CatalogAiSkillInstallability = "materialisation-unavailable" + // Compatibility value for discovery-only callers that did not negotiate Skill installation. CatalogAiSkillInstallabilityNotInstallableKind CatalogAiSkillInstallability = "not-installable-kind" + // Policy refuses Skill installation for the selected session or authority. + CatalogAiSkillInstallabilityPolicyForbids CatalogAiSkillInstallability = "policy-forbids" ) // Canonical AI skill media type @@ -19278,11 +22633,27 @@ const ( // Understands explicit numbered navigation and authority-reported pagination metadata with // opaque tokens. Advertised and granted only when requested. CatalogCapabilityCatalogSearchPagination CatalogCapability = "catalog-search-pagination" + // 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. + CatalogCapabilityCatalogSearchSessionBound CatalogCapability = "catalog-search-session-bound" // Understands exact candidate selection through model-safe opaque references and host-only // candidate-handle hand-off. CatalogCapabilityCatalogSelection CatalogCapability = "catalog-selection" // Understands the legacy `application/mcp-server+json` media type. CatalogCapabilityLegacyMCPServerCard CatalogCapability = "legacy-mcp-server-card" + // 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. + CatalogCapabilityMCPConfiguredRemoteInstallation CatalogCapability = "mcp-configured-remote-installation" + // 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. + CatalogCapabilityMCPConfirmedRemoteInstallation CatalogCapability = "mcp-confirmed-remote-installation" // Understands side-effect-free MCP install-plan requests, results, and plan handles; // `planning-unavailable` separately reports that planning is not enabled. CatalogCapabilityMCPInstallPlanning CatalogCapability = "mcp-install-planning" @@ -19291,6 +22662,11 @@ const ( // Understands plans that enumerate every eligible transport rather than a single preferred // one. CatalogCapabilityMultipleTransportChoice CatalogCapability = "multiple-transport-choice" + // 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. + CatalogCapabilitySkillConfirmedInstallation CatalogCapability = "skill-confirmed-installation" // Understands versioned candidate trust snapshots. Protocol-3 callers must require this // capability before the runtime adds the optional snapshot field. CatalogCapabilityTrustSnapshot CatalogCapability = "trust-snapshot" @@ -19366,6 +22742,8 @@ const ( // The pagination token or target was invalid, or the authority rejected the continuation. // Repeat the search without page. CatalogInvalidRequestFieldPage CatalogInvalidRequestField = "page" + // The selected existing attached session was missing, malformed or unavailable. + CatalogInvalidRequestFieldPolicySessionID CatalogInvalidRequestField = "policySessionId" // The search query was empty or longer than permitted. CatalogInvalidRequestFieldQuery CatalogInvalidRequestField = "query" // The requested configuration scope is not one this runtime writes. @@ -19824,6 +23202,20 @@ const ( ConnectorConnectResultKindPending ConnectorConnectResultKind = "pending" ) +// Availability. +// Experimental: ConnectorDiscoveryAvailability is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryAvailability string + +const ( + // Disabled. + ConnectorDiscoveryAvailabilityDisabled ConnectorDiscoveryAvailability = "disabled" + // Enabled. + ConnectorDiscoveryAvailabilityEnabled ConnectorDiscoveryAvailability = "enabled" + // Unavailable. + ConnectorDiscoveryAvailabilityUnavailable ConnectorDiscoveryAvailability = "unavailable" +) + // Live MCP status of one Connector-owned runtime server. // Experimental: ConnectorMCPStatus is part of an experimental API and may change or be // removed. @@ -19880,6 +23272,44 @@ const ( CopilotAPITokenAuthInfoHostHTTPSGitHubCom CopilotAPITokenAuthInfoHost = "https://github.com" ) +// Result of reloading a customization component. +// Experimental: CustomizationReloadStatus is part of an experimental API and may change or +// be removed. +type CustomizationReloadStatus string + +const ( + // The component could not be refreshed; other components may still reload. + CustomizationReloadStatusFailed CustomizationReloadStatus = "failed" + // The component was refreshed successfully. + CustomizationReloadStatusReloaded CustomizationReloadStatus = "reloaded" + // The component was not configured, loaded, or eligible for refresh. + CustomizationReloadStatusSkipped CustomizationReloadStatus = "skipped" +) + +// Component of session customization discovery. +// Experimental: CustomizationReloadSubsystem is part of an experimental API and may change +// or be removed. +type CustomizationReloadSubsystem string + +const ( + // Discovered custom agents. + CustomizationReloadSubsystemAgents CustomizationReloadSubsystem = "agents" + // Configured session extensions. + CustomizationReloadSubsystemExtensions CustomizationReloadSubsystem = "extensions" + // Configured session and plugin hooks. + CustomizationReloadSubsystemHooks CustomizationReloadSubsystem = "hooks" + // Session instructions and their cached dynamic context. + CustomizationReloadSubsystemInstructions CustomizationReloadSubsystem = "instructions" + // Loaded MCP server configuration. + CustomizationReloadSubsystemMCP CustomizationReloadSubsystem = "mcp" + // Discovered plugin configuration. + CustomizationReloadSubsystemPlugins CustomizationReloadSubsystem = "plugins" + // Repository metadata and working-directory context. + CustomizationReloadSubsystemRepositoryContext CustomizationReloadSubsystem = "repositoryContext" + // Discovered skills. + CustomizationReloadSubsystemSkills CustomizationReloadSubsystem = "skills" +) + // Kind discriminator for DebugCollectLogsDestination. type DebugCollectLogsDestinationKind string @@ -20054,6 +23484,23 @@ const ( DiscoveredMCPServerTypeStdio DiscoveredMCPServerType = "stdio" ) +// GitHub Mission Control compute kind. +// Experimental: EnvironmentKind is part of an experimental API and may change or be removed. +type EnvironmentKind string + +const ( + // A GitHub-managed environment backed by GitHub Actions. + EnvironmentKindManagedActions EnvironmentKind = "managed-actions" + // A GitHub-managed cloud coding agent environment. + EnvironmentKindManagedCca EnvironmentKind = "managed-cca" + // A GitHub-managed sandbox environment. + EnvironmentKindManagedSandbox EnvironmentKind = "managed-sandbox" + // A user-managed environment in a GitHub Codespace. + EnvironmentKindUserCodespace EnvironmentKind = "user-codespace" + // A user-managed environment on a local machine. + EnvironmentKindUserLocal EnvironmentKind = "user-local" +) + type EventLogTypesString string const ( @@ -20177,133 +23624,6 @@ const ( ExternalToolTextResultForLlmContentTypeText ExternalToolTextResultForLlmContentType = "text" ) -// Execution-critical factory storage operation. -// Experimental: FactoryDurableOperation is part of an experimental API and may change or be -// removed. -type FactoryDurableOperation string - -const ( - // Persisting active execution time. - FactoryDurableOperationAddElapsed FactoryDurableOperation = "addElapsed" - // Persisting an idempotent model-usage charge. - FactoryDurableOperationChargeCredit FactoryDurableOperation = "chargeCredit" - // Creating the durable run and declared phases. - FactoryDurableOperationCreateRun FactoryDurableOperation = "createRun" - // Persisting the terminal run envelope. - FactoryDurableOperationFinishRun FactoryDurableOperation = "finishRun" - // Reading a journal entry without treating storage failure as a cache miss. - FactoryDurableOperationJournalGet FactoryDurableOperation = "journalGet" - // Persisting a journal entry before reporting success. - FactoryDurableOperationJournalPut FactoryDurableOperation = "journalPut" - // Persisting the transition to running. - FactoryDurableOperationMarkRunStarted FactoryDurableOperation = "markRunStarted" - // Reading the authoritative AI-credit total. - FactoryDurableOperationReconcileCreditTotal FactoryDurableOperation = "reconcileCreditTotal" - // Renewing the durable owner lease that proves this process still owns the run. - FactoryDurableOperationRefreshLease FactoryDurableOperation = "refreshLease" - // Rolling back an uncommitted subagent admission. - FactoryDurableOperationReleaseAgent FactoryDurableOperation = "releaseAgent" - // Persisting subagent admission accounting. - FactoryDurableOperationReserveAgent FactoryDurableOperation = "reserveAgent" -) - -// Kind of factory progress line. -// Experimental: FactoryLogLineKind is part of an experimental API and may change or be -// removed. -type FactoryLogLineKind string - -const ( - // A narrator log line. - FactoryLogLineKindLog FactoryLogLineKind = "log" - // A named factory phase marker. - FactoryLogLineKindPhase FactoryLogLineKind = "phase" -) - -// Action the runtime selected for a durable factory pause checkpoint. -// Experimental: FactoryPauseCheckpointAction is part of an experimental API and may change -// or be removed. -type FactoryPauseCheckpointAction string - -const ( - // The checkpoint was committed by a prior paused attempt, so execution may continue. - FactoryPauseCheckpointActionContinue FactoryPauseCheckpointAction = "continue" - // This attempt claimed the checkpoint and must cooperatively stop. - FactoryPauseCheckpointActionPause FactoryPauseCheckpointAction = "pause" -) - -// Type discriminator for FactoryPauseInfo. -type FactoryPauseInfoType string - -const ( - FactoryPauseInfoTypeCheckpoint FactoryPauseInfoType = "checkpoint" - FactoryPauseInfoTypeUser FactoryPauseInfoType = "user" -) - -// Derived lifecycle state of a factory phase. -// Experimental: FactoryPhaseStatus is part of an experimental API and may change or be -// removed. -type FactoryPhaseStatus string - -const ( - // The phase is currently entered and accumulating active time. - FactoryPhaseStatusActive FactoryPhaseStatus = "active" - // The phase was entered and has since been closed. - FactoryPhaseStatusCompleted FactoryPhaseStatus = "completed" - // The phase has not been entered yet. - FactoryPhaseStatusPending FactoryPhaseStatus = "pending" - // The phase was never entered because a later phase was entered or the run reached a - // terminal state. - FactoryPhaseStatusSkipped FactoryPhaseStatus = "skipped" -) - -// Cumulative resource ceiling that stopped a factory run. -// Experimental: FactoryRunFailureKind is part of an experimental API and may change or be -// removed. -type FactoryRunFailureKind string - -const ( - // The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no - // headroom remained for another subagent. - FactoryRunFailureKindMaxAiCredits FactoryRunFailureKind = "maxAiCredits" - // The run admitted the approved maximum total number of subagents. - FactoryRunFailureKindMaxTotalSubagents FactoryRunFailureKind = "maxTotalSubagents" - // The run reached the approved accumulated active-execution time in seconds. - FactoryRunFailureKindTimeoutSeconds FactoryRunFailureKind = "timeoutSeconds" -) - -// Type discriminator for FactoryRunFailure. -type FactoryRunFailureType string - -const ( - FactoryRunFailureTypeFactoryAccountingIncomplete FactoryRunFailureType = "factory_accounting_incomplete" - FactoryRunFailureTypeFactoryDurableFailure FactoryRunFailureType = "factory_durable_failure" - FactoryRunFailureTypeFactoryLimitReached FactoryRunFailureType = "factory_limit_reached" - FactoryRunFailureTypeFactoryProviderDisconnected FactoryRunFailureType = "factory_provider_disconnected" - FactoryRunFailureTypeFactoryResumeDeclined FactoryRunFailureType = "factory_resume_declined" -) - -// Current or terminal state of a factory run. -// Experimental: FactoryRunStatus is part of an experimental API and may change or be -// removed. -type FactoryRunStatus string - -const ( - // The run was cancelled before completion. - FactoryRunStatusCancelled FactoryRunStatus = "cancelled" - // The run completed successfully. - FactoryRunStatusCompleted FactoryRunStatus = "completed" - // The factory body failed or reached a cumulative resource ceiling. - FactoryRunStatusError FactoryRunStatus = "error" - // The run was interrupted while resource budget remained. - FactoryRunStatusHalted FactoryRunStatus = "halted" - // The current attempt stopped intentionally and the run may be resumed. - FactoryRunStatusPaused FactoryRunStatus = "paused" - // The run was minted and is awaiting approval. - FactoryRunStatusPending FactoryRunStatus = "pending" - // The run is executing. - FactoryRunStatusRunning FactoryRunStatus = "running" -) - // Why the runtime is requesting a GitHub credential. // Experimental: GitHubTokenAcquireReason is part of an experimental API and may change or // be removed. @@ -20493,6 +23813,21 @@ const ( HookTypeUserPromptTransformed HookType = "userPromptTransformed" ) +// Experimental: HostExitReason is part of an experimental API and may change or be removed. +type HostExitReason string + +const ( + // The owner requested disposal. + HostExitReasonDisposed HostExitReason = "disposed" + // The hosting task or its SDK transport exited; this does not mean the runtime process + // exited. + HostExitReasonExited HostExitReason = "exited" + // The owning SDK connection disconnected. + HostExitReasonOwnerDisconnected HostExitReason = "ownerDisconnected" + // The runtime is shutting down. + HostExitReasonRuntimeShutdown HostExitReason = "runtimeShutdown" +) + // Live indexed-search state for this session activation, never inferred from persisted // history. // Experimental: IndexedSearchState is part of an experimental API and may change or be @@ -20512,6 +23847,28 @@ const ( IndexedSearchStateStarting IndexedSearchState = "starting" ) +// Explicit user decisions, never inferred from a permission grant or model response. +// Experimental: InstallationDecision is part of an experimental API and may change or be +// removed. +type InstallationDecision string + +const ( + // The user cancelled the pending decision without granting consent. + InstallationDecisionCancel InstallationDecision = "cancel" + // The user explicitly approved the exact review on this request. + InstallationDecisionConfirm InstallationDecision = "confirm" + // The user declined the reviewed operation. + InstallationDecisionDecline InstallationDecision = "decline" +) + +// Resource discriminator for InstallationReview. +type InstallationReviewResource string + +const ( + InstallationReviewResourceMCP InstallationReviewResource = "mcp" + InstallationReviewResourceSkill InstallationReviewResource = "skill" +) + // Constant value. Always "github". type InstalledPluginSourceGitHubSource string @@ -20619,6 +23976,47 @@ const ( LlmInferenceHTTPRequestStartTransportWebsocket LlmInferenceHTTPRequestStartTransport = "websocket" ) +// A provider a consumer may interactively sign in with. +// Experimental: LoginProviderKind is part of an experimental API and may change or be +// removed. +type LoginProviderKind string + +const ( + // Microsoft Entra sign-in that derives a GitHub (EMU) credential. + LoginProviderKindEntra LoginProviderKind = "entra" + // OAuth github.com sign-in via the browser (web loopback + PKCE). + LoginProviderKindGitHubDotCom LoginProviderKind = "githubDotCom" + // A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. + // *.ghe.com; the host is supplied interactively through the neutral input-required step. + LoginProviderKindProxima LoginProviderKind = "proxima" +) + +// A channel accepted by managedSettings.compose. +// Experimental: ManagedSettingsChannel is part of an experimental API and may change or be +// removed. +type ManagedSettingsChannel string + +const ( + // Device policy, the strongest channel. + ManagedSettingsChannelDevice ManagedSettingsChannel = "device" + // Session-local helper output, the weakest channel. + ManagedSettingsChannelPolicyHelper ManagedSettingsChannel = "policyHelper" + // Account or organization policy. + ManagedSettingsChannelServer ManagedSettingsChannel = "server" +) + +// Severity of a managed-settings validation finding. +// Experimental: ManagedSettingsDiagnosticSeverity is part of an experimental API and may +// change or be removed. +type ManagedSettingsDiagnosticSeverity string + +const ( + // The runtime rejects the document. + ManagedSettingsDiagnosticSeverityError ManagedSettingsDiagnosticSeverity = "error" + // The runtime accepts the document but ignores the flagged content. + ManagedSettingsDiagnosticSeverityWarning ManagedSettingsDiagnosticSeverity = "warning" +) + // Summary of which managed-settings channels contributed to the effective session policy. // Use the per-channel booleans for exact provenance. // Experimental: ManagedSettingsResolvedSource is part of an experimental API and may change @@ -20796,6 +24194,156 @@ const ( MCPHeadersHandlePendingHeadersRefreshRequestKindNone MCPHeadersHandlePendingHeadersRefreshRequestKind = "none" ) +// Bounded refusal categories, without echoing handles, credentials or configuration. +// Experimental: MCPInstallationFailureReason is part of an experimental API and may change +// or be removed. +type MCPInstallationFailureReason string + +const ( + // The original operation was cancelled. + MCPInstallationFailureReasonCancelled MCPInstallationFailureReason = "cancelled" + // Required installation capabilities were omitted. + MCPInstallationFailureReasonCapabilityRequired MCPInstallationFailureReason = "capability-required" + // Configuration changed after the reviewed snapshot. + MCPInstallationFailureReasonConfigurationChanged MCPInstallationFailureReason = "configuration-changed" + // Installed configuration no longer matches ownership evidence. + MCPInstallationFailureReasonConfigurationModified MCPInstallationFailureReason = "configuration-modified" + // The confirmation response is malformed or mismatched. + MCPInstallationFailureReasonConfirmationInvalid MCPInstallationFailureReason = "confirmation-invalid" + // The original host cannot receive human confirmation. + MCPInstallationFailureReasonConfirmationUnavailable MCPInstallationFailureReason = "confirmation-unavailable" + // The handle belongs to a different runtime, session or connection. + MCPInstallationFailureReasonForeignRuntime MCPInstallationFailureReason = "foreign-runtime" + // The selected choice or request is unsupported or malformed. + MCPInstallationFailureReasonInvalidRequest MCPInstallationFailureReason = "invalid-request" + // Required owned admission and lifecycle support is absent. + MCPInstallationFailureReasonLifecycleUnavailable MCPInstallationFailureReason = "lifecycle-unavailable" + // The bounded original-connection operation limit was reached. + MCPInstallationFailureReasonOperationLimit MCPInstallationFailureReason = "operation-limit" + // The original plan deadline elapsed. + MCPInstallationFailureReasonPlanExpired MCPInstallationFailureReason = "plan-expired" + // The one-use plan or prepared operation was already consumed. + MCPInstallationFailureReasonPlanReplayed MCPInstallationFailureReason = "plan-replayed" + // The original authority or policy changed. + MCPInstallationFailureReasonPolicyChanged MCPInstallationFailureReason = "policy-changed" + // Existing authenticated session and host authority is unavailable. + MCPInstallationFailureReasonPolicyContextUnavailable MCPInstallationFailureReason = "policy-context-unavailable" + // Current managed policy refuses the operation. + MCPInstallationFailureReasonPolicyDenied MCPInstallationFailureReason = "policy-denied" + // Authoritative Registry interpretation is unavailable. + MCPInstallationFailureReasonRegistryUnavailable MCPInstallationFailureReason = "registry-unavailable" + // Fresh bound planning is required. + MCPInstallationFailureReasonReplanRequired MCPInstallationFailureReason = "replan-required" + // No matching owned resource or original operation exists. + MCPInstallationFailureReasonResourceNotFound MCPInstallationFailureReason = "resource-not-found" + // The selected secret backend is unavailable. + MCPInstallationFailureReasonSecretStoreUnavailable MCPInstallationFailureReason = "secret-store-unavailable" + // The source differs from the retained commitment. + MCPInstallationFailureReasonSourceChanged MCPInstallationFailureReason = "source-changed" + // Exact original source revalidation is unsupported. + MCPInstallationFailureReasonSourceRevalidationUnavailable MCPInstallationFailureReason = "source-revalidation-unavailable" + // The original source could not be retrieved safely. + MCPInstallationFailureReasonSourceUnavailable MCPInstallationFailureReason = "source-unavailable" + // A storage operation failed; inspect any allocated operation before retrying. + MCPInstallationFailureReasonWriteFailed MCPInstallationFailureReason = "write-failed" +) + +// Kind discriminator for MCPInstallationManagementOutcome. +type MCPInstallationManagementOutcomeKind string + +const ( + MCPInstallationManagementOutcomeKindInstallPrepared MCPInstallationManagementOutcomeKind = "install-prepared" + MCPInstallationManagementOutcomeKindListed MCPInstallationManagementOutcomeKind = "listed" + MCPInstallationManagementOutcomeKindOperation MCPInstallationManagementOutcomeKind = "operation" + MCPInstallationManagementOutcomeKindRecovered MCPInstallationManagementOutcomeKind = "recovered" + MCPInstallationManagementOutcomeKindRecoveryRequired MCPInstallationManagementOutcomeKind = "recovery-required" + MCPInstallationManagementOutcomeKindRefused MCPInstallationManagementOutcomeKind = "refused" + MCPInstallationManagementOutcomeKindUninstallPlanned MCPInstallationManagementOutcomeKind = "uninstall-planned" +) + +// Kind discriminator for MCPInstallationManagementResult. +type MCPInstallationManagementResultKind string + +const ( + MCPInstallationManagementResultKindInvalidRequest MCPInstallationManagementResultKind = "invalid-request" + MCPInstallationManagementResultKindNegotiationRefused MCPInstallationManagementResultKind = "negotiation-refused" + MCPInstallationManagementResultKindOutcome MCPInstallationManagementResultKind = "outcome" +) + +// Phase discriminator for MCPInstallationOperationStatus. +type MCPInstallationOperationStatusPhase string + +const ( + MCPInstallationOperationStatusPhaseApplying MCPInstallationOperationStatusPhase = "applying" + MCPInstallationOperationStatusPhaseAwaitingConfirmation MCPInstallationOperationStatusPhase = "awaiting-confirmation" + MCPInstallationOperationStatusPhaseCompleted MCPInstallationOperationStatusPhase = "completed" + MCPInstallationOperationStatusPhasePrepared MCPInstallationOperationStatusPhase = "prepared" + MCPInstallationOperationStatusPhasePreparing MCPInstallationOperationStatusPhase = "preparing" + MCPInstallationOperationStatusPhaseRevalidating MCPInstallationOperationStatusPhase = "revalidating" +) + +// Kind discriminator for MCPInstallationOutcome. +type MCPInstallationOutcomeKind string + +const ( + MCPInstallationOutcomeKindCancelled MCPInstallationOutcomeKind = "cancelled" + MCPInstallationOutcomeKindDeclined MCPInstallationOutcomeKind = "declined" + MCPInstallationOutcomeKindInstalled MCPInstallationOutcomeKind = "installed" + MCPInstallationOutcomeKindRecoveryRequired MCPInstallationOutcomeKind = "recovery-required" + MCPInstallationOutcomeKindRefused MCPInstallationOutcomeKind = "refused" + MCPInstallationOutcomeKindRolledBack MCPInstallationOutcomeKind = "rolled-back" + MCPInstallationOutcomeKindUninstalled MCPInstallationOutcomeKind = "uninstalled" +) + +// Kind discriminator for MCPInstallationResult. +type MCPInstallationResultKind string + +const ( + MCPInstallationResultKindInvalidRequest MCPInstallationResultKind = "invalid-request" + MCPInstallationResultKindNegotiationRefused MCPInstallationResultKind = "negotiation-refused" + MCPInstallationResultKindOutcome MCPInstallationResultKind = "outcome" +) + +// Action discriminator for MCPInstallationReview. +type MCPInstallationReviewAction string + +const ( + MCPInstallationReviewActionInstall MCPInstallationReviewAction = "install" + MCPInstallationReviewActionUninstall MCPInstallationReviewAction = "uninstall" +) + +// Explicit backend selection is part of the final review; failures never switch backends. +// Experimental: MCPInstallationSecretStorage is part of an experimental API and may change +// or be removed. +type MCPInstallationSecretStorage string + +const ( + // The selected operating-system keychain, without fallback to file storage. + MCPInstallationSecretStorageKeychain MCPInstallationSecretStorage = "keychain" + // The explicitly selected private file backend. + MCPInstallationSecretStoragePrivateFile MCPInstallationSecretStorage = "private-file" +) + +// Configuration ownership and setup observations, distinct from tool permissions. +// Experimental: MCPInstallationState is part of an experimental API and may change or be +// removed. +type MCPInstallationState string + +const ( + // The selected server could not be activated. + MCPInstallationStateActivationFailed MCPInstallationState = "activation-failed" + // The selected authorised session reports an active installation. + MCPInstallationStateActive MCPInstallationState = "active" + // The selected server requires explicit sign-in. + MCPInstallationStateAuthenticationRequired MCPInstallationState = "authentication-required" + // Owned configuration no longer matches its receipt. + MCPInstallationStateConfigurationModified MCPInstallationState = "configuration-modified" + // Owned configuration exists; inventory alone does not grant activation. + MCPInstallationStateNeedsSetup MCPInstallationState = "needs-setup" + // Confirmed durable work or unsafe evidence requires recovery. + MCPInstallationStateRecoveryRequired MCPInstallationState = "recovery-required" +) + // OAuth grant type override for this login. // Experimental: MCPOauthLoginGrantType is part of an experimental API and may change or be // removed. @@ -20844,6 +24392,18 @@ const ( MCPOauthProbeResultStatusNoAuthRequired MCPOauthProbeResultStatus = "no-auth-required" ) +// Outcome of starting the original prepared owned login. +// Experimental: MCPOwnedOauthLoginStatus is part of an experimental API and may change or +// be removed. +type MCPOwnedOauthLoginStatus string + +const ( + // The original requester may open the returned authorisation URL. + MCPOwnedOauthLoginStatusAwaitingBrowser MCPOwnedOauthLoginStatus = "awaiting-browser" + // Cached credentials were accepted and the original server finished reconnecting. + MCPOwnedOauthLoginStatusConnected MCPOwnedOauthLoginStatus = "connected" +) + // Whether a planned configuration change would create or modify an entry // Experimental: MCPPlanConfigurationOperation is part of an experimental API and may change // or be removed. @@ -21068,6 +24628,17 @@ const ( MCPPlanValueCategoryURLVariable MCPPlanValueCategory = "url-variable" ) +// The sender role of an MCP prompt message. +// Experimental: MCPPromptRole is part of an experimental API and may change or be removed. +type MCPPromptRole string + +const ( + // A message from the assistant. + MCPPromptRoleAssistant MCPPromptRole = "assistant" + // A message from the user. + MCPPromptRoleUser MCPPromptRole = "user" +) + // Outcome of the sampling inference. 'success' produced a response; 'failure' encountered // an error (including agent-side rejection by content filter or criteria); 'cancelled' the // caller cancelled this execution via cancelSamplingExecution. @@ -21182,11 +24753,13 @@ const ( MCPServerConfigStdioTypeStdio MCPServerConfigStdioType = "stdio" ) -// Configuration source: user, workspace, plugin, builtin, or managed +// Configuration source: user, workspace, plugin, builtin, managed, or account // Experimental: MCPServerSource is part of an experimental API and may change or be removed. type MCPServerSource string const ( + // Server contributed by a signed-in account; enablement and organization policy still apply. + MCPServerSourceAccount MCPServerSource = "account" // Server bundled with the runtime. MCPServerSourceBuiltin MCPServerSource = "builtin" // Server supplied by a trusted host-managed catalog. @@ -21289,6 +24862,8 @@ const ( // The runtime selected the model automatically, such as rate-limit recovery or refusal // fallback. ModelChangeSourceAutomatic ModelChangeSource = "automatic" + // The user accepted a CAPI-issued Auto tier recommendation. + ModelChangeSourceAutoTierRecommendation ModelChangeSource = "auto_tier_recommendation" // The user selected the promoted model from the changeboarding card or its keyboard // shortcut. ModelChangeSourceChangeboardingShortcut ModelChangeSource = "changeboarding_shortcut" @@ -21357,6 +24932,103 @@ const ( ModelPolicyStateUnconfigured ModelPolicyState = "unconfigured" ) +// When the runtime may run an adapter without an explicit user action. +// Experimental: ModelProviderAutomaticDiscoveryMode is part of an experimental API and may +// change or be removed. +type ModelProviderAutomaticDiscoveryMode string + +const ( + // The adapter declares that automatic discovery is safe when the other policy fields are + // satisfied. + ModelProviderAutomaticDiscoveryModeAutomatic ModelProviderAutomaticDiscoveryMode = "automatic" + // The adapter may refresh instances the user already configured, but must not scan for new + // instances automatically. + ModelProviderAutomaticDiscoveryModeConfiguredOnly ModelProviderAutomaticDiscoveryMode = "configuredOnly" + // The adapter must run only after an explicit user action. + ModelProviderAutomaticDiscoveryModeExplicit ModelProviderAutomaticDiscoveryMode = "explicit" +) + +// Whether a planned configuration entry is new or already present in the session registry. +// Experimental: ModelProviderConfigurationDisposition is part of an experimental API and +// may change or be removed. +type ModelProviderConfigurationDisposition string + +const ( + // An equivalent entry is already registered; the caller should reuse it rather than adding + // a duplicate. + ModelProviderConfigurationDispositionAlreadyConfigured ModelProviderConfigurationDisposition = "alreadyConfigured" + // No matching entry is registered; the caller should add the entry. + ModelProviderConfigurationDispositionCreate ModelProviderConfigurationDisposition = "create" +) + +// Network reach an adapter may use during discovery. +// Experimental: ModelProviderDiscoveryNetworkScope is part of an experimental API and may +// change or be removed. +type ModelProviderDiscoveryNetworkScope string + +const ( + // Discovery contacts only endpoints the user already configured. + ModelProviderDiscoveryNetworkScopeConfiguredEndpointOnly ModelProviderDiscoveryNetworkScope = "configuredEndpointOnly" + // Discovery may contact remote internet services. + ModelProviderDiscoveryNetworkScopeInternet ModelProviderDiscoveryNetworkScope = "internet" + // Discovery may scan or contact the local network. + ModelProviderDiscoveryNetworkScopeLocalNetwork ModelProviderDiscoveryNetworkScope = "localNetwork" + // Discovery is limited to loopback addresses on the local machine. + ModelProviderDiscoveryNetworkScopeLoopbackOnly ModelProviderDiscoveryNetworkScope = "loopbackOnly" + // Discovery does not contact a network service. + ModelProviderDiscoveryNetworkScopeNone ModelProviderDiscoveryNetworkScope = "none" +) + +// The neutral kind of a model provider — the model analog of `AccountKind`. A model +// provider is the live, entitled source a model came from; central code never branches on +// this beyond a single dispatch. +// Experimental: ModelProviderKind is part of an experimental API and may change or be +// removed. +type ModelProviderKind string + +const ( + // GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot + // seat. + ModelProviderKindCopilot ModelProviderKind = "copilot" + // Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki + // account. + ModelProviderKindLoki ModelProviderKind = "loki" +) + +// Typed outcome for a provider operation. +// Experimental: ModelProviderOperationOutcomeCode is part of an experimental API and may +// change or be removed. +type ModelProviderOperationOutcomeCode string + +const ( + // The provider or instance is absent during discovery, status, or model listing. Distinct + // from a successful empty inventory. + ModelProviderOperationOutcomeCodeAbsent ModelProviderOperationOutcomeCode = "absent" + // The operation failed for a reason other than absence or reachability. + ModelProviderOperationOutcomeCodeFailed ModelProviderOperationOutcomeCode = "failed" + // The operation completed successfully; an empty inventory is valid. + ModelProviderOperationOutcomeCodeSuccess ModelProviderOperationOutcomeCode = "success" + // The provider is configured or expected but could not be reached. + ModelProviderOperationOutcomeCodeUnreachable ModelProviderOperationOutcomeCode = "unreachable" +) + +// Kind of component that supplied a provider adapter or row. Attribution does not confer +// authority. +// Experimental: ModelProviderProvenanceSource is part of an experimental API and may change +// or be removed. +type ModelProviderProvenanceSource string + +const ( + // Built into the runtime. + ModelProviderProvenanceSourceBuiltIn ModelProviderProvenanceSource = "builtIn" + // Derived from existing user configuration. + ModelProviderProvenanceSourceConfigured ModelProviderProvenanceSource = "configured" + // Supplied by another trusted contributor. + ModelProviderProvenanceSourceCustom ModelProviderProvenanceSource = "custom" + // Supplied by an extension. + ModelProviderProvenanceSourceExtension ModelProviderProvenanceSource = "extension" +) + // Whether the requested preference was already effective or was accepted for later // transactional activation. // Experimental: ModelSwitchAutoTierStatus is part of an experimental API and may change or @@ -21465,11 +25137,11 @@ const ( PermissionDecisionApproveForLocationApprovalKindExtensionEnvAccess PermissionDecisionApproveForLocationApprovalKind = "extension-env-access" PermissionDecisionApproveForLocationApprovalKindExtensionManagement PermissionDecisionApproveForLocationApprovalKind = "extension-management" PermissionDecisionApproveForLocationApprovalKindExtensionPermissionAccess PermissionDecisionApproveForLocationApprovalKind = "extension-permission-access" - PermissionDecisionApproveForLocationApprovalKindFactory PermissionDecisionApproveForLocationApprovalKind = "factory" PermissionDecisionApproveForLocationApprovalKindMCP PermissionDecisionApproveForLocationApprovalKind = "mcp" PermissionDecisionApproveForLocationApprovalKindMCPSampling PermissionDecisionApproveForLocationApprovalKind = "mcp-sampling" PermissionDecisionApproveForLocationApprovalKindMemory PermissionDecisionApproveForLocationApprovalKind = "memory" PermissionDecisionApproveForLocationApprovalKindRead PermissionDecisionApproveForLocationApprovalKind = "read" + PermissionDecisionApproveForLocationApprovalKindWorkflow PermissionDecisionApproveForLocationApprovalKind = "workflow" PermissionDecisionApproveForLocationApprovalKindWrite PermissionDecisionApproveForLocationApprovalKind = "write" ) @@ -21482,11 +25154,11 @@ const ( PermissionDecisionApproveForSessionApprovalKindExtensionEnvAccess PermissionDecisionApproveForSessionApprovalKind = "extension-env-access" PermissionDecisionApproveForSessionApprovalKindExtensionManagement PermissionDecisionApproveForSessionApprovalKind = "extension-management" PermissionDecisionApproveForSessionApprovalKindExtensionPermissionAccess PermissionDecisionApproveForSessionApprovalKind = "extension-permission-access" - PermissionDecisionApproveForSessionApprovalKindFactory PermissionDecisionApproveForSessionApprovalKind = "factory" PermissionDecisionApproveForSessionApprovalKindMCP PermissionDecisionApproveForSessionApprovalKind = "mcp" PermissionDecisionApproveForSessionApprovalKindMCPSampling PermissionDecisionApproveForSessionApprovalKind = "mcp-sampling" PermissionDecisionApproveForSessionApprovalKindMemory PermissionDecisionApproveForSessionApprovalKind = "memory" PermissionDecisionApproveForSessionApprovalKindRead PermissionDecisionApproveForSessionApprovalKind = "read" + PermissionDecisionApproveForSessionApprovalKindWorkflow PermissionDecisionApproveForSessionApprovalKind = "workflow" PermissionDecisionApproveForSessionApprovalKindWrite PermissionDecisionApproveForSessionApprovalKind = "write" ) @@ -21501,6 +25173,7 @@ const ( PermissionDecisionKindApproveForSession PermissionDecisionKind = "approve-for-session" PermissionDecisionKindApproveOnce PermissionDecisionKind = "approve-once" PermissionDecisionKindApprovePermanently PermissionDecisionKind = "approve-permanently" + PermissionDecisionKindApproveReadOnlyForSession PermissionDecisionKind = "approve-read-only-for-session" PermissionDecisionKindCancelled PermissionDecisionKind = "cancelled" PermissionDecisionKindDeniedByContentExclusionPolicy PermissionDecisionKind = "denied-by-content-exclusion-policy" PermissionDecisionKindDeniedByPermissionRequestHook PermissionDecisionKind = "denied-by-permission-request-hook" @@ -21533,9 +25206,8 @@ type PermissionDecisionSource string const ( // The response followed the assisted-approval judge recommendation. PermissionDecisionSourceAssistedApproval PermissionDecisionSource = "assisted_approval" - // A live authorization record from an earlier human decision in this session contained the - // proposal, so it ran without another prompt. This is not a new human decision and never - // mints authority of its own. + // Historical compatibility value for sessions created while authorization carry-forward was + // executable. Current runtimes do not produce this source. PermissionDecisionSourceAuthorizationCarryForward PermissionDecisionSource = "authorization_carry_forward" // The host applied a standing policy or override rather than a judge recommendation or // human decision. @@ -21590,8 +25262,9 @@ const ( PermissionModeManual PermissionMode = "manual" ) -// Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK -// callers. +// 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. // Experimental: PermissionModeSource is part of an experimental API and may change or be // removed. type PermissionModeSource string @@ -21601,7 +25274,8 @@ const ( PermissionModeSourceAutopilotConfirmation PermissionModeSource = "autopilot_confirmation" // The mode was set from a CLI command-line flag. PermissionModeSourceCLIFlag PermissionModeSource = "cli_flag" - // The mode was set at startup by authenticated organization targeting. + // The mode was set at startup because the authenticated account matched an organization + // targeting policy. PermissionModeSourceOrganizationTargeting PermissionModeSource = "organization_targeting" // The mode was set through an RPC caller. PermissionModeSourceRPC PermissionModeSource = "rpc" @@ -21753,11 +25427,11 @@ const ( PermissionsLocationsAddToolApprovalDetailsKindExtensionEnvAccess PermissionsLocationsAddToolApprovalDetailsKind = "extension-env-access" PermissionsLocationsAddToolApprovalDetailsKindExtensionManagement PermissionsLocationsAddToolApprovalDetailsKind = "extension-management" PermissionsLocationsAddToolApprovalDetailsKindExtensionPermissionAccess PermissionsLocationsAddToolApprovalDetailsKind = "extension-permission-access" - PermissionsLocationsAddToolApprovalDetailsKindFactory PermissionsLocationsAddToolApprovalDetailsKind = "factory" PermissionsLocationsAddToolApprovalDetailsKindMCP PermissionsLocationsAddToolApprovalDetailsKind = "mcp" PermissionsLocationsAddToolApprovalDetailsKindMCPSampling PermissionsLocationsAddToolApprovalDetailsKind = "mcp-sampling" PermissionsLocationsAddToolApprovalDetailsKindMemory PermissionsLocationsAddToolApprovalDetailsKind = "memory" PermissionsLocationsAddToolApprovalDetailsKindRead PermissionsLocationsAddToolApprovalDetailsKind = "read" + PermissionsLocationsAddToolApprovalDetailsKindWorkflow PermissionsLocationsAddToolApprovalDetailsKind = "workflow" PermissionsLocationsAddToolApprovalDetailsKindWrite PermissionsLocationsAddToolApprovalDetailsKind = "write" ) @@ -21871,6 +25545,28 @@ const ( ProtocolSystemMessageConfigModeReplace ProtocolSystemMessageConfigMode = "replace" ) +// The product serving the model, reported in telemetry as `model_provider`. +// Experimental: ProviderConfigModelProvider is part of an experimental API and may change +// or be removed. +type ProviderConfigModelProvider string + +const ( + // Anthropic API. + ProviderConfigModelProviderAnthropic ProviderConfigModelProvider = "anthropic" + // Azure OpenAI Service. + ProviderConfigModelProviderAzureOpenai ProviderConfigModelProvider = "azure_openai" + // Foundry Local. + ProviderConfigModelProviderFoundryLocal ProviderConfigModelProvider = "foundry_local" + // llama.cpp server. + ProviderConfigModelProviderLlamaCpp ProviderConfigModelProvider = "llama_cpp" + // LM Studio. + ProviderConfigModelProviderLmStudio ProviderConfigModelProvider = "lm_studio" + // Ollama. + ProviderConfigModelProviderOllama ProviderConfigModelProvider = "ollama" + // OpenAI API. + ProviderConfigModelProviderOpenai ProviderConfigModelProvider = "openai" +) + // Provider transport. Defaults to "http". // Experimental: ProviderConfigTransport is part of an experimental API and may change or be // removed. @@ -22097,7 +25793,8 @@ const ( ResponseFormatTypeJSONSchema ResponseFormatType = "json_schema" ) -// Origin of the sandbox choice supplied by an internal client. +// Origin of the sandbox choice supplied by the host. This value describes preference or +// session intent; it does not authorize bypassing managed policy. // Experimental: SandboxConfigSource is part of an experimental API and may change or be // removed. type SandboxConfigSource string @@ -22109,7 +25806,7 @@ const ( SandboxConfigSourceRepositoryPolicy SandboxConfigSource = "repository_policy" // The user disabled the sandbox for the current session. SandboxConfigSourceSessionDisabled SandboxConfigSource = "session_disabled" - // A command-line flag selected the sandbox state for this session. + // An explicit session-scoped choice selected the sandbox state, such as a command-line flag. SandboxConfigSourceSessionFlag SandboxConfigSource = "session_flag" // The client disabled the sandbox because the host cannot enforce it. SandboxConfigSourceUnsupportedHost SandboxConfigSource = "unsupported_host" @@ -22119,6 +25816,23 @@ const ( SandboxConfigSourceUserEnabled SandboxConfigSource = "user_enabled" ) +// State of the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaState is part of an experimental API and may change or be +// removed. +type SandboxProxyCaState string + +const ( + // The runtime could not read the certificate authority or the OS trust store. + SandboxProxyCaStateError SandboxProxyCaState = "error" + // OS trust includes the stored certificate authority. + SandboxProxyCaStateInstalled SandboxProxyCaState = "installed" + // OS trust does not include the certificate authority, or none is stored. + SandboxProxyCaStateNotInstalled SandboxProxyCaState = "notInstalled" + // This platform has no supported OS trust store. The proxy uses a per-process certificate + // bundle. + SandboxProxyCaStateUnsupported SandboxProxyCaState = "unsupported" +) + // A session-scoped sandbox transition applied while handling a slash command // Experimental: SandboxSessionChange is part of an experimental API and may change or be // removed. @@ -22608,6 +26322,7 @@ const ( type SettableAuthInfoType string const ( + SettableAuthInfoTypeAccount SettableAuthInfoType = "account" SettableAuthInfoTypeAPIKey SettableAuthInfoType = "api-key" SettableAuthInfoTypeCopilotAPIToken SettableAuthInfoType = "copilot-api-token" SettableAuthInfoTypeEnv SettableAuthInfoType = "env" @@ -22683,6 +26398,180 @@ const ( SkillDiscoveryScopeProject SkillDiscoveryScope = "project" ) +// Bounded refusal categories for verified Skill installation management. +// Experimental: SkillInstallationFailureReason is part of an experimental API and may +// change or be removed. +type SkillInstallationFailureReason string + +const ( + // An owned Skill with the same identity or target already exists. + SkillInstallationFailureReasonAlreadyInstalled SkillInstallationFailureReason = "already-installed" + // Skill installation storage or admission is busy. + SkillInstallationFailureReasonBusy SkillInstallationFailureReason = "busy" + // The original operation was cancelled. + SkillInstallationFailureReasonCancelled SkillInstallationFailureReason = "cancelled" + // Installed Skill files no longer match ownership evidence. + SkillInstallationFailureReasonConfigurationModified SkillInstallationFailureReason = "configuration-modified" + // The confirmation response is malformed or mismatched. + SkillInstallationFailureReasonConfirmationInvalid SkillInstallationFailureReason = "confirmation-invalid" + // The original host cannot receive human confirmation. + SkillInstallationFailureReasonConfirmationUnavailable SkillInstallationFailureReason = "confirmation-unavailable" + // The verified Skill descriptor failed validation. + SkillInstallationFailureReasonDescriptorInvalid SkillInstallationFailureReason = "descriptor-invalid" + // The verified Skill descriptor could not be retrieved safely. + SkillInstallationFailureReasonDescriptorUnavailable SkillInstallationFailureReason = "descriptor-unavailable" + // The Skill entrypoint could not be retrieved or verified. + SkillInstallationFailureReasonEntrypointUnavailable SkillInstallationFailureReason = "entrypoint-unavailable" + // The handle or operation expired. + SkillInstallationFailureReasonExpired SkillInstallationFailureReason = "expired" + // The selected session does not have the Agent Finder Skill installation feature flag + // enabled for acquisition. + SkillInstallationFailureReasonFeatureDisabled SkillInstallationFailureReason = "feature-disabled" + // The handle belongs to a different runtime, session or connection. + SkillInstallationFailureReasonForeignRuntime SkillInstallationFailureReason = "foreign-runtime" + // The candidate handle is not a verified installable Skill candidate. + SkillInstallationFailureReasonInvalidCandidate SkillInstallationFailureReason = "invalid-candidate" + // The request is unsupported or malformed. + SkillInstallationFailureReasonInvalidRequest SkillInstallationFailureReason = "invalid-request" + // The Skill entrypoint is not a valid Skill. + SkillInstallationFailureReasonInvalidSkill SkillInstallationFailureReason = "invalid-skill" + // The selected runtime cannot inspect or control the requested lifecycle operation. + SkillInstallationFailureReasonLifecycleUnavailable SkillInstallationFailureReason = "lifecycle-unavailable" + // The bounded operation limit was reached. + SkillInstallationFailureReasonOperationLimit SkillInstallationFailureReason = "operation-limit" + // The acquired payload no longer matches the reviewed descriptor. + SkillInstallationFailureReasonPayloadMismatch SkillInstallationFailureReason = "payload-mismatch" + // The reviewed Skill payload could not be acquired. + SkillInstallationFailureReasonPayloadUnavailable SkillInstallationFailureReason = "payload-unavailable" + // The original plan deadline elapsed. + SkillInstallationFailureReasonPlanExpired SkillInstallationFailureReason = "plan-expired" + // The one-use plan was already consumed. + SkillInstallationFailureReasonPlanReplayed SkillInstallationFailureReason = "plan-replayed" + // The original authority or policy changed. + SkillInstallationFailureReasonPolicyChanged SkillInstallationFailureReason = "policy-changed" + // Existing authenticated session and host authority is unavailable. + SkillInstallationFailureReasonPolicyContextUnavailable SkillInstallationFailureReason = "policy-context-unavailable" + // Durable Skill installation evidence requires recovery before mutation. + SkillInstallationFailureReasonRecoveryRequired SkillInstallationFailureReason = "recovery-required" + // No matching owned resource or original operation exists. + SkillInstallationFailureReasonResourceNotFound SkillInstallationFailureReason = "resource-not-found" + // The Skill entrypoint exceeds the bounded complete review size. + SkillInstallationFailureReasonReviewTooLarge SkillInstallationFailureReason = "review-too-large" + // The handle was minted for a different search result or authority. + SkillInstallationFailureReasonSearchMismatch SkillInstallationFailureReason = "search-mismatch" + // The retained Skill source changed after planning. + SkillInstallationFailureReasonSourceChanged SkillInstallationFailureReason = "source-changed" + // A storage operation failed; inspect durable state before retrying. + SkillInstallationFailureReasonWriteFailed SkillInstallationFailureReason = "write-failed" + // The handle is not the expected Skill handle kind. + SkillInstallationFailureReasonWrongKind SkillInstallationFailureReason = "wrong-kind" +) + +// Kind discriminator for SkillInstallationManagementOutcome. +type SkillInstallationManagementOutcomeKind string + +const ( + SkillInstallationManagementOutcomeKindEnabledChanged SkillInstallationManagementOutcomeKind = "enabled-changed" + SkillInstallationManagementOutcomeKindInstallPlanned SkillInstallationManagementOutcomeKind = "install-planned" + SkillInstallationManagementOutcomeKindListed SkillInstallationManagementOutcomeKind = "listed" + SkillInstallationManagementOutcomeKindOperation SkillInstallationManagementOutcomeKind = "operation" + SkillInstallationManagementOutcomeKindRecovered SkillInstallationManagementOutcomeKind = "recovered" + SkillInstallationManagementOutcomeKindRecoveryRequired SkillInstallationManagementOutcomeKind = "recovery-required" + SkillInstallationManagementOutcomeKindRefused SkillInstallationManagementOutcomeKind = "refused" + SkillInstallationManagementOutcomeKindRolledBack SkillInstallationManagementOutcomeKind = "rolled-back" + SkillInstallationManagementOutcomeKindUninstallPlanned SkillInstallationManagementOutcomeKind = "uninstall-planned" +) + +// Kind discriminator for SkillInstallationManagementResult. +type SkillInstallationManagementResultKind string + +const ( + SkillInstallationManagementResultKindInvalidRequest SkillInstallationManagementResultKind = "invalid-request" + SkillInstallationManagementResultKindNegotiationRefused SkillInstallationManagementResultKind = "negotiation-refused" + SkillInstallationManagementResultKindOutcome SkillInstallationManagementResultKind = "outcome" +) + +// Phase discriminator for SkillInstallationOperationStatus. +type SkillInstallationOperationStatusPhase string + +const ( + SkillInstallationOperationStatusPhaseApplying SkillInstallationOperationStatusPhase = "applying" + SkillInstallationOperationStatusPhaseAwaitingConfirmation SkillInstallationOperationStatusPhase = "awaiting-confirmation" + SkillInstallationOperationStatusPhaseCompleted SkillInstallationOperationStatusPhase = "completed" + SkillInstallationOperationStatusPhasePrepared SkillInstallationOperationStatusPhase = "prepared" + SkillInstallationOperationStatusPhasePreparing SkillInstallationOperationStatusPhase = "preparing" + SkillInstallationOperationStatusPhaseRevalidating SkillInstallationOperationStatusPhase = "revalidating" +) + +// Kind discriminator for SkillInstallationOutcome. +type SkillInstallationOutcomeKind string + +const ( + SkillInstallationOutcomeKindCancelled SkillInstallationOutcomeKind = "cancelled" + SkillInstallationOutcomeKindDeclined SkillInstallationOutcomeKind = "declined" + SkillInstallationOutcomeKindInstalled SkillInstallationOutcomeKind = "installed" + SkillInstallationOutcomeKindRecoveryRequired SkillInstallationOutcomeKind = "recovery-required" + SkillInstallationOutcomeKindRefused SkillInstallationOutcomeKind = "refused" + SkillInstallationOutcomeKindRolledBack SkillInstallationOutcomeKind = "rolled-back" + SkillInstallationOutcomeKindUninstalled SkillInstallationOutcomeKind = "uninstalled" +) + +// Owned Skill state observed from files and receipts. +// Experimental: SkillInstallationOwnershipState is part of an experimental API and may +// change or be removed. +type SkillInstallationOwnershipState string + +const ( + // Owned files and receipt evidence match. + SkillInstallationOwnershipStateIntact SkillInstallationOwnershipState = "intact" + // Owned files no longer match the receipt. + SkillInstallationOwnershipStateModified SkillInstallationOwnershipState = "modified" + // Ownership evidence requires recovery before mutation. + SkillInstallationOwnershipStateRecoveryRequired SkillInstallationOwnershipState = "recovery-required" +) + +// Kind discriminator for SkillInstallationResult. +type SkillInstallationResultKind string + +const ( + SkillInstallationResultKindInvalidRequest SkillInstallationResultKind = "invalid-request" + SkillInstallationResultKindNegotiationRefused SkillInstallationResultKind = "negotiation-refused" + SkillInstallationResultKindOutcome SkillInstallationResultKind = "outcome" +) + +// Action discriminator for SkillInstallationReview. +type SkillInstallationReviewAction string + +const ( + SkillInstallationReviewActionInstall SkillInstallationReviewAction = "install" + SkillInstallationReviewActionUninstall SkillInstallationReviewAction = "uninstall" +) + +// Experimental: SkillInstallationScope is part of an experimental API and may change or be +// removed. +type SkillInstallationScope string + +const ( + // The user's personal Copilot home. + SkillInstallationScopePersonal SkillInstallationScope = "personal" +) + +// Bound-session observation after reconciling persisted enablement. +// Experimental: SkillInstallationSessionState is part of an experimental API and may change +// or be removed. +type SkillInstallationSessionState string + +const ( + // The selected session has loaded this Skill or settings and it is disabled. + SkillInstallationSessionStateLoadedDisabled SkillInstallationSessionState = "loaded-disabled" + // The selected session has loaded this Skill and it is enabled. + SkillInstallationSessionStateLoadedEnabled SkillInstallationSessionState = "loaded-enabled" + // The selected session has not loaded Skills after the latest change. + SkillInstallationSessionStateNotLoaded SkillInstallationSessionState = "not-loaded" + // The selected session could not be inspected. + SkillInstallationSessionStateUnknown SkillInstallationSessionState = "unknown" +) + // Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) // Experimental: SkillSource is part of an experimental API and may change or be removed. type SkillSource string @@ -23087,10 +26976,10 @@ const ( UserToolSessionApprovalKindExtensionEnvAccess UserToolSessionApprovalKind = "extension-env-access" UserToolSessionApprovalKindExtensionManagement UserToolSessionApprovalKind = "extension-management" UserToolSessionApprovalKindExtensionPermissionAccess UserToolSessionApprovalKind = "extension-permission-access" - UserToolSessionApprovalKindFactory UserToolSessionApprovalKind = "factory" UserToolSessionApprovalKindMCP UserToolSessionApprovalKind = "mcp" UserToolSessionApprovalKindMemory UserToolSessionApprovalKind = "memory" UserToolSessionApprovalKindRead UserToolSessionApprovalKind = "read" + UserToolSessionApprovalKindWorkflow UserToolSessionApprovalKind = "workflow" UserToolSessionApprovalKindWrite UserToolSessionApprovalKind = "write" ) @@ -23543,6 +27432,147 @@ func (a *ServerCommandsAPI) List(ctx context.Context) (*CommandList, error) { return &result, nil } +// Experimental: ServerConnectorsAPI contains experimental APIs that may change or be +// removed. +type ServerConnectorsAPI serverAPI + +// GetAccounts returns eligible accounts. +// +// RPC method: connectors.getAccounts. +// +// Returns: Eligible accounts. +func (a *ServerConnectorsAPI) GetAccounts(ctx context.Context) (*ConnectorDiscoveryAccountList, error) { + raw, err := a.client.Request(ctx, "connectors.getAccounts", nil) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryAccountList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetCapabilities returns feature availability. +// +// RPC method: connectors.getCapabilities. +// +// Returns: Feature availability. +func (a *ServerConnectorsAPI) GetCapabilities(ctx context.Context) (*ConnectorDiscoveryCapabilities, error) { + raw, err := a.client.Request(ctx, "connectors.getCapabilities", nil) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCapabilities + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists entries for the selected account. +// +// RPC method: connectors.list. +// +// Parameters: Selected account. +// +// Returns: Entries for the selected account. +func (a *ServerConnectorsAPI) List(ctx context.Context, params *ConnectorDiscoveryAccountRequest) (*ConnectorDiscoveryCatalogResult, error) { + raw, err := a.client.Request(ctx, "connectors.list", params) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Refreshes entries for the selected account. +// +// RPC method: connectors.refresh. +// +// Parameters: Selected account. +// +// Returns: Entries for the selected account. +func (a *ServerConnectorsAPI) Refresh(ctx context.Context, params *ConnectorDiscoveryAccountRequest) (*ConnectorDiscoveryCatalogResult, error) { + raw, err := a.client.Request(ctx, "connectors.refresh", params) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ServerEnvironmentsAPI contains experimental APIs that may change or be +// removed. +type ServerEnvironmentsAPI serverAPI + +// Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments +// cannot be deleted. Does not stop a running host, which may register again. +// +// RPC method: environments.delete. +// +// Parameters: Identify a user-managed Mission Control environment to delete. +// +// Returns: Acknowledgement that the requested environment was deleted. +func (a *ServerEnvironmentsAPI) Delete(ctx context.Context, params *EnvironmentsDeleteRequest) (*EnvironmentsDeleteResult, error) { + raw, err := a.client.Request(ctx, "environments.delete", params) + if err != nil { + return nil, err + } + var result EnvironmentsDeleteResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Gets safe discovery information for a GitHub Mission Control environment without +// requiring a running host. +// +// RPC method: environments.get. +// +// Parameters: Identify a Mission Control environment to retrieve. +// +// Returns: Safe discovery information for the requested environment. +func (a *ServerEnvironmentsAPI) Get(ctx context.Context, params *EnvironmentsGetRequest) (*EnvironmentsGetResult, error) { + raw, err := a.client.Request(ctx, "environments.get", params) + if err != nil { + return nil, err + } + var result EnvironmentsGetResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists GitHub Mission Control environments visible to the authenticated identity. Does not +// require a running host and excludes host relay credentials. +// +// RPC method: environments.list. +// +// Parameters: Optional discovery filters supported by GitHub Mission Control. +// +// Returns: Environments visible to the authenticated caller and matching the supplied +// filters. +func (a *ServerEnvironmentsAPI) List(ctx context.Context, params *EnvironmentsListRequest) (*EnvironmentsListResult, error) { + raw, err := a.client.Request(ctx, "environments.list", params) + if err != nil { + return nil, err + } + var result EnvironmentsListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerExtensionsAPI contains experimental APIs that may change or be // removed. type ServerExtensionsAPI serverAPI @@ -23630,6 +27660,70 @@ func (a *ServerHooksAPI) Discover(ctx context.Context, params *HooksDiscoverRequ return &result, nil } +// Experimental: ServerHostAPI contains experimental APIs that may change or be removed. +type ServerHostAPI serverAPI + +// Dispose stops a listener owned by this SDK connection and joins its cleanup without +// deleting sessions. +// +// RPC method: host.dispose. +// +// Parameters: Stops a connection-owned listener and joins its teardown. +// +// Returns: Empty acknowledgement for a completed host lifecycle operation. +func (a *ServerHostAPI) Dispose(ctx context.Context, params *HostDisposeRequest) (*HostDisposeResult, error) { + raw, err := a.client.Request(ctx, "host.dispose", params) + if err != nil { + return nil, err + } + var result HostDisposeResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// PublishSession publishes an attached resident session for this listener's lifetime +// without copying it. +// +// RPC method: host.publishSession. +// +// Parameters: Publishes a resident session attached to the listener's owning connection. +// +// Returns: The existing runtime identity and its resource on the listener. +func (a *ServerHostAPI) PublishSession(ctx context.Context, params *HostPublishSessionRequest) (*HostPublishSessionResult, error) { + raw, err := a.client.Request(ctx, "host.publishSession", params) + if err != nil { + return nil, err + } + var result HostPublishSessionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment +// transports as a supervised SDK participant. +// +// RPC method: host.start. +// +// Parameters: 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. +func (a *ServerHostAPI) Start(ctx context.Context, params *HostStartRequest) (*HostStartResult, error) { + raw, err := a.client.Request(ctx, "host.start", params) + if err != nil { + return nil, err + } + var result HostStartResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerInstructionsAPI contains experimental APIs that may change or be // removed. type ServerInstructionsAPI serverAPI @@ -23752,11 +27846,11 @@ type ServerManagedSettingsAPI serverAPI // 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. +// 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. // // RPC method: managedSettings.clearCache. func (a *ServerManagedSettingsAPI) ClearCache(ctx context.Context) (*ManagedSettingsClearCacheResult, error) { @@ -23771,9 +27865,36 @@ func (a *ServerManagedSettingsAPI) ClearCache(ctx context.Context) (*ManagedSett return &result, nil } +// Compose 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. +// +// RPC method: managedSettings.compose. +// +// Parameters: 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. +func (a *ServerManagedSettingsAPI) Compose(ctx context.Context, params *ManagedSettingsComposeRequest) (*ManagedSettingsComposeResult, error) { + raw, err := a.client.Request(ctx, "managedSettings.compose", params) + if err != nil { + return nil, err + } + var result ManagedSettingsComposeResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Read 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. +// canonical JSON without requiring a session. `managedSettings.resolve` returns the same +// device settings together with the account's server policy. // // RPC method: managedSettings.read. // @@ -23790,9 +27911,130 @@ func (a *ServerManagedSettingsAPI) Read(ctx context.Context) (*ManagedSettingsRe return &result, nil } +// 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. +// +// RPC method: managedSettings.resolve. +// +// Parameters: Optional opaque account selection or GitHub token whose managed settings are +// resolved. +// +// Returns: Effective enterprise managed settings for an account, resolved without a session. +func (a *ServerManagedSettingsAPI) Resolve(ctx context.Context, params *ManagedSettingsResolveRequest) (*ManagedSettingsResolveResult, error) { + if params == nil { + params = &ManagedSettingsResolveRequest{} + } + raw, err := a.client.Request(ctx, "managedSettings.resolve", params) + if err != nil { + return nil, err + } + var result ManagedSettingsResolveResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Schema 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. +// +// RPC method: managedSettings.schema. +// +// Returns: The authoring JSON schema for managed settings recognized by this runtime. +func (a *ServerManagedSettingsAPI) Schema(ctx context.Context) (*ManagedSettingsSchemaResult, error) { + raw, err := a.client.Request(ctx, "managedSettings.schema", nil) + if err != nil { + return nil, err + } + var result ManagedSettingsSchemaResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// 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. +// +// RPC method: managedSettings.validate. +// +// Parameters: A candidate managed-settings document to validate without applying it. +// +// Returns: Result of validating a managed-settings document. +func (a *ServerManagedSettingsAPI) Validate(ctx context.Context, params *ManagedSettingsValidateRequest) (*ManagedSettingsValidateResult, error) { + raw, err := a.client.Request(ctx, "managedSettings.validate", params) + if err != nil { + return nil, err + } + var result ManagedSettingsValidateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerMCPAPI contains experimental APIs that may change or be removed. type ServerMCPAPI serverAPI +// ApplyInstall 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. +// +// RPC method: mcp.applyInstall. +// +// Parameters: Applies exactly one previously prepared operation on its original connection. +// +// Returns: An installation result together with the exact honoured contract, or a +// negotiation refusal. +func (a *ServerMCPAPI) ApplyInstall(ctx context.Context, params *MCPApplyInstallRequest) (MCPInstallationResult, error) { + raw, err := a.client.Request(ctx, "mcp.applyInstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// ApplyUninstall 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. +// +// RPC method: mcp.applyUninstall. +// +// Parameters: One-use application of the exact retained removal plan. +// +// Returns: An installation result together with the exact honoured contract, or a +// negotiation refusal. +func (a *ServerMCPAPI) ApplyUninstall(ctx context.Context, params *MCPApplyUninstallRequest) (MCPInstallationResult, error) { + raw, err := a.client.Request(ctx, "mcp.applyUninstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + // Discovers MCP servers from user, workspace, plugin, and builtin sources. // // RPC method: mcp.discover. @@ -23842,6 +28084,52 @@ func (a *ServerMCPAPI) PlanInstall(ctx context.Context, params *MCPPlanInstallRe return result, nil } +// PlanUninstall 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. +// +// RPC method: mcp.planUninstall. +// +// Parameters: Read-only preparation of one owned removal under fresh selected-session +// authority. +// +// Returns: Management result with contract receipt, or a typed request/negotiation refusal. +func (a *ServerMCPAPI) PlanUninstall(ctx context.Context, params *MCPPlanUninstallRequest) (MCPInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "mcp.planUninstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// PrepareInstall 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. +// +// RPC method: mcp.prepareInstall. +// +// Parameters: Side-effect-free preparation of one original bound remote MCP choice. +// +// Returns: Management result with contract receipt, or a typed request/negotiation refusal. +func (a *ServerMCPAPI) PrepareInstall(ctx context.Context, params *MCPPrepareInstallRequest) (MCPInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "mcp.prepareInstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + // Experimental: ServerMCPConfigAPI contains experimental APIs that may change or be removed. type ServerMCPConfigAPI serverAPI @@ -23968,6 +28256,103 @@ func (s *ServerMCPAPI) Config() *ServerMCPConfigAPI { return (*ServerMCPConfigAPI)(s) } +// Experimental: ServerMCPInstallationsAPI contains experimental APIs that may change or be +// removed. +type ServerMCPInstallationsAPI serverAPI + +// Cancel 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. +// +// RPC method: mcp.installations.cancel. +// +// Parameters: Existing-operation control. A new session selector is deliberately not +// accepted. +// +// Returns: Management result with contract receipt, or a typed request/negotiation refusal. +func (a *ServerMCPInstallationsAPI) Cancel(ctx context.Context, params *MCPInstallationOperationRequest) (MCPInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "mcp.installations.cancel", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// List 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. +// +// RPC method: mcp.installations.list. +// +// Parameters: New-work inventory or recovery request under an explicitly selected existing +// session. +// +// Returns: Management result with contract receipt, or a typed request/negotiation refusal. +func (a *ServerMCPInstallationsAPI) List(ctx context.Context, params *MCPInstallationsRequest) (MCPInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "mcp.installations.list", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Recover 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. +// +// RPC method: mcp.installations.recover. +// +// Parameters: New-work inventory or recovery request under an explicitly selected existing +// session. +// +// Returns: Management result with contract receipt, or a typed request/negotiation refusal. +func (a *ServerMCPInstallationsAPI) Recover(ctx context.Context, params *MCPInstallationsRequest) (MCPInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "mcp.installations.recover", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Status 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. +// +// RPC method: mcp.installations.status. +// +// Parameters: Existing-operation control. A new session selector is deliberately not +// accepted. +// +// Returns: Management result with contract receipt, or a typed request/negotiation refusal. +func (a *ServerMCPInstallationsAPI) Status(ctx context.Context, params *MCPInstallationOperationRequest) (MCPInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "mcp.installations.status", params) + if err != nil { + return nil, err + } + result, err := unmarshalMCPInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Experimental: Installations returns experimental APIs that may change or be removed. +func (s *ServerMCPAPI) Installations() *ServerMCPInstallationsAPI { + return (*ServerMCPInstallationsAPI)(s) +} + // Experimental: ServerModelsAPI contains experimental APIs that may change or be removed. type ServerModelsAPI serverAPI @@ -24313,6 +28698,133 @@ func (a *ServerSandboxAPI) GetHostSupport(ctx context.Context) (*SandboxHostSupp return &result, nil } +// Experimental: ServerSandboxProxyCaAPI contains experimental APIs that may change or be +// removed. +type ServerSandboxProxyCaAPI serverAPI + +// Creates the persistent certificate authority of the sandbox credential proxy if none is +// stored, without changing OS trust, and returns the path of its public certificate. Keeps +// an existing certificate authority, even one that must be rotated. Fails where OS trust is +// unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the +// saved user settings, so it refuses a certificate authority that also covers hosts from +// sandboxConfig. +// +// RPC method: sandbox.proxyCa.create. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Result of creating the persistent certificate authority of the sandbox +// credential proxy. +func (a *ServerSandboxProxyCaAPI) Create(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaCreateResult, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.create", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaCreateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetStatus reports whether the persistent certificate authority of the sandbox credential +// proxy exists, whether OS trust includes it, and whether it must be rotated. Changes +// nothing. +// +// RPC method: sandbox.proxyCa.getStatus. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) GetStatus(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.getStatus", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Removes the persistent certificate authority of the sandbox credential proxy from OS +// trust. Keeps the stored certificate authority. Can show an OS authentication prompt. +// Sandboxed clients that read only OS trust then reject the proxy; clients that read the +// per-process certificate bundle continue to work. +// +// RPC method: sandbox.proxyCa.remove. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Remove(ctx context.Context) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.remove", nil) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Rotate replaces the persistent certificate authority of the sandbox credential proxy with +// a new one for the current credential hosts. If OS trust included the old one, removes it +// and trusts the new one, which can show an OS authentication prompt. Running sandboxed +// tools keep the old certificate authority until they restart. +// +// RPC method: sandbox.proxyCa.rotate. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Rotate(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.rotate", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Trust adds the persistent certificate authority of the sandbox credential proxy to OS +// trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. +// Refuses a certificate authority that is not constrained to the current credential hosts. +// Can show an OS authentication prompt. +// +// RPC method: sandbox.proxyCa.trust. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Trust(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.trust", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ProxyCa returns experimental APIs that may change or be removed. +func (s *ServerSandboxAPI) ProxyCa() *ServerSandboxProxyCaAPI { + return (*ServerSandboxProxyCaAPI)(s) +} + // Experimental: ServerSecretsAPI contains experimental APIs that may change or be removed. type ServerSecretsAPI serverAPI @@ -24913,6 +29425,50 @@ func (a *ServerSessionsAPI) TransferRemoteControl(ctx context.Context, params *S // Experimental: ServerSkillsAPI contains experimental APIs that may change or be removed. type ServerSkillsAPI serverAPI +// ApplyInstall consumes one verified Skill installation plan, requests explicit human +// consent through installations.confirm on the original connection, then revalidates and +// installs the Skill disabled. +// +// RPC method: skills.applyInstall. +// +// Parameters: Applies exactly one retained verified Skill installation plan. +// +// Returns: Skill installation result with the honoured contract, or a typed +// request/negotiation refusal. +func (a *ServerSkillsAPI) ApplyInstall(ctx context.Context, params *SkillApplyInstallRequest) (SkillInstallationResult, error) { + raw, err := a.client.Request(ctx, "skills.applyInstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// ApplyUninstall consumes an owned Skill removal plan, requests explicit human consent +// through installations.confirm, refuses drift, and removes the exact owned files through +// quarantine. +// +// RPC method: skills.applyUninstall. +// +// Parameters: One-use application of the exact retained Skill removal plan. +// +// Returns: Skill installation result with the honoured contract, or a typed +// request/negotiation refusal. +func (a *ServerSkillsAPI) ApplyUninstall(ctx context.Context, params *SkillApplyUninstallRequest) (SkillInstallationResult, error) { + raw, err := a.client.Request(ctx, "skills.applyUninstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + // Discovers skills across global and project sources. // // RPC method: skills.discover. @@ -24955,6 +29511,50 @@ func (a *ServerSkillsAPI) GetDiscoveryPaths(ctx context.Context, params *SkillsG return &result, nil } +// PlanInstall 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. +// +// RPC method: skills.planInstall. +// +// Parameters: Side-effect-free planning of one verified Agent Finder Skill candidate. +// +// Returns: Skill installation management result with the honoured contract, or a typed +// refusal. +func (a *ServerSkillsAPI) PlanInstall(ctx context.Context, params *SkillPlanInstallRequest) (SkillInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "skills.planInstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// PlanUninstall 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. +// +// RPC method: skills.planUninstall. +// +// Parameters: Read-only preparation of one owned Skill removal under fresh selected-session +// authority. +// +// Returns: Skill installation management result with the honoured contract, or a typed +// refusal. +func (a *ServerSkillsAPI) PlanUninstall(ctx context.Context, params *SkillPlanUninstallRequest) (SkillInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "skills.planUninstall", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + // Experimental: ServerSkillsConfigAPI contains experimental APIs that may change or be // removed. type ServerSkillsConfigAPI serverAPI @@ -25000,6 +29600,124 @@ func (s *ServerSkillsAPI) Config() *ServerSkillsConfigAPI { return (*ServerSkillsConfigAPI)(s) } +// Experimental: ServerSkillsInstallationsAPI contains experimental APIs that may change or +// be removed. +type ServerSkillsInstallationsAPI serverAPI + +// Cancel requests cancellation of a known Skill installation operation before commit. +// Already-started durable work requires recovery instead of silent replay. +// +// RPC method: skills.installations.cancel. +// +// Parameters: Existing-operation control. A new session selector is deliberately not +// accepted. +// +// Returns: Skill installation management result with the honoured contract, or a typed +// refusal. +func (a *ServerSkillsInstallationsAPI) Cancel(ctx context.Context, params *SkillInstallationOperationRequest) (SkillInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "skills.installations.cancel", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Lists owned verified Agent Finder Skill installations for the selected existing session. +// Listing is never gated by the Skill-install feature flag. +// +// RPC method: skills.installations.list. +// +// Parameters: Inventory request under an explicitly selected existing session. +// +// Returns: Skill installation management result with the honoured contract, or a typed +// refusal. +func (a *ServerSkillsInstallationsAPI) List(ctx context.Context, params *SkillInstallationsRequest) (SkillInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "skills.installations.list", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Recover 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. +// +// RPC method: skills.installations.recover. +// +// Parameters: Inventory request under an explicitly selected existing session. +// +// Returns: Skill installation management result with the honoured contract, or a typed +// refusal. +func (a *ServerSkillsInstallationsAPI) Recover(ctx context.Context, params *SkillInstallationsRequest) (SkillInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "skills.installations.recover", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// SetEnabled 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. +// +// RPC method: skills.installations.setEnabled. +// +// Parameters: Persisted enablement update for one owned Skill installation. +// +// Returns: Skill installation management result with the honoured contract, or a typed +// refusal. +func (a *ServerSkillsInstallationsAPI) SetEnabled(ctx context.Context, params *SkillSetEnabledRequest) (SkillInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "skills.installations.setEnabled", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Status inspects a known Skill installation operation on its original runtime connection. +// Status is never gated by the Skill-install feature flag. +// +// RPC method: skills.installations.status. +// +// Parameters: Existing-operation control. A new session selector is deliberately not +// accepted. +// +// Returns: Skill installation management result with the honoured contract, or a typed +// refusal. +func (a *ServerSkillsInstallationsAPI) Status(ctx context.Context, params *SkillInstallationOperationRequest) (SkillInstallationManagementResult, error) { + raw, err := a.client.Request(ctx, "skills.installations.status", params) + if err != nil { + return nil, err + } + result, err := unmarshalSkillInstallationManagementResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Experimental: Installations returns experimental APIs that may change or be removed. +func (s *ServerSkillsAPI) Installations() *ServerSkillsInstallationsAPI { + return (*ServerSkillsInstallationsAPI)(s) +} + // Experimental: ServerToolsAPI contains experimental APIs that may change or be removed. type ServerToolsAPI serverAPI @@ -25031,17 +29749,15 @@ type ServerUserAPI serverAPI // removed. type ServerUserSettingsAPI serverAPI -// Get lists every known user setting (settings.json overlaid with the legacy config.json, -// config.json wins), each with its effective value, its default, and whether it is at the -// default — so settings the user has never set still appear with their default value. Does -// not include repository- or enterprise-managed overrides that the runtime layers on top at -// session time. +// Get lists every known user setting from settings.json, each with its effective value, its +// default, and whether it is at the default — so settings the user has never set still +// appear with their default value. Does not include repository- or enterprise-managed +// overrides that the runtime layers on top at session time. // // RPC method: user.settings.get. // -// Returns: Per-key metadata for every known user setting (settings.json overlaid with the -// legacy config.json, config.json wins), including settings left at their default. Excludes -// repository- and enterprise-managed overrides. +// Returns: Per-key metadata for every known user setting in settings.json, including +// settings left at their default. Excludes repository- and enterprise-managed overrides. func (a *ServerUserSettingsAPI) Get(ctx context.Context) (*UserSettingsGetResult, error) { raw, err := a.client.Request(ctx, "user.settings.get", nil) if err != nil { @@ -25054,33 +29770,13 @@ func (a *ServerUserSettingsAPI) Get(ctx context.Context) (*UserSettingsGetResult return &result, nil } -// Reload drops this runtime process's in-memory user settings cache so the next settings -// read observes disk. -// -// RPC method: user.settings.reload. -func (a *ServerUserSettingsAPI) Reload(ctx context.Context) (*UserSettingsReloadResult, error) { - raw, err := a.client.Request(ctx, "user.settings.reload", nil) - if err != nil { - return nil, err - } - var result UserSettingsReloadResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - // Set writes one or more user settings to settings.json, replacing each provided top-level -// key. A key whose value is null is removed. Returns the keys whose new value is shadowed -// by a legacy config.json entry (config.json wins on read), which the runtime leaves in -// place — such writes do not take effect until the legacy value is removed. +// key. A key whose value is null is removed. // // RPC method: user.settings.set. // // Parameters: Partial user settings to write to settings.json. Each top-level key is // written individually, replacing the existing value; a key whose value is null is removed. -// -// Returns: Outcome of writing user settings. func (a *ServerUserSettingsAPI) Set(ctx context.Context, params *UserSettingsSetRequest) (*UserSettingsSetResult, error) { raw, err := a.client.Request(ctx, "user.settings.set", params) if err != nil { @@ -25108,8 +29804,11 @@ type ServerRPC struct { Agents *ServerAgentsAPI Catalog *ServerCatalogAPI Commands *ServerCommandsAPI + Connectors *ServerConnectorsAPI + Environments *ServerEnvironmentsAPI Extensions *ServerExtensionsAPI Hooks *ServerHooksAPI + Host *ServerHostAPI Instructions *ServerInstructionsAPI LlmInference *ServerLlmInferenceAPI ManagedSettings *ServerManagedSettingsAPI @@ -25174,8 +29873,11 @@ func NewServerRPC(client *jsonrpc2.Client) *ServerRPC { r.Agents = (*ServerAgentsAPI)(&r.common) r.Catalog = (*ServerCatalogAPI)(&r.common) r.Commands = (*ServerCommandsAPI)(&r.common) + r.Connectors = (*ServerConnectorsAPI)(&r.common) + r.Environments = (*ServerEnvironmentsAPI)(&r.common) r.Extensions = (*ServerExtensionsAPI)(&r.common) r.Hooks = (*ServerHooksAPI)(&r.common) + r.Host = (*ServerHostAPI)(&r.common) r.Instructions = (*ServerInstructionsAPI)(&r.common) r.LlmInference = (*ServerLlmInferenceAPI)(&r.common) r.ManagedSettings = (*ServerManagedSettingsAPI)(&r.common) @@ -25197,6 +29899,554 @@ type internalServerAPI struct { client *jsonrpc2.Client } +// Experimental: InternalServerAgentsAPI contains experimental APIs that may change or be +// removed. +type InternalServerAgentsAPI internalServerAPI + +// CustomAgentInitialModelDecision resolves the model a custom agent asks for against the +// models actually available, and answers both the model to switch to and the warning a user +// should see when the agent's preference cannot be met. A custom agent may name several +// acceptable models in preference order, so the decision is a match rather than a lookup, +// and an agent whose preference is unavailable is a normal outcome that produces a warning +// rather than an error. A host must call this rather than pick the first available name +// itself, because the preference order and the wording of the warning are what keep one +// installation's agent selection the same as another's. +// +// RPC method: agents.customAgentInitialModelDecision. +// +// Parameters: The models a custom agent asks for, and the models actually available. +// +// Returns: The model to switch to, and the warning to show when the agent's preference +// could not be met. +// Internal: CustomAgentInitialModelDecision is part of the SDK's internal +// handshake/plumbing; external callers should not use it. +func (a *InternalServerAgentsAPI) CustomAgentInitialModelDecision(ctx context.Context, params *AgentsCustomAgentInitialModelDecisionParams) (*AgentsCustomAgentInitialModelDecisionResult, error) { + raw, err := a.client.Request(ctx, "agents.customAgentInitialModelDecision", params) + if err != nil { + return nil, err + } + var result AgentsCustomAgentInitialModelDecisionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetAvailableBuiltins lists the shipped agents a client should offer right now, filtered +// by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; +// some of those are gated, so a client rendering a picker wants this narrower list together +// with the description to show beside each name. +// +// RPC method: agents.getAvailableBuiltins. +// +// Parameters: The feature flags to evaluate shipped agents against. +// +// Returns: The shipped agents available under the requested flags. +// Internal: GetAvailableBuiltins is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerAgentsAPI) GetAvailableBuiltins(ctx context.Context, params *AgentsGetAvailableBuiltinsRequest) (*AgentsGetAvailableBuiltinsResult, error) { + raw, err := a.client.Request(ctx, "agents.getAvailableBuiltins", params) + if err != nil { + return nil, err + } + var result AgentsGetAvailableBuiltinsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltinDefinition loads one shipped agent's YAML definition, for a client that needs +// what the agent declares rather than only its name. `getBuiltins` reports which names have +// a definition to load: a name outside its `yamlBasedNames` is special-cased in code and +// has none. The definition crosses as its own JSON rather than as contract-typed fields, +// because the runtime parses it with the agent schema's tolerant shape and re-typing it +// here would drop the keys that shape accepts and this one does not. The projected +// `__nativeCustomAgent` view the runtime derives is included, so a caller reading the +// declared model and a caller rendering the agent see the same definition. +// +// RPC method: agents.getBuiltinDefinition. +// +// Parameters: The shipped agent whose definition to load. +// +// Returns: One shipped agent's definition. +// Internal: GetBuiltinDefinition is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerAgentsAPI) GetBuiltinDefinition(ctx context.Context, params *AgentsGetBuiltinDefinitionRequest) (*AgentsGetBuiltinDefinitionResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltinDefinition", params) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinDefinitionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltinListingDefinition projects one shipped agent the way a picker lists it, reading +// only the metadata at the head of the definition file and stopping before the prompt body. +// `getBuiltinDefinition` answers the whole definition instead, so a client listing every +// shipped agent should prefer this one: the cost of a listing grows with the number of +// agents, and the prompt body is the part a listing never shows. The two also differ in +// shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` +// returns the authored definition with that projection nested under `__nativeCustomAgent`. +// +// RPC method: agents.getBuiltinListingDefinition. +// +// Parameters: The shipped agent whose listing entry to load. +// +// Returns: One shipped agent, projected for a listing. +// Internal: GetBuiltinListingDefinition is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerAgentsAPI) GetBuiltinListingDefinition(ctx context.Context, params *AgentsGetBuiltinListingDefinitionRequest) (*AgentsGetBuiltinListingDefinitionResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltinListingDefinition", params) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinListingDefinitionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltins lists the agents this runtime ships, by name. A consumer separating shipped +// agents from ones the user or a plugin authored should compare against these names rather +// than against `AgentInfo.source`: an authored agent may carry the `builtin` source while +// not being one of these, and the runtime treats the two as separate questions. +// `disableableNames` is the subset a user may turn off, which a client needs to decide +// whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML +// definition, which a client needs before asking the runtime to load one. +// +// RPC method: agents.getBuiltins. +// +// Returns: The agents this runtime ships, named so a consumer can tell them apart from +// authored ones. +// Internal: GetBuiltins is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerAgentsAPI) GetBuiltins(ctx context.Context) (*AgentsGetBuiltinsResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltins", nil) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitAPI contains experimental APIs that may change or be +// removed. +type InternalServerGitAPI internalServerAPI + +// CurrentBranchRemote reads the remote that the branch checked out in a working tree +// tracks, as `branch..remote` configures it. Reports `origin` rather than failing +// whenever there is no tracking configuration to read — on a detached HEAD, on a branch +// with no upstream, or when git itself fails — because a caller asking which remote to talk +// to needs an answer it can act on, not an error. Marked internal because it exists to +// carry a CLI call site off the napi boundary onto the SDK contract; it is migration +// plumbing, not a surface consumers are meant to depend on. +// +// RPC method: git.currentBranchRemote. +// +// Parameters: Working-tree path a git query applies to. +// +// Returns: The remote the checked-out branch tracks. +// Internal: CurrentBranchRemote is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitAPI) CurrentBranchRemote(ctx context.Context, params *GitCwdRequest) (*GitCurrentBranchRemoteResult, error) { + raw, err := a.client.Request(ctx, "git.currentBranchRemote", params) + if err != nil { + return nil, err + } + var result GitCurrentBranchRemoteResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ReposFromRemotes lists the GitHub repositories a working tree's remotes point at, one +// entry per distinct repository, so a caller can resolve a base and head repository without +// parsing remote URLs itself. When several remotes name the same repository, only the first +// is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are +// left out, so an empty list means the tree reaches GitHub through no remote. Failing to +// read the remotes is reported as an error rather than as an empty list, because the two +// mean different things to a caller. Marked internal because it exists to carry a CLI call +// site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface +// consumers are meant to depend on. +// +// RPC method: git.reposFromRemotes. +// +// Parameters: Git working tree whose GitHub remotes should be listed. +// +// Returns: The GitHub repositories a working tree's remotes point at. +// Internal: ReposFromRemotes is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitAPI) ReposFromRemotes(ctx context.Context, params *GitReposFromRemotesRequest) (*GitReposFromRemotesResult, error) { + raw, err := a.client.Request(ctx, "git.reposFromRemotes", params) + if err != nil { + return nil, err + } + var result GitReposFromRemotesResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// WorkingDirectoryContext collects the repository context of a working directory in one +// call: working tree root, repository identifier and host, current branch, and the HEAD and +// base commits. Every repository field is omitted when the path is not inside a git working +// tree, and the requested path is echoed back as `cwd`. The answer is the same +// `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so +// a caller polling for a context change can forward the result unchanged. Marked internal +// because it exists to carry a CLI call site off the napi boundary onto the SDK contract; +// it is migration plumbing, not a surface consumers are meant to depend on. It can become +// public once an SDK consumer needs to derive session context from a directory itself. +// +// RPC method: git.workingDirectoryContext. +// +// Parameters: Working-tree path a git query applies to. +// +// Returns: Updated working directory and git context. Emitted as the new payload of +// `session.context_changed`. +// Internal: WorkingDirectoryContext is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerGitAPI) WorkingDirectoryContext(ctx context.Context, params *GitCwdRequest) (*SessionWorkingDirectoryContext, error) { + raw, err := a.client.Request(ctx, "git.workingDirectoryContext", params) + if err != nil { + return nil, err + } + var result SessionWorkingDirectoryContext + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitHubOwnersAPI contains experimental APIs that may change or +// be removed. +type InternalServerGitHubOwnersAPI internalServerAPI + +// Cancel abandons an owner listing started with the given request id. Answers `canceled: +// true` while a listing with that id is running. Answers `canceled: false` when the id was +// never registered, was registered but not used, was released after being abandoned, or its +// listing has ended. Canceling an unused id releases it, and a later `list` with that id is +// refused. The cancel acts only on owner listings and never reaches another request of the +// host. +// +// RPC method: gitHubOwners.cancel. +// +// Parameters: The owner listing to abandon. +// +// Returns: Whether the id named a running owner listing. +// Internal: Cancel is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGitHubOwnersAPI) Cancel(ctx context.Context, params *GitHubOwnersCancelRequest) (*GitHubOwnersCancelResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.cancel", params) + if err != nil { + return nil, err + } + var result GitHubOwnersCancelResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists the logins the authenticated user may act as — their own account first, then the +// organizations they belong to — by asking the GitHub API under the supplied credential. No +// credential travels in the request: `authInfo` selects one the runtime already holds, and +// the runtime resolves the token and the GitHub host from it. A failure the caller should +// render arrives as `message`; one it should raise arrives as `throwError`. +// +// RPC method: gitHubOwners.list. +// +// Parameters: Credential to list owners under, and the request id that makes the listing +// cancellable. +// +// Returns: Outcome of an owner listing. Exactly one of `owners` and `message` is present, +// except that `throwError` reports a failure the caller is expected to raise rather than +// render. +// Internal: List is part of the SDK's internal handshake/plumbing; external callers should +// not use it. +func (a *InternalServerGitHubOwnersAPI) List(ctx context.Context, params *GitHubOwnersListRequest) (*GitHubOwnersListResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.list", params) + if err != nil { + return nil, err + } + var result GitHubOwnersListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// NextRequestId registers a cancellable owner listing and returns its request id. Separate +// from `gitHubOwners.list` so the id exists before the listing starts: a caller that +// abandons the listing the moment it begins would otherwise have nothing to name in +// `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be +// released by later allocations. +// +// RPC method: gitHubOwners.nextRequestId. +// +// Returns: A freshly registered request id. Registering it before the listing starts is +// what lets a cancel that races the request still find the owner listing slot. The id +// serves one listing only. Long-abandoned unused ids can be released by later allocations. +// Internal: NextRequestId is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitHubOwnersAPI) NextRequestId(ctx context.Context) (*GitHubOwnersRequestIDResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.nextRequestId", nil) + if err != nil { + return nil, err + } + var result GitHubOwnersRequestIDResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitHubRepositoryAPI contains experimental APIs that may +// change or be removed. +type InternalServerGitHubRepositoryAPI internalServerAPI + +// AtPath resolves the GitHub repository that owns a working-tree path by reading the +// selected git remote configured for it, preferring `origin`. Returns a null `repository` +// when the path is inside a git working tree but that selected remote does not resolve to a +// GitHub host. Fails when the path is not inside a git working tree at all, so a caller can +// tell 'not a repository' apart from 'a repository with no GitHub remote'. +// +// RPC method: gitHubRepository.atPath. +// +// Parameters: Working-tree path whose owning GitHub repository should be resolved. +// +// Returns: The GitHub repository that owns the requested path, when the selected remote +// (`origin`, else the first) is on a GitHub host. +// Internal: AtPath is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGitHubRepositoryAPI) AtPath(ctx context.Context, params *GitHubRepositoryAtPathRequest) (*GitHubRepositoryAtPathResult, error) { + raw, err := a.client.Request(ctx, "gitHubRepository.atPath", params) + if err != nil { + return nil, err + } + var result GitHubRepositoryAtPathResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGlobalStateAPI contains experimental APIs that may change or +// be removed. +type InternalServerGlobalStateAPI internalServerAPI + +// Load reads the host's machine-wide state: which plugins are installed and the one-off +// flags and timestamps that record what the user has already been shown or migrated. This +// is the state that outlives a single session and a single workspace, so a host reads it to +// decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has +// already completed. The stored credentials are deliberately not part of this result; a +// caller that needs an authenticated identity asks the account methods for it instead. +// Reading is non-destructive and every field is optional, because a fresh install has +// recorded nothing yet. +// +// RPC method: globalState.load. +// +// Returns: The host's machine-wide state. Every field is optional because a fresh install +// has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Internal: Load is part of the SDK's internal handshake/plumbing; external callers should +// not use it. +func (a *InternalServerGlobalStateAPI) Load(ctx context.Context) (*GlobalStateLoadResult, error) { + raw, err := a.client.Request(ctx, "globalState.load", nil) + if err != nil { + return nil, err + } + var result GlobalStateLoadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// LoadForConfigDir reads the host's machine-wide state exactly as `globalState.load` does, +// but from a caller-supplied configuration directory instead of the one the server resolved +// for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's +// per-session `configDir` override — so the state read matches the directory that session +// actually uses. An absent or empty `configDir` resolves the server's own home, making this +// identical to `globalState.load`. The stored credentials are omitted here for the same +// reason they are omitted from `globalState.load`: a caller that needs an authenticated +// identity asks the account methods instead, so pointing this at another directory cannot +// be used to read the credentials kept in it. +// +// RPC method: globalState.loadForConfigDir. +// +// Parameters: Selects the configuration directory whose machine-wide state to read. +// +// Returns: The host's machine-wide state. Every field is optional because a fresh install +// has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Internal: LoadForConfigDir is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGlobalStateAPI) LoadForConfigDir(ctx context.Context, params *GlobalStateLoadForConfigDirRequest) (*GlobalStateLoadResult, error) { + raw, err := a.client.Request(ctx, "globalState.loadForConfigDir", params) + if err != nil { + return nil, err + } + var result GlobalStateLoadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// WriteKey records one top-level key in the host's machine-wide state, the counterpart to +// `globalState.load`. A host calls this to remember that it has shown an onboarding step, +// asked a one-off question, or completed a migration, so the next run can skip it. Only the +// named key is replaced and the rest of the document is preserved, which lets two writers +// record different flags without overwriting each other; passing no value removes the key +// instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, +// `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, +// `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every +// other key is refused, including `installedPlugins`, the stored credentials, +// `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use +// the plugin APIs, which apply repository and managed-policy checks. +// +// RPC method: globalState.writeKey. +// +// Parameters: A single top-level key to record in the host's machine-wide state. The write +// replaces only that key and leaves the rest of the document untouched, so two writers +// recording different one-off flags do not overwrite each other. The stored credential keys +// cannot be written through this method. +// Internal: WriteKey is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGlobalStateAPI) WriteKey(ctx context.Context, params *GlobalStateWriteKeyRequest) (*GlobalStateWriteKeyResult, error) { + raw, err := a.client.Request(ctx, "globalState.writeKey", params) + if err != nil { + return nil, err + } + var result GlobalStateWriteKeyResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerHostAPI contains experimental APIs that may change or be +// removed. +type InternalServerHostAPI internalServerAPI + +// CreateSession requests app-owned materialization over the owning SDK participant. +// +// RPC method: host.createSession. +// +// Parameters: One application-owned session handoff, requested by the supervised hosting +// participant. +// +// Returns: The resident session the application has materialized on its own connection. +// Internal: CreateSession is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerHostAPI) CreateSession(ctx context.Context, params *HostSessionCreateRequest) (*HostCreateSessionResult, error) { + raw, err := a.client.Request(ctx, "host.createSession", params) + if err != nil { + return nil, err + } + var result HostCreateSessionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetConfiguration returns listener settings only to the supervised hosting participant +// over its SDK connection. +// +// RPC method: host.getConfiguration. +// +// Returns: Normalized listener settings delivered only to the supervised hosting +// participant. +// Internal: GetConfiguration is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerHostAPI) GetConfiguration(ctx context.Context) (*HostGetConfigurationResult, error) { + raw, err := a.client.Request(ctx, "host.getConfiguration", nil) + if err != nil { + return nil, err + } + var result HostGetConfigurationResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetEnvironmentCredentials resolves current authenticated credentials and remote-control +// policy only for the runtime-owned Mission Control hosting participant. +// +// RPC method: host.getEnvironmentCredentials. +// +// Returns: Private credentials delivered only to a runtime-owned Mission Control hosting +// participant. +// Internal: GetEnvironmentCredentials is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerHostAPI) GetEnvironmentCredentials(ctx context.Context) (*HostGetEnvironmentCredentialsResult, error) { + raw, err := a.client.Request(ctx, "host.getEnvironmentCredentials", nil) + if err != nil { + return nil, err + } + var result HostGetEnvironmentCredentialsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Ready reports a supervised hosting participant's bound AHP endpoint after its SDK +// handshake. +// +// RPC method: host.ready. +// +// Parameters: Readiness reported by the supervised hosting participant on its own SDK +// connection. +// +// Returns: Empty acknowledgement for a completed host lifecycle operation. +// Internal: Ready is part of the SDK's internal handshake/plumbing; external callers should +// not use it. +func (a *InternalServerHostAPI) Ready(ctx context.Context, params *HostReadyRequest) (*HostReadyResult, error) { + raw, err := a.client.Request(ctx, "host.ready", params) + if err != nil { + return nil, err + } + var result HostReadyResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ReleaseSession releases app ownership retention after AHP detaches. +// +// RPC method: host.releaseSession. +// +// Parameters: Ends one participation, not the application's session lifetime. +// +// Returns: Empty acknowledgement for a completed host lifecycle operation. +// Internal: ReleaseSession is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerHostAPI) ReleaseSession(ctx context.Context, params *HostSessionReleaseRequest) (*HostReleaseSessionResult, error) { + raw, err := a.client.Request(ctx, "host.releaseSession", params) + if err != nil { + return nil, err + } + var result HostReleaseSessionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalServerSessionsAPI contains experimental APIs that may change or be // removed. type InternalServerSessionsAPI internalServerAPI @@ -25223,6 +30473,33 @@ func (a *InternalServerSessionsAPI) ConfigureSessionExtensions(ctx context.Conte return &result, nil } +// CreateWorkspace creates the workspace record for a session that has not been opened yet. +// A host that hands a session off to another application — writing the record and then +// launching that application against the session ID — needs the record on disk before any +// session exists to carry it, which the session-scoped workspace methods cannot do. +// Replaces any existing record and resets the checkpoint index. When writing to the local +// filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a +// surviving stored `fork_count` can differ from the answer. +// +// RPC method: sessions.createWorkspace. +// +// Parameters: Identity, state location and starting context for a workspace record. +// +// Returns: The workspace record that was written. +// Internal: CreateWorkspace is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerSessionsAPI) CreateWorkspace(ctx context.Context, params *SessionsCreateWorkspaceRequest) (*SessionsCreateWorkspaceResult, error) { + raw, err := a.client.Request(ctx, "sessions.createWorkspace", params) + if err != nil { + return nil, err + } + var result SessionsCreateWorkspaceResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Deletes one local session from disk after running the same lifecycle hooks as the session // manager. // @@ -25363,13 +30640,73 @@ func (a *InternalServerSessionsAPI) ListNonEmptySessionIds(ctx context.Context, return &result, nil } +// LoadWorkspace reads a session's workspace record straight from disk, without opening the +// session. Resuming by session ID has to know where the session lives before it can +// connect, so the lookup cannot come from the session-scoped workspace methods, which +// resolve their location from a live session's context. Returns no record when the file is +// absent. +// +// RPC method: sessions.loadWorkspace. +// +// Parameters: Where the session's state lives, as a root directory and the session ID under +// it. +// +// Returns: The workspace record on disk, omitted when the session has none. +// Internal: LoadWorkspace is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerSessionsAPI) LoadWorkspace(ctx context.Context, params *SessionsLoadWorkspaceRequest) (*SessionsLoadWorkspaceResult, error) { + raw, err := a.client.Request(ctx, "sessions.loadWorkspace", params) + if err != nil { + return nil, err + } + var result SessionsLoadWorkspaceResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// UpdateWorkspaceFields merges fields into a session's workspace record on disk, creating +// the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same +// before-the-session-exists case. It preserves stored workspace-schema fields the request +// does not supply, does not preserve stored keys outside the workspace schema, and never +// replaces a stored `fork_count`. +// +// RPC method: sessions.updateWorkspaceFields. +// +// Parameters: Where the session's state lives, plus workspace-schema fields to merge into +// its workspace record. Stored keys outside the schema are not preserved, and a stored +// `fork_count` is never replaced. +// +// Returns: The merge completed. The record carries the supplied workspace-schema fields, +// but a stored `fork_count` stays. +// Internal: UpdateWorkspaceFields is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerSessionsAPI) UpdateWorkspaceFields(ctx context.Context, params *SessionsUpdateWorkspaceFieldsRequest) (*SessionsUpdateWorkspaceFieldsResult, error) { + raw, err := a.client.Request(ctx, "sessions.updateWorkspaceFields", params) + if err != nil { + return nil, err + } + var result SessionsUpdateWorkspaceFieldsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // InternalServerRPC provides internal SDK server-scoped RPC methods (handshake helpers // etc.). Not part of the public API. type InternalServerRPC struct { // Reuse a single struct instead of allocating one for each service on the heap. common internalServerAPI - Sessions *InternalServerSessionsAPI + Agents *InternalServerAgentsAPI + Git *InternalServerGitAPI + GitHubOwners *InternalServerGitHubOwnersAPI + GitHubRepository *InternalServerGitHubRepositoryAPI + GlobalState *InternalServerGlobalStateAPI + Host *InternalServerHostAPI + Sessions *InternalServerSessionsAPI } // Connect performs the SDK server connection handshake and validates the optional @@ -25404,6 +30741,12 @@ func (a *InternalServerRPC) Connect(ctx context.Context, params *ConnectRequest) func NewInternalServerRPC(client *jsonrpc2.Client) *InternalServerRPC { r := &InternalServerRPC{} r.common = internalServerAPI{client: client} + r.Agents = (*InternalServerAgentsAPI)(&r.common) + r.Git = (*InternalServerGitAPI)(&r.common) + r.GitHubOwners = (*InternalServerGitHubOwnersAPI)(&r.common) + r.GitHubRepository = (*InternalServerGitHubRepositoryAPI)(&r.common) + r.GlobalState = (*InternalServerGlobalStateAPI)(&r.common) + r.Host = (*InternalServerHostAPI)(&r.common) r.Sessions = (*InternalServerSessionsAPI)(&r.common) return r } @@ -25413,6 +30756,165 @@ type sessionAPI struct { sessionID string } +// Experimental: AccountsAPI contains experimental APIs that may change or be removed. +type AccountsAPI sessionAPI + +// Enumerate a typed accounts collection: the signed-in accounts, or the providers offered +// for interactive login. +// +// RPC method: session.accounts.enumerate. +// +// Parameters: Enumerate request carrying the typed collection query. +// +// Returns: The enumerated collection, keyed by the same selector as the query. +func (a *AccountsAPI) Enumerate(ctx context.Context, params *AccountsEnumerateRequest) (AuthEnumerateValue, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["query"] = params.Query + } + raw, err := a.client.Request(ctx, "session.accounts.enumerate", req) + if err != nil { + return nil, err + } + result, err := unmarshalAuthEnumerateValue(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Get read one typed accounts datum: the active account, a neutral status summary, or the +// last authentication errors. +// +// RPC method: session.accounts.get. +// +// Parameters: Read request carrying the typed datum query. +// +// Returns: The read result, keyed by the same selector as the query. +func (a *AccountsAPI) Get(ctx context.Context, params *AccountsGetRequest) (AuthReadValue, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["query"] = params.Query + } + raw, err := a.client.Request(ctx, "session.accounts.get", req) + if err != nil { + return nil, err + } + result, err := unmarshalAuthReadValue(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Set apply one non-interactive accounts mutation: switch the active account, log an +// account out, or set credentials from a token. +// +// RPC method: session.accounts.set. +// +// Parameters: Mutation request carrying the typed write command. +// +// Returns: Result of a non-interactive accounts mutation. +func (a *AccountsAPI) Set(ctx context.Context, params *AccountsSetRequest) (*AuthWriteResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["command"] = params.Command + } + raw, err := a.client.Request(ctx, "session.accounts.set", req) + if err != nil { + return nil, err + } + var result AuthWriteResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: AccountsLoginAPI contains experimental APIs that may change or be removed. +type AccountsLoginAPI sessionAPI + +// Advance an in-flight login flow, optionally fulfilling an input-required step, and return +// the next step. +// +// RPC method: session.accounts.login.advance. +// +// Parameters: Advance an in-flight login flow, optionally fulfilling an input-required step. +// +// 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). +func (a *AccountsLoginAPI) Advance(ctx context.Context, params *AuthLoginAdvanceRequest) (AuthLoginStep, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["flowId"] = params.FlowID + if params.Input != nil { + req["input"] = *params.Input + } + } + raw, err := a.client.Request(ctx, "session.accounts.login.advance", req) + if err != nil { + return nil, err + } + result, err := unmarshalAuthLoginStep(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Begin an interactive login flow for a provider kind (dispatch is kind-only) and return +// its opaque flow id and first step. +// +// RPC method: session.accounts.login.begin. +// +// Parameters: Begin an interactive login flow for a provider kind. Dispatch is kind-only. +// +// Returns: A started login flow: its opaque id and first step. +func (a *AccountsLoginAPI) Begin(ctx context.Context, params *AuthLoginBeginRequest) (*AuthLoginBegun, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["kind"] = params.Kind + } + raw, err := a.client.Request(ctx, "session.accounts.login.begin", req) + if err != nil { + return nil, err + } + var result AuthLoginBegun + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Cancel an in-flight login flow and release its resources. +// +// RPC method: session.accounts.login.cancel. +// +// Parameters: Cancel an in-flight login flow. +func (a *AccountsLoginAPI) Cancel(ctx context.Context, params *AuthLoginCancelRequest) (*SessionAccountsLoginCancelResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["flowId"] = params.FlowID + } + raw, err := a.client.Request(ctx, "session.accounts.login.cancel", req) + if err != nil { + return nil, err + } + var result SessionAccountsLoginCancelResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Login returns experimental APIs that may change or be removed. +func (s *AccountsAPI) Login() *AccountsLoginAPI { + return (*AccountsLoginAPI)(s) +} + // Experimental: AgentAPI contains experimental APIs that may change or be removed. type AgentAPI sessionAPI @@ -26002,6 +31504,24 @@ func (a *ConnectorsAPI) Disconnect(ctx context.Context, params *ConnectorConnect return &result, nil } +// GetAccount returns the session account selection, or null. +// +// RPC method: session.connectors.getAccount. +// +// Returns: Session account selection, or null. +func (a *ConnectorsAPI) GetAccount(ctx context.Context) (*ConnectorSessionAccount, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.connectors.getAccount", req) + if err != nil { + return nil, err + } + var result *ConnectorSessionAccount + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return result, nil +} + // GetCapabilities returns feature availability and bounded polling limits for the // EXPERIMENTAL session connector API. This method never performs a Connector service // request. @@ -26082,6 +31602,9 @@ func (a *ConnectorsAPI) Reconcile(ctx context.Context, params *ConnectorReconcil req := map[string]any{"sessionId": a.sessionID} if params != nil { req["accountId"] = params.AccountID + if params.ForceConnectorName != nil { + req["forceConnectorName"] = *params.ForceConnectorName + } if params.RefreshCatalog != nil { req["refreshCatalog"] = *params.RefreshCatalog } @@ -26182,21 +31705,24 @@ func (a *ContentExclusionAPI) CheckPaths(ctx context.Context, params *ContentExc // Experimental: CustomizationsAPI contains experimental APIs that may change or be removed. type CustomizationsAPI sessionAPI -// Reloads all repository and user customizations for the active session: instructions, -// plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns -// diagnostics from the final skill reload. +// Reload for local sessions, reconciles repository context and discovered instructions, +// plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change +// under the working directory. Independent component failures are returned in outcomes and +// errors; a rejected call can have partially applied earlier steps. Remote sessions must +// reload on their agent host instead. The model-facing context is rebuilt on the next turn. // // RPC method: session.customizations.reload. // -// Returns: Diagnostics from reloading skill definitions, with warnings and errors as -// separate lists. -func (a *CustomizationsAPI) Reload(ctx context.Context) (*SkillsLoadDiagnostics, error) { +// Returns: Results of reloading discovered session customizations. Inspect outcomes for +// reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes +// to the model-facing prompt and tools apply on the next turn. +func (a *CustomizationsAPI) Reload(ctx context.Context) (*CustomizationsReloadResult, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.customizations.reload", req) if err != nil { return nil, err } - var result SkillsLoadDiagnostics + var result CustomizationsReloadResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } @@ -26525,336 +32051,6 @@ func (a *ExtensionsAPI) SendAttachmentsToMessage(ctx context.Context, params *Se return &result, nil } -// Experimental: FactoryAPI contains experimental APIs that may change or be removed. -type FactoryAPI sessionAPI - -// Agent runs one factory-scoped subagent and returns its result. -// -// RPC method: session.factory.agent. -// -// Parameters: Parameters for one factory-scoped subagent call. -// -// Returns: Result of one factory-scoped subagent call. -func (a *FactoryAPI) Agent(ctx context.Context, params *FactoryAgentRequest) (*FactoryAgentResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["executionToken"] = params.ExecutionToken - req["factoryRunId"] = params.FactoryRunID - req["opts"] = params.Opts - req["prompt"] = params.Prompt - } - raw, err := a.client.Request(ctx, "session.factory.agent", req) - if err != nil { - return nil, err - } - var result FactoryAgentResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Cancel requests cancellation of a factory run and returns its run envelope. -// -// RPC method: session.factory.cancel. -// -// Parameters: Parameters for cancelling a factory run. -// -// Returns: Complete current or terminal factory run envelope. -func (a *FactoryAPI) Cancel(ctx context.Context, params *FactoryCancelRequest) (*FactoryRunResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.cancel", req) - if err != nil { - return nil, err - } - var result FactoryRunResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// GetRun gets the current or settled envelope for a factory run. -// -// RPC method: session.factory.getRun. -// -// Parameters: Parameters for retrieving a factory run. -// -// Returns: Complete current or terminal factory run envelope. -func (a *FactoryAPI) GetRun(ctx context.Context, params *FactoryGetRunRequest) (*FactoryRunResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.getRun", req) - if err != nil { - return nil, err - } - var result FactoryRunResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// GetRunDetail gets durable and live observability detail for one factory run. -// -// RPC method: session.factory.getRunDetail. -// -// Parameters: Parameters for retrieving a factory run. -// -// Returns: Full factory run observability detail. -func (a *FactoryAPI) GetRunDetail(ctx context.Context, params *FactoryGetRunRequest) (*FactoryRunDetail, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.getRunDetail", req) - if err != nil { - return nil, err - } - var result FactoryRunDetail - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// GetRunProgress pages durable progress for one factory run. -// -// RPC method: session.factory.getRunProgress. -// -// Parameters: Parameters for paging factory progress. -// -// Returns: A bidirectional page of factory progress. -func (a *FactoryAPI) GetRunProgress(ctx context.Context, params *FactoryGetRunProgressRequest) (*FactoryProgressPage, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - if params.AfterSeq != nil { - req["afterSeq"] = *params.AfterSeq - } - if params.BeforeSeq != nil { - req["beforeSeq"] = *params.BeforeSeq - } - if params.Limit != nil { - req["limit"] = *params.Limit - } - if params.PhaseID != nil { - req["phaseId"] = *params.PhaseID - } - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.getRunProgress", req) - if err != nil { - return nil, err - } - var result FactoryProgressPage - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// ListRuns lists durable factory runs for this session in creation order. -// -// RPC method: session.factory.listRuns. -// -// Parameters: Parameters for paging factory runs. -// -// Returns: A page of factory runs in durable creation order. -func (a *FactoryAPI) ListRuns(ctx context.Context, params *FactoryListRunsRequest) (*FactoryListRunsResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - if params.AfterSeq != nil { - req["afterSeq"] = *params.AfterSeq - } - if params.BeforeSeq != nil { - req["beforeSeq"] = *params.BeforeSeq - } - if params.Limit != nil { - req["limit"] = *params.Limit - } - } - raw, err := a.client.Request(ctx, "session.factory.listRuns", req) - if err != nil { - return nil, err - } - var result FactoryListRunsResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Log records a batch of ordered factory progress lines. -// -// RPC method: session.factory.log. -// -// Parameters: Parameters for recording factory progress. -// -// Returns: Acknowledgement that a factory request was accepted. -func (a *FactoryAPI) Log(ctx context.Context, params *FactoryLogRequest) (*FactoryAckResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["executionToken"] = params.ExecutionToken - req["lines"] = params.Lines - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.log", req) - if err != nil { - return nil, err - } - var result FactoryAckResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Pauses a running factory and returns its settled run envelope. -// -// RPC method: session.factory.pause. -// -// Parameters: Parameters for pausing a running factory. -// -// Returns: Complete current or terminal factory run envelope. -func (a *FactoryAPI) Pause(ctx context.Context, params *FactoryPauseRequest) (*FactoryRunResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.pause", req) - if err != nil { - return nil, err - } - var result FactoryRunResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Resumes a factory run using its persisted name, arguments, journal, and accounting. -// -// RPC method: session.factory.resume. -// -// Parameters: Parameters for resuming a factory run from its persisted identity. -// -// Returns: Resolved persisted factory identity and resumed run envelope. -func (a *FactoryAPI) Resume(ctx context.Context, params *FactoryResumeRequest) (*FactoryResumeResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - if params.Limits != nil { - req["limits"] = *params.Limits - } - if params.LogPhaseNames != nil { - req["logPhaseNames"] = *params.LogPhaseNames - } - if params.NotifyOnComplete != nil { - req["notifyOnComplete"] = *params.NotifyOnComplete - } - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.resume", req) - if err != nil { - return nil, err - } - var result FactoryResumeResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Runs a registered factory by name at the top level. -// -// RPC method: session.factory.run. -// -// Parameters: Parameters for invoking a registered factory. -// -// Returns: Complete current or terminal factory run envelope. -func (a *FactoryAPI) Run(ctx context.Context, params *FactoryRunRequest) (*FactoryRunResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["args"] = params.Args - req["name"] = params.Name - if params.Options != nil { - req["options"] = *params.Options - } - } - raw, err := a.client.Request(ctx, "session.factory.run", req) - if err != nil { - return nil, err - } - var result FactoryRunResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Experimental: FactoryJournalAPI contains experimental APIs that may change or be removed. -type FactoryJournalAPI sessionAPI - -// Get reads a memoized factory journal entry. -// -// RPC method: session.factory.journal.get. -// -// Parameters: Parameters for reading a factory journal entry. -// -// Returns: Result of reading a factory journal entry. -func (a *FactoryJournalAPI) Get(ctx context.Context, params *FactoryJournalGetRequest) (*FactoryJournalGetResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["executionToken"] = params.ExecutionToken - req["key"] = params.Key - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.journal.get", req) - if err != nil { - return nil, err - } - var result FactoryJournalGetResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Put stores a memoized factory journal entry. -// -// RPC method: session.factory.journal.put. -// -// Parameters: Parameters for storing a factory journal entry. -// -// Returns: Acknowledgement that a factory request was accepted. -func (a *FactoryJournalAPI) Put(ctx context.Context, params *FactoryJournalPutRequest) (*FactoryAckResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["executionToken"] = params.ExecutionToken - req["key"] = params.Key - req["resultJson"] = params.ResultJSON - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.journal.put", req) - if err != nil { - return nil, err - } - var result FactoryAckResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// Experimental: Journal returns experimental APIs that may change or be removed. -func (s *FactoryAPI) Journal() *FactoryJournalAPI { - return (*FactoryJournalAPI)(s) -} - // Experimental: FleetAPI contains experimental APIs that may change or be removed. type FleetAPI sessionAPI @@ -27187,17 +32383,20 @@ func (a *InstructionsAPI) GetSources(ctx context.Context) (*InstructionsGetSourc return &result, nil } -// Reload invalidates cached custom-instruction discovery so subsequent turns and source -// reads observe instruction files currently on disk. +// Reload for local sessions, invalidates instruction discovery and the model-facing prompt, +// then returns freshly discovered sources. The updated prompt takes effect on the next +// turn. Remote sessions must reload on their agent host instead. // // RPC method: session.instructions.reload. -func (a *InstructionsAPI) Reload(ctx context.Context) (*SessionInstructionsReloadResult, error) { +// +// Returns: Instruction sources loaded for the session, in merge order. +func (a *InstructionsAPI) Reload(ctx context.Context) (*InstructionsGetSourcesResult, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.instructions.reload", req) if err != nil { return nil, err } - var result SessionInstructionsReloadResult + var result InstructionsGetSourcesResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } @@ -27284,16 +32483,12 @@ type ManagedSettingsAPI sessionAPI // // RPC method: session.managedSettings.get. // -// 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. func (a *ManagedSettingsAPI) Get(ctx context.Context) (*ManagedSettingsResolvedData, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.managedSettings.get", req) @@ -27342,6 +32537,9 @@ func (a *MCPAPI) CancelSamplingExecution(ctx context.Context, params *MCPCancelS func (a *MCPAPI) Disable(ctx context.Context, params *MCPDisableRequest) (*SessionMCPDisableResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { + if params.ExpectedInstallationID != nil { + req["expectedInstallationId"] = *params.ExpectedInstallationID + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.disable", req) @@ -27363,6 +32561,9 @@ func (a *MCPAPI) Disable(ctx context.Context, params *MCPDisableRequest) (*Sessi func (a *MCPAPI) Enable(ctx context.Context, params *MCPEnableRequest) (*SessionMCPEnableResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { + if params.ExpectedInstallationID != nil { + req["expectedInstallationId"] = *params.ExpectedInstallationID + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.enable", req) @@ -27428,10 +32629,8 @@ func (a *MCPAPI) IsServerRunning(ctx context.Context, params *MCPIsServerRunning return &result, nil } -// Lists MCP servers configured for the session, their connection status, and host-level -// state. The host-level state (disabled/filtered servers, failed/needs-auth/pending -// connections, mcp3p policy, full config) is empty/zero when no MCP host has been -// initialized for the session. +// Lists materialized MCP servers and their connection status. Cache misses may start and +// wait for MCP servers. // // RPC method: session.mcp.list. // @@ -27450,6 +32649,27 @@ func (a *MCPAPI) List(ctx context.Context) (*MCPServerList, error) { return &result, nil } +// ListConfigured lists effective MCP configuration without starting, restarting, +// authenticating, or waiting for servers. An optional live observation is from an already +// materialized matching server; this is not a readiness guarantee. +// +// RPC method: session.mcp.listConfigured. +// +// Returns: Effective MCP configuration with optional live observations from matching +// already materialized servers. +func (a *MCPAPI) ListConfigured(ctx context.Context) (*MCPConfiguredServerList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.mcp.listConfigured", req) + if err != nil { + return nil, err + } + var result MCPConfiguredServerList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // ListTools lists the tools exposed by a connected MCP server on this session's host. This // performs a live `tools/list` request. Tool UI metadata is returned independently of // whether MCP Apps rendering is enabled for the session. @@ -27547,6 +32767,9 @@ func (a *MCPAPI) RestartServer(ctx context.Context, params *MCPRestartServerRequ if params.Config != nil { req["config"] = params.Config } + if params.ExpectedInstallationID != nil { + req["expectedInstallationId"] = *params.ExpectedInstallationID + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.restartServer", req) @@ -27604,6 +32827,9 @@ func (a *MCPAPI) StartServer(ctx context.Context, params *MCPStartServerRequest) if params.Config != nil { req["config"] = params.Config } + if params.ExpectedInstallationID != nil { + req["expectedInstallationId"] = *params.ExpectedInstallationID + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.startServer", req) @@ -27625,6 +32851,9 @@ func (a *MCPAPI) StartServer(ctx context.Context, params *MCPStartServerRequest) func (a *MCPAPI) StopServer(ctx context.Context, params *MCPStopServerRequest) (*SessionMCPStopServerResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { + if params.ExpectedInstallationID != nil { + req["expectedInstallationId"] = *params.ExpectedInstallationID + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.stopServer", req) @@ -27856,6 +33085,56 @@ func (a *MCPOauthAPI) AuthenticationStateChanged(ctx context.Context, params *MC return &result, nil } +// CancelLogin cancels the exact owned OAuth login issued to this original session +// requester, without clearing shared credentials. +// +// RPC method: session.mcp.oauth.cancelLogin. +// +// Parameters: Targets only the original prepared/applying owned login on this exact session +// requester. +// +// Returns: Honest terminal cancellation result; persistence or recovery failures remain RPC +// errors. +func (a *MCPOauthAPI) CancelLogin(ctx context.Context, params *SessionMCPOauthCancelLoginRequest) (*SessionMCPOauthCancelLoginResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["expectedInstallationId"] = params.ExpectedInstallationID + req["loginId"] = params.LoginID + } + raw, err := a.client.Request(ctx, "session.mcp.oauth.cancelLogin", req) + if err != nil { + return nil, err + } + var result SessionMCPOauthCancelLoginResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Completes a runtime-managed MCP OAuth login after the authorization server redirects to a +// host-managed callback URL. +// +// RPC method: session.mcp.oauth.complete. +// +// Parameters: Host-delivered callback for a runtime-managed MCP OAuth login. +func (a *MCPOauthAPI) Complete(ctx context.Context, params *MCPOauthCompleteRequest) (*SessionMCPOauthCompleteResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["authorizationId"] = params.AuthorizationID + req["callbackUrl"] = params.CallbackURL + } + raw, err := a.client.Request(ctx, "session.mcp.oauth.complete", req) + if err != nil { + return nil, err + } + var result SessionMCPOauthCompleteResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // HandlePendingRequest resolves a pending MCP OAuth request with a host-provided token or // cancellation. The pending request is emitted as mcp.oauth_required with the data // necessary to authorize the request. @@ -27882,12 +33161,14 @@ func (a *MCPOauthAPI) HandlePendingRequest(ctx context.Context, params *MCPOauth return &result, nil } -// Login starts OAuth authentication for a remote MCP server. +// Login 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. // // RPC method: session.mcp.oauth.login. // // Parameters: Remote MCP server name and optional overrides controlling reauthentication, -// OAuth client display name, callback success-page copy, and static OAuth client selection. +// OAuth client display name, callback handling, and static OAuth client selection. // // Returns: OAuth authorization URL the caller should open, or empty when cached tokens // already authenticated the server. @@ -27906,15 +33187,24 @@ func (a *MCPOauthAPI) Login(ctx context.Context, params *MCPOauthLoginRequest) ( if params.ClientSecret != nil { req["clientSecret"] = *params.ClientSecret } + if params.ExpectedInstallationID != nil { + req["expectedInstallationId"] = *params.ExpectedInstallationID + } if params.ForceReauth != nil { req["forceReauth"] = *params.ForceReauth } if params.GrantType != nil { req["grantType"] = *params.GrantType } + if params.LoginID != nil { + req["loginId"] = *params.LoginID + } if params.PublicClient != nil { req["publicClient"] = *params.PublicClient } + if params.RedirectURI != nil { + req["redirectUri"] = *params.RedirectURI + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.oauth.login", req) @@ -27928,6 +33218,43 @@ func (a *MCPOauthAPI) Login(ctx context.Context, params *MCPOauthLoginRequest) ( return &result, nil } +// PrepareLogin 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. +// +// RPC method: session.mcp.oauth.prepareLogin. +// +// Parameters: Effect-free preparation bound to the existing local session, requester and +// installation, with frozen options. +// +// Returns: An inert runtime-issued login handle. Preparation alone performs no activation +// or OAuth work. +func (a *MCPOauthAPI) PrepareLogin(ctx context.Context, params *SessionMCPOauthPrepareLoginRequest) (*SessionMCPOauthPrepareLoginResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.CallbackSuccessMessage != nil { + req["callbackSuccessMessage"] = *params.CallbackSuccessMessage + } + if params.ClientName != nil { + req["clientName"] = *params.ClientName + } + req["expectedInstallationId"] = params.ExpectedInstallationID + if params.ForceReauth != nil { + req["forceReauth"] = *params.ForceReauth + } + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.oauth.prepareLogin", req) + if err != nil { + return nil, err + } + var result SessionMCPOauthPrepareLoginResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // 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. @@ -27944,6 +33271,9 @@ func (a *MCPOauthAPI) Login(ctx context.Context, params *MCPOauthLoginRequest) ( func (a *MCPOauthAPI) Probe(ctx context.Context, params *MCPOauthProbeRequest) (MCPOauthProbeResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { + if params.ExpectedInstallationID != nil { + req["expectedInstallationId"] = *params.ExpectedInstallationID + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.oauth.probe", req) @@ -27985,6 +33315,70 @@ func (s *MCPAPI) Oauth() *MCPOauthAPI { return (*MCPOauthAPI)(s) } +// Experimental: MCPPromptsAPI contains experimental APIs that may change or be removed. +type MCPPromptsAPI sessionAPI + +// Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content +// is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch +// referenced resources. +// +// RPC method: session.mcp.prompts.get. +// +// Parameters: MCP server, prompt name, and optional string-valued arguments. +// +// Returns: Prompt messages returned by the MCP server without sending them to the model. +func (a *MCPPromptsAPI) Get(ctx context.Context, params *MCPPromptsGetRequest) (*MCPPromptsGetResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Arguments != nil { + req["arguments"] = params.Arguments + } + req["promptName"] = params.PromptName + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.prompts.get", req) + if err != nil { + return nil, err + } + var result MCPPromptsGetResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// List enumerate one page of prompts a connected MCP server exposes (proxies MCP +// `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. +// +// RPC method: session.mcp.prompts.list. +// +// Parameters: MCP server whose prompts to enumerate. +// +// Returns: One page of prompts advertised by the named MCP server. +func (a *MCPPromptsAPI) List(ctx context.Context, params *MCPPromptsListRequest) (*MCPPromptsListResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Cursor != nil { + req["cursor"] = *params.Cursor + } + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.prompts.list", req) + if err != nil { + return nil, err + } + var result MCPPromptsListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Prompts returns experimental APIs that may change or be removed. +func (s *MCPAPI) Prompts() *MCPPromptsAPI { + return (*MCPPromptsAPI)(s) +} + // Experimental: MCPResourcesAPI contains experimental APIs that may change or be removed. type MCPResourcesAPI sessionAPI @@ -30030,6 +35424,177 @@ func (a *ProviderAPI) Sync(ctx context.Context, params *ProviderSyncRequest) (*P return &result, nil } +// 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. +// +// RPC method: session.provider.withdraw. +// +// Parameters: Host-managed model selection ids to withdraw from the session's BYOK registry. +// +// Returns: What the withdrawal actually removed from the registry. +func (a *ProviderAPI) Withdraw(ctx context.Context, params *ProviderWithdrawRequest) (*ProviderWithdrawResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["models"] = params.Models + } + raw, err := a.client.Request(ctx, "session.provider.withdraw", req) + if err != nil { + return nil, err + } + var result ProviderWithdrawResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ProvidersAPI contains experimental APIs that may change or be removed. +type ProvidersAPI sessionAPI + +// Discovers reachable instances using an adapter from this session's effective provider +// catalog and provider-specific discovery input. +// +// RPC method: session.providers.discover. +// +// Parameters: Provider discovery parameters. +// +// Returns: Provider instances found by a discovery operation. +func (a *ProvidersAPI) Discover(ctx context.Context, params *ModelProviderDiscoverRequest) (*ModelProviderDiscoverResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["adapterId"] = params.AdapterID + if params.Input != nil { + req["input"] = params.Input + } + } + raw, err := a.client.Request(ctx, "session.providers.discover", req) + if err != nil { + return nil, err + } + var result ModelProviderDiscoverResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetCatalog returns adapter definitions and supported operations in this session's +// effective provider catalog, without running discovery. Does not list provider instances +// or select inference models. +// +// RPC method: session.providers.getCatalog. +// +// Returns: Normalized model-provider adapter definitions available to the session, not +// discovered instances. +func (a *ProvidersAPI) GetCatalog(ctx context.Context) (*ModelProviderAdapterCatalog, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.providers.getCatalog", req) + if err != nil { + return nil, err + } + var result ModelProviderAdapterCatalog + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetStatus gets current health and version information for a discovered model-provider +// instance. +// +// RPC method: session.providers.getStatus. +// +// Parameters: Provider status request parameters. +// +// Returns: Current health information for a provider instance. +func (a *ProvidersAPI) GetStatus(ctx context.Context, params *ModelProviderGetStatusRequest) (*ModelProviderStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + } + raw, err := a.client.Request(ctx, "session.providers.getStatus", req) + if err != nil { + return nil, err + } + var result ModelProviderStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ProvidersModelsAPI contains experimental APIs that may change or be removed. +type ProvidersModelsAPI sessionAPI + +// Lists models installed or otherwise available from a discovered model-provider instance. +// +// RPC method: session.providers.models.list. +// +// Parameters: Provider model inventory request parameters. +// +// Returns: Models offered for agent conversations by one provider instance. Adapters +// exclude known-incompatible models, but retain candidates with unknown capabilities. +// Listing does not guarantee compatibility. +func (a *ProvidersModelsAPI) List(ctx context.Context, params *ModelProviderModelsListRequest) (*DiscoveredModelList, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + } + raw, err := a.client.Request(ctx, "session.providers.models.list", req) + if err != nil { + return nil, err + } + var result DiscoveredModelList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// PrepareConfiguration translates a discovered model into the provider and model +// configuration needed to use it, and reports whether each is already registered in this +// session. Prepares only: it registers nothing, writes nothing, and performs no provider +// requests. +// +// RPC method: session.providers.models.prepareConfiguration. +// +// Parameters: A discovered instance and one of its models to translate into provider +// configuration. Pass back the instance and model as returned by +// `session.providers.discover` and `session.providers.models.list`. +// +// Returns: Provider configuration prepared from a discovered model. Preparing a plan +// changes nothing: it neither registers the model with the session nor writes durable +// configuration. To apply it, pass `provider` and `model` to `session.provider.add`, +// omitting whichever the dispositions report as already configured. +func (a *ProvidersModelsAPI) PrepareConfiguration(ctx context.Context, params *ModelProviderPrepareConfigurationRequest) (*ModelProviderConfigurationPlan, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + req["model"] = params.Model + } + raw, err := a.client.Request(ctx, "session.providers.models.prepareConfiguration", req) + if err != nil { + return nil, err + } + var result ModelProviderConfigurationPlan + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Models returns experimental APIs that may change or be removed. +func (s *ProvidersAPI) Models() *ProvidersModelsAPI { + return (*ProvidersModelsAPI)(s) +} + // Experimental: QueueAPI contains experimental APIs that may change or be removed. type QueueAPI sessionAPI @@ -30441,6 +36006,38 @@ func (a *SandboxAPI) GetEnforcementStatus(ctx context.Context) (*SandboxEnforcem return &result, nil } +// GrantPathForRequest 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. +// +// RPC method: session.sandbox.grantPathForRequest. +// +// Parameters: Request to accept the sandbox path grant offered on an active sandbox +// escalation permission prompt. +// +// Returns: Result of accepting a sandbox path grant. +func (a *SandboxAPI) GrantPathForRequest(ctx context.Context, params *SandboxGrantPathForRequestRequest) (*SandboxGrantPathForRequestResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.DecisionContext != nil { + req["decisionContext"] = *params.DecisionContext + } + req["requestId"] = params.RequestID + } + raw, err := a.client.Request(ctx, "session.sandbox.grantPathForRequest", req) + if err != nil { + return nil, err + } + var result SandboxGrantPathForRequestResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ScheduleAPI contains experimental APIs that may change or be removed. type ScheduleAPI sessionAPI @@ -30512,7 +36109,7 @@ func (a *ShellAPI) CancelUserRequested(ctx context.Context, params *ShellCancelU return &result, nil } -// Exec starts a shell command and streams output through session notifications. The command +// Exec starts a shell command, returning an RPC error if it cannot be spawned. The command // runs as the leader of its own process group (POSIX) or in a dedicated job object // (Windows), so a forced termination — via "shell.kill", the request timeout, or session // disposal — signals that whole group/job rather than only the direct child. Two gaps are @@ -30524,10 +36121,10 @@ func (a *ShellAPI) CancelUserRequested(ctx context.Context, params *ShellCancelU // RPC method: session.shell.exec. // // Parameters: Shell command to run, with optional working directory and timeout in -// milliseconds. +// milliseconds. Spawn failures return an RPC error. // -// Returns: Identifier of the spawned process, used to correlate streamed output and exit -// notifications. +// Returns: Identifier of the spawned shell process, usable with shell.kill while the +// process is running. func (a *ShellAPI) Exec(ctx context.Context, params *ShellExecRequest) (*ShellExecResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { @@ -32450,6 +38047,7 @@ type SessionRPC struct { // Reuse a single struct instead of allocating one for each service on the heap. common sessionAPI + Accounts *AccountsAPI Agent *AgentAPI AutopilotObjective *AutopilotObjectiveAPI Canvas *CanvasAPI @@ -32462,7 +38060,6 @@ type SessionRPC struct { Diagnostics *DiagnosticsAPI EventLog *EventLogAPI Extensions *ExtensionsAPI - Factory *FactoryAPI Fleet *FleetAPI GitHubAuth *GitHubAuthAPI History *HistoryAPI @@ -32480,6 +38077,7 @@ type SessionRPC struct { Plan *PlanAPI Plugins *PluginsAPI Provider *ProviderAPI + Providers *ProvidersAPI Queue *QueueAPI Remote *RemoteAPI Sandbox *SandboxAPI @@ -32787,6 +38385,7 @@ func (a *SessionRPC) Suspend(ctx context.Context) (*SessionSuspendResult, error) func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r := &SessionRPC{} r.common = sessionAPI{client: client, sessionID: sessionID} + r.Accounts = (*AccountsAPI)(&r.common) r.Agent = (*AgentAPI)(&r.common) r.AutopilotObjective = (*AutopilotObjectiveAPI)(&r.common) r.Canvas = (*CanvasAPI)(&r.common) @@ -32799,7 +38398,6 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Diagnostics = (*DiagnosticsAPI)(&r.common) r.EventLog = (*EventLogAPI)(&r.common) r.Extensions = (*ExtensionsAPI)(&r.common) - r.Factory = (*FactoryAPI)(&r.common) r.Fleet = (*FleetAPI)(&r.common) r.GitHubAuth = (*GitHubAuthAPI)(&r.common) r.History = (*HistoryAPI)(&r.common) @@ -32817,6 +38415,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Plan = (*PlanAPI)(&r.common) r.Plugins = (*PluginsAPI)(&r.common) r.Provider = (*ProviderAPI)(&r.common) + r.Providers = (*ProvidersAPI)(&r.common) r.Queue = (*QueueAPI)(&r.common) r.Remote = (*RemoteAPI)(&r.common) r.Sandbox = (*SandboxAPI)(&r.common) @@ -32986,97 +38585,6 @@ func (a *InternalConnectorsAPI) WithdrawProjection(ctx context.Context) (*Connec return &result, nil } -// Experimental: InternalFactoryAPI contains experimental APIs that may change or be removed. -type InternalFactoryAPI internalSessionAPI - -// PauseAtCheckpoint atomically pauses an owned factory attempt at a durable checkpoint. -// -// RPC method: session.factory.pauseAtCheckpoint. -// -// Parameters: Parameters for an owned durable pause checkpoint. -// Internal: PauseAtCheckpoint is part of the SDK's internal handshake/plumbing; external -// callers should not use it. -func (a *InternalFactoryAPI) PauseAtCheckpoint(ctx context.Context, params *FactoryPauseCheckpointRequest) (*SessionFactoryPauseAtCheckpointResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["executionToken"] = params.ExecutionToken - req["key"] = params.Key - req["runId"] = params.RunID - } - raw, err := a.client.Request(ctx, "session.factory.pauseAtCheckpoint", req) - if err != nil { - return nil, err - } - var result SessionFactoryPauseAtCheckpointResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// ResumeFromTool internal tool-originated factory resume. -// -// RPC method: session.factory.resumeFromTool. -// -// Parameters: Internal parameters for resuming a factory run from a tool. -// -// Returns: Resolved persisted factory identity and resumed run envelope. -// Internal: ResumeFromTool is part of the SDK's internal handshake/plumbing; external -// callers should not use it. -func (a *InternalFactoryAPI) ResumeFromTool(ctx context.Context, params *FactoryToolResumeRequest) (*FactoryResumeResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - if params.Limits != nil { - req["limits"] = *params.Limits - } - req["runId"] = params.RunID - if params.ToolCallID != nil { - req["toolCallId"] = *params.ToolCallID - } - } - raw, err := a.client.Request(ctx, "session.factory.resumeFromTool", req) - if err != nil { - return nil, err - } - var result FactoryResumeResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// RunFromTool internal tool-originated factory invocation. -// -// RPC method: session.factory.runFromTool. -// -// Parameters: Internal parameters for invoking a registered factory from a tool. -// -// Returns: Complete current or terminal factory run envelope. -// Internal: RunFromTool is part of the SDK's internal handshake/plumbing; external callers -// should not use it. -func (a *InternalFactoryAPI) RunFromTool(ctx context.Context, params *FactoryToolRunRequest) (*FactoryRunResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["args"] = params.Args - req["name"] = params.Name - if params.Options != nil { - req["options"] = *params.Options - } - if params.ToolCallID != nil { - req["toolCallId"] = *params.ToolCallID - } - } - raw, err := a.client.Request(ctx, "session.factory.runFromTool", req) - if err != nil { - return nil, err - } - var result FactoryRunResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - // Experimental: InternalGitHubAuthAPI contains experimental APIs that may change or be // removed. type InternalGitHubAuthAPI internalSessionAPI @@ -33115,11 +38623,11 @@ func (a *InternalGitHubAuthAPI) GetCurrentAuthInfo(ctx context.Context) (*AuthId if err != nil { return nil, err } - var result AuthIdentity + var result *AuthIdentity if err := json.Unmarshal(raw, &result); err != nil { return nil, err } - return &result, nil + return result, nil } // LastAuthErrors gets validation errors from the most recent authentication attempt. @@ -33231,11 +38739,11 @@ func (a *InternalGitHubAuthAPI) RefreshCopilotUser(ctx context.Context) (*AuthId if err != nil { return nil, err } - var result AuthIdentity + var result *AuthIdentity if err := json.Unmarshal(raw, &result); err != nil { return nil, err } - return &result, nil + return result, nil } // SwitchToAuth switches the session to another available authentication. @@ -33350,6 +38858,35 @@ func (a *InternalMCPAPI) ReloadWithConfig(ctx context.Context, params *MCPReload return &result, nil } +// SetConnectedIdeInfo records the IDE the host is connected to, so the agent's system +// prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded +// value, which is how a host reports that it is disconnected; there is no separate clear +// method. Both `ideName` and `workspaceFolder` are required together, because half a state +// cannot be attributed to a project. +// +// RPC method: session.mcp.setConnectedIdeInfo. +// +// Parameters: Records which IDE the host is connected to, or clears it. +// Internal: SetConnectedIdeInfo is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalMCPAPI) SetConnectedIdeInfo(ctx context.Context, params *SessionMCPSetConnectedIdeInfoParams) (*SessionMCPSetConnectedIdeInfoResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Ide != nil { + req["ide"] = *params.Ide + } + } + raw, err := a.client.Request(ctx, "session.mcp.setConnectedIdeInfo", req) + if err != nil { + return nil, err + } + var result SessionMCPSetConnectedIdeInfoResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // UnregisterExternalClient unregisters a previously registered external MCP client by // server name. Marked internal as the paired companion of `registerExternalClient`: only // in-process callers that registered a client this way can meaningfully unregister it. @@ -33407,6 +38944,12 @@ func (a *InternalModelAPI) ApplyStartupOverlay(ctx context.Context, params *Mode if params.DeviceManagedModel != nil { req["deviceManagedModel"] = *params.DeviceManagedModel } + if params.ManagedContextTier != nil { + req["managedContextTier"] = *params.ManagedContextTier + } + if params.ManagedReasoningEffort != nil { + req["managedReasoningEffort"] = *params.ManagedReasoningEffort + } if params.PolicyHelperModel != nil { req["policyHelperModel"] = *params.PolicyHelperModel } @@ -33876,6 +39419,94 @@ func (a *InternalSettingsAPI) Snapshot(ctx context.Context) (*SessionSettingsSna return &result, nil } +// Experimental: InternalUIAPI contains experimental APIs that may change or be removed. +type InternalUIAPI internalSessionAPI + +// HandleHumanAskUser resolves a pending elicitation request after direct interaction in the +// trusted in-process client. Only an accepted response to the built-in ask_user tool can +// become trusted human evidence. +// +// RPC method: session.ui.handleHumanAskUser. +// +// Parameters: Pending elicitation request ID and the user's response (accept/decline/cancel +// + form values). +// +// Returns: Indicates whether the elicitation response was accepted; false if it was already +// resolved by another client. +// Internal: HandleHumanAskUser is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalUIAPI) HandleHumanAskUser(ctx context.Context, params *UIHandlePendingElicitationRequest) (*UIElicitationResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["result"] = params.Result + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanAskUser", req) + if err != nil { + return nil, err + } + var result UIElicitationResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// HandleHumanExitPlanMode resolves a pending `exit_plan_mode.requested` event after direct +// interaction in the trusted in-process client. +// +// RPC method: session.ui.handleHumanExitPlanMode. +// +// Parameters: Request ID of a pending `exit_plan_mode.requested` event and the user's +// response. +// +// Returns: Indicates whether the pending UI request was resolved by this call. +// Internal: HandleHumanExitPlanMode is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalUIAPI) HandleHumanExitPlanMode(ctx context.Context, params *UIHandlePendingExitPlanModeRequest) (*UIHandlePendingResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["response"] = params.Response + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanExitPlanMode", req) + if err != nil { + return nil, err + } + var result UIHandlePendingResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// HandleHumanUserInput resolves a pending `user_input.requested` event after direct +// interaction in the trusted in-process client. +// +// RPC method: session.ui.handleHumanUserInput. +// +// Parameters: Request ID of a pending `user_input.requested` event and the user's response. +// +// Returns: Indicates whether the pending UI request was resolved by this call. +// Internal: HandleHumanUserInput is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalUIAPI) HandleHumanUserInput(ctx context.Context, params *UIHandlePendingUserInputRequest) (*UIHandlePendingResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["response"] = params.Response + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanUserInput", req) + if err != nil { + return nil, err + } + var result UIHandlePendingResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalWorkflowAPI contains experimental APIs that may change or be // removed. type InternalWorkflowAPI internalSessionAPI @@ -33978,13 +39609,13 @@ type InternalSessionRPC struct { Canvas *InternalCanvasAPI Commands *InternalCommandsAPI Connectors *InternalConnectorsAPI - Factory *InternalFactoryAPI GitHubAuth *InternalGitHubAuthAPI MCP *InternalMCPAPI Model *InternalModelAPI Queue *InternalQueueAPI Schedule *InternalScheduleAPI Settings *InternalSettingsAPI + UI *InternalUIAPI Workflow *InternalWorkflowAPI } @@ -34026,13 +39657,13 @@ func NewInternalSessionRPC(client *jsonrpc2.Client, sessionID string) *InternalS r.Canvas = (*InternalCanvasAPI)(&r.common) r.Commands = (*InternalCommandsAPI)(&r.common) r.Connectors = (*InternalConnectorsAPI)(&r.common) - r.Factory = (*InternalFactoryAPI)(&r.common) r.GitHubAuth = (*InternalGitHubAuthAPI)(&r.common) r.MCP = (*InternalMCPAPI)(&r.common) r.Model = (*InternalModelAPI)(&r.common) r.Queue = (*InternalQueueAPI)(&r.common) r.Schedule = (*InternalScheduleAPI)(&r.common) r.Settings = (*InternalSettingsAPI)(&r.common) + r.UI = (*InternalUIAPI)(&r.common) r.Workflow = (*InternalWorkflowAPI)(&r.common) return r } @@ -34063,26 +39694,6 @@ type CanvasHandler interface { Open(request *CanvasProviderOpenRequest) (*CanvasProviderOpenResult, error) } -// Experimental: FactoryHandler contains experimental APIs that may change or be removed. -type FactoryHandler interface { - // Abort asks the owning extension connection to abort a running factory cooperatively. - // - // RPC method: factory.abort. - // - // Parameters: Parameters for cooperatively aborting a factory body. - // - // Returns: Acknowledgement that a factory request was accepted. - Abort(request *FactoryAbortRequest) (*FactoryAckResult, error) - // Execute asks the owning extension connection to execute a registered factory closure. - // - // RPC method: factory.execute. - // - // Parameters: Parameters sent to the owning extension to execute a factory closure. - // - // Returns: Result returned by an extension factory closure. - Execute(request *FactoryExecuteRequest) (*FactoryExecuteResult, error) -} - // Experimental: ProviderTokenHandler contains experimental APIs that may change or be // removed. type ProviderTokenHandler interface { @@ -34166,6 +39777,14 @@ type SessionFSHandler interface { // // Returns: File content as a UTF-8 string, or a filesystem error if the read failed. ReadFile(request *SessionFSReadFileRequest) (*SessionFSReadFileResult, error) + // ReadFileBytes reads binary file content from the client-provided session filesystem. + // + // RPC method: sessionFs.readFileBytes. + // + // Parameters: Path of the binary file to read from the client-provided session filesystem. + // + // Returns: File bytes as standard base64, or a filesystem error if the read failed. + ReadFileBytes(request *SessionFSReadFileBytesRequest) (*SessionFSReadFileBytesResult, error) // Renames or moves a path in the client-provided session filesystem. // // RPC method: sessionFs.rename. @@ -34233,6 +39852,15 @@ type SessionFSHandler interface { // // Returns: Describes a filesystem error. WriteFile(request *SessionFSWriteFileRequest) (*SessionFSError, error) + // WriteFileBytes writes binary file content to the client-provided session filesystem. + // + // RPC method: sessionFs.writeFileBytes. + // + // Parameters: File path, standard-base64-encoded bytes to write, and optional mode for the + // client-provided session filesystem. + // + // Returns: Describes a filesystem error. + WriteFileBytes(request *SessionFSWriteFileBytesRequest) (*SessionFSError, error) } // Experimental: TasksHandler contains experimental APIs that may change or be removed. @@ -34272,7 +39900,6 @@ type WorkflowHandler interface { // ClientSessionAPIHandlers provides all client session API handler groups for a session. type ClientSessionAPIHandlers struct { Canvas CanvasHandler - Factory FactoryHandler ProviderToken ProviderTokenHandler SessionFS SessionFSHandler Tasks TasksHandler @@ -34350,44 +39977,6 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) - client.SetRequestHandler("factory.abort", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var request FactoryAbortRequest - if err := json.Unmarshal(params, &request); err != nil { - return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} - } - handlers := getHandlers(request.SessionID) - if handlers == nil || handlers.Factory == nil { - return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No factory handler registered for session: %s", request.SessionID)} - } - result, err := handlers.Factory.Abort(&request) - if err != nil { - return nil, clientSessionHandlerError(err) - } - raw, err := json.Marshal(result) - if err != nil { - return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} - } - return raw, nil - }) - client.SetRequestHandler("factory.execute", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - var request FactoryExecuteRequest - if err := json.Unmarshal(params, &request); err != nil { - return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} - } - handlers := getHandlers(request.SessionID) - if handlers == nil || handlers.Factory == nil { - return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No factory handler registered for session: %s", request.SessionID)} - } - result, err := handlers.Factory.Execute(&request) - if err != nil { - return nil, clientSessionHandlerError(err) - } - raw, err := json.Marshal(result) - if err != nil { - return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} - } - return raw, nil - }) client.SetRequestHandler("providerToken.getToken", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request ProviderTokenAcquireRequest if err := json.Unmarshal(params, &request); err != nil { @@ -34521,6 +40110,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("sessionFs.readFileBytes", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request SessionFSReadFileBytesRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.SessionFS == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No sessionFs handler registered for session: %s", request.SessionID)} + } + result, err := handlers.SessionFS.ReadFileBytes(&request) + if err != nil { + return nil, clientSessionHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) client.SetRequestHandler("sessionFs.rename", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request SessionFSRenameRequest if err := json.Unmarshal(params, &request); err != nil { @@ -34654,6 +40262,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("sessionFs.writeFileBytes", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request SessionFSWriteFileBytesRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.SessionFS == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No sessionFs handler registered for session: %s", request.SessionID)} + } + result, err := handlers.SessionFS.WriteFileBytes(&request) + if err != nil { + return nil, clientSessionHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) client.SetRequestHandler("tasks.cancel", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request ClientTaskCancelRequest if err := json.Unmarshal(params, &request); err != nil { @@ -34776,6 +40403,65 @@ type HooksHandler interface { Invoke(request *HookInvokeRequest) (*HookInvokeResponse, error) } +// Experimental: HostHandler contains experimental APIs that may change or be removed. +type HostHandler interface { + // Exited reports termination of a connection-owned host listener. + // + // RPC method: host.exited. + // + // Parameters: Reports a supervised listener's hosting-task termination and cleanup outcome. + Exited(request *HostExitedNotification) error + // MaterializeSession materializes an AHP session in the owning application. + // + // RPC method: host.materializeSession. + // + // Parameters: Application callback routed over its existing SDK connection. + // + // Returns: The resident session the application has materialized on its own connection. + MaterializeSession(request *HostSessionCreateCallback) (*HostMaterializeSessionResult, error) + // RegisterSession registers an existing resident session on the supervised listener without + // durable adoption. + // + // RPC method: host.registerSession. + // + // Parameters: Listener-scoped registration, not a copy or durable adoption of a session. + // + // Returns: The existing runtime identity and its resource on the listener. + RegisterSession(request *HostRegisterSessionRequest) (*HostPublishSessionResult, error) + // SessionReleased ends application object retention for an AHP participation. + // + // RPC method: host.sessionReleased. + // + // Parameters: Releases the original application session object retained for one handoff. + SessionReleased(request *HostSessionReleasedNotification) error + // Shutdown requests graceful shutdown of a supervised AHP listener and its clients. + // + // RPC method: host.shutdown. + // + // Parameters: Empty acknowledgement for a completed host lifecycle operation. + // + // Returns: Empty acknowledgement for a completed host lifecycle operation. + Shutdown(request *HostEmptyResult) (*HostShutdownResult, error) +} + +// Experimental: InstallationsHandler contains experimental APIs that may change or be +// removed. +type InstallationsHandler interface { + // Confirm requests a fresh explicit human decision for one sealed installation operation on + // its original connection. Present the complete typed review, return the original challenge + // and fingerprint, and never infer approval. The expiresAt deadline, connection closure or + // standard JSON-RPC $/cancelRequest retires the request; late replies grant no authority. + // + // RPC method: installations.confirm. + // + // Parameters: One connection-owned, expiring request for a trusted host's explicit user + // decision. + // + // Returns: A response is meaningful only on the connection and request that issued its + // challenge. + Confirm(request *InstallationConfirmationRequest) (*InstallationsConfirmResult, error) +} + // Experimental: LlmInferenceHandler contains experimental APIs that may change or be // removed. type LlmInferenceHandler interface { @@ -34816,6 +40502,8 @@ type ClientGlobalAPIHandlers struct { GitHubTelemetry GitHubTelemetryHandler GitHubToken GitHubTokenHandler Hooks HooksHandler + Host HostHandler + Installations InstallationsHandler LlmInference LlmInferenceHandler } @@ -34900,6 +40588,104 @@ func RegisterClientGlobalAPIHandlers(client *jsonrpc2.Client, handlers *ClientGl } return raw, nil }) + client.SetRequestHandler("host.exited", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request HostExitedNotification + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.Host == nil { + return nil, nil + } + if err := handlers.Host.Exited(&request); err != nil { + return nil, clientGlobalHandlerError(err) + } + return nil, nil + }) + client.SetRequestHandler("host.materializeSession", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request HostSessionCreateCallback + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.Host == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "No host client-global handler registered"} + } + result, err := handlers.Host.MaterializeSession(&request) + if err != nil { + return nil, clientGlobalHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) + client.SetRequestHandler("host.registerSession", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request HostRegisterSessionRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.Host == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "No host client-global handler registered"} + } + result, err := handlers.Host.RegisterSession(&request) + if err != nil { + return nil, clientGlobalHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) + client.SetRequestHandler("host.sessionReleased", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request HostSessionReleasedNotification + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.Host == nil { + return nil, nil + } + if err := handlers.Host.SessionReleased(&request); err != nil { + return nil, clientGlobalHandlerError(err) + } + return nil, nil + }) + client.SetRequestHandler("host.shutdown", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request HostEmptyResult + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.Host == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "No host client-global handler registered"} + } + result, err := handlers.Host.Shutdown(&request) + if err != nil { + return nil, clientGlobalHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) + client.SetRequestHandler("installations.confirm", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request InstallationConfirmationRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.Installations == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "No installations client-global handler registered"} + } + result, err := handlers.Installations.Confirm(&request) + if err != nil { + return nil, clientGlobalHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) client.SetRequestHandler("llmInference.httpRequestChunk", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request LlmInferenceHTTPRequestChunkRequest if err := json.Unmarshal(params, &request); err != nil { diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 97d51702da..4835a1bef9 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -21,6 +21,12 @@ func unmarshalAuthInfo(data []byte) (AuthInfo, error) { } switch raw.Type { + case AuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case AuthInfoTypeAPIKey: var d APIKeyAuthInfo if err := json.Unmarshal(data, &d); err != nil { @@ -85,6 +91,17 @@ func (r RawAuthInfoData) MarshalJSON() ([]byte, error) { }) } +func (r AccountAuthInfo) MarshalJSON() ([]byte, error) { + type alias AccountAuthInfo + return json.Marshal(struct { + Type AuthInfoType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r APIKeyAuthInfo) MarshalJSON() ([]byte, error) { type alias APIKeyAuthInfo return json.Marshal(struct { @@ -235,6 +252,283 @@ func (r *AccountLogoutRequest) UnmarshalJSON(data []byte) error { return nil } +func unmarshalAuthEnumerateQuery(data []byte) (AuthEnumerateQuery, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind AuthEnumerateQueryKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case AuthEnumerateQueryKindAccounts: + var d AuthEnumerateQueryAccounts + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthEnumerateQueryKindProviders: + var d AuthEnumerateQueryProviders + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawAuthEnumerateQueryData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawAuthEnumerateQueryData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind AuthEnumerateQueryKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r AuthEnumerateQueryAccounts) MarshalJSON() ([]byte, error) { + type alias AuthEnumerateQueryAccounts + return json.Marshal(struct { + Kind AuthEnumerateQueryKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthEnumerateQueryProviders) MarshalJSON() ([]byte, error) { + type alias AuthEnumerateQueryProviders + return json.Marshal(struct { + Kind AuthEnumerateQueryKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *AccountsEnumerateRequest) UnmarshalJSON(data []byte) error { + type rawAccountsEnumerateRequest struct { + Query json.RawMessage `json:"query"` + } + var raw rawAccountsEnumerateRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Query != nil { + value, err := unmarshalAuthEnumerateQuery(raw.Query) + if err != nil { + return err + } + r.Query = value + } + return nil +} + +func unmarshalAuthReadQuery(data []byte) (AuthReadQuery, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind AuthReadQueryKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case AuthReadQueryKindActiveAccount: + var d AuthReadQueryActiveAccount + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthReadQueryKindLastErrors: + var d AuthReadQueryLastErrors + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthReadQueryKindStatus: + var d AuthReadQueryStatus + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawAuthReadQueryData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawAuthReadQueryData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind AuthReadQueryKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r AuthReadQueryActiveAccount) MarshalJSON() ([]byte, error) { + type alias AuthReadQueryActiveAccount + return json.Marshal(struct { + Kind AuthReadQueryKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthReadQueryLastErrors) MarshalJSON() ([]byte, error) { + type alias AuthReadQueryLastErrors + return json.Marshal(struct { + Kind AuthReadQueryKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthReadQueryStatus) MarshalJSON() ([]byte, error) { + type alias AuthReadQueryStatus + return json.Marshal(struct { + Kind AuthReadQueryKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *AccountsGetRequest) UnmarshalJSON(data []byte) error { + type rawAccountsGetRequest struct { + Query json.RawMessage `json:"query"` + } + var raw rawAccountsGetRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Query != nil { + value, err := unmarshalAuthReadQuery(raw.Query) + if err != nil { + return err + } + r.Query = value + } + return nil +} + +func unmarshalAuthWrite(data []byte) (AuthWrite, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind AuthWriteKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case AuthWriteKindLogout: + var d AuthWriteLogout + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthWriteKindSetCredentials: + var d AuthWriteSetCredentials + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthWriteKindSwitchActive: + var d AuthWriteSwitchActive + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawAuthWriteData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawAuthWriteData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind AuthWriteKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r AuthWriteLogout) MarshalJSON() ([]byte, error) { + type alias AuthWriteLogout + return json.Marshal(struct { + Kind AuthWriteKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthWriteSetCredentials) MarshalJSON() ([]byte, error) { + type alias AuthWriteSetCredentials + return json.Marshal(struct { + Kind AuthWriteKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthWriteSwitchActive) MarshalJSON() ([]byte, error) { + type alias AuthWriteSwitchActive + return json.Marshal(struct { + Kind AuthWriteKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *AccountsSetRequest) UnmarshalJSON(data []byte) error { + type rawAccountsSetRequest struct { + Command json.RawMessage `json:"command"` + } + var raw rawAccountsSetRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Command != nil { + value, err := unmarshalAuthWrite(raw.Command) + if err != nil { + return err + } + r.Command = value + } + return nil +} + func unmarshalAgentRegistrySpawnResult(data []byte) (AgentRegistrySpawnResult, error) { if string(data) == "null" { return nil, nil @@ -616,39 +910,333 @@ func (r AttachmentSelection) MarshalJSON() ([]byte, error) { }) } -func unmarshalBuiltinToolSafeForTelemetry(data []byte) (BuiltinToolSafeForTelemetry, error) { +func unmarshalAuthEnumerateValue(data []byte) (AuthEnumerateValue, error) { if string(data) == "null" { return nil, nil } - { - var value bool - if err := json.Unmarshal(data, &value); err == nil { - return BuiltinToolSafeForTelemetryBoolean(value), nil - } + type rawUnion struct { + Kind AuthEnumerateValueKind `json:"kind"` } - { - var value BuiltinToolSafeTelemetryFields - if err := json.Unmarshal(data, &value); err == nil { - return &value, nil + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case AuthEnumerateValueKindAccounts: + var d AuthEnumerateValueAccounts + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthEnumerateValueKindProviders: + var d AuthEnumerateValueProviders + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } + return &d, nil + default: + return &RawAuthEnumerateValueData{Discriminator: raw.Kind, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for BuiltinToolSafeForTelemetry") } -func (r *BuiltinToolDescriptor) UnmarshalJSON(data []byte) error { - type rawBuiltinToolDescriptor struct { - Description string `json:"description"` - Format *BuiltinToolFormat `json:"format"` - HasSummariseIntention bool `json:"hasSummariseIntention"` - InputSchema *BuiltinToolInputSchema `json:"inputSchema"` - Instructions *string `json:"instructions"` - IsTerminal bool `json:"isTerminal"` - Name string `json:"name"` - SafeForTelemetry json.RawMessage `json:"safeForTelemetry"` - Title *string `json:"title"` - Type *string `json:"type"` +func (r RawAuthEnumerateValueData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } - var raw rawBuiltinToolDescriptor + return json.Marshal(struct { + Kind AuthEnumerateValueKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r AuthEnumerateValueAccounts) MarshalJSON() ([]byte, error) { + type alias AuthEnumerateValueAccounts + return json.Marshal(struct { + Kind AuthEnumerateValueKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthEnumerateValueProviders) MarshalJSON() ([]byte, error) { + type alias AuthEnumerateValueProviders + return json.Marshal(struct { + Kind AuthEnumerateValueKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func unmarshalAuthLoginStep(data []byte) (AuthLoginStep, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind AuthLoginStepKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case AuthLoginStepKindAwaiting: + var d AuthLoginStepAwaiting + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthLoginStepKindCompleted: + var d AuthLoginStepCompleted + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthLoginStepKindError: + var d AuthLoginStepError + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthLoginStepKindInputRequired: + var d AuthLoginStepInputRequired + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthLoginStepKindNeedsInteraction: + var d AuthLoginStepNeedsInteraction + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthLoginStepKindOpenURL: + var d AuthLoginStepOpenURL + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawAuthLoginStepData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawAuthLoginStepData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind AuthLoginStepKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r AuthLoginStepAwaiting) MarshalJSON() ([]byte, error) { + type alias AuthLoginStepAwaiting + return json.Marshal(struct { + Kind AuthLoginStepKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthLoginStepCompleted) MarshalJSON() ([]byte, error) { + type alias AuthLoginStepCompleted + return json.Marshal(struct { + Kind AuthLoginStepKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthLoginStepError) MarshalJSON() ([]byte, error) { + type alias AuthLoginStepError + return json.Marshal(struct { + Kind AuthLoginStepKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthLoginStepInputRequired) MarshalJSON() ([]byte, error) { + type alias AuthLoginStepInputRequired + return json.Marshal(struct { + Kind AuthLoginStepKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthLoginStepNeedsInteraction) MarshalJSON() ([]byte, error) { + type alias AuthLoginStepNeedsInteraction + return json.Marshal(struct { + Kind AuthLoginStepKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthLoginStepOpenURL) MarshalJSON() ([]byte, error) { + type alias AuthLoginStepOpenURL + return json.Marshal(struct { + Kind AuthLoginStepKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *AuthLoginBegun) UnmarshalJSON(data []byte) error { + type rawAuthLoginBegun struct { + FlowID string `json:"flowId"` + Step json.RawMessage `json:"step"` + } + var raw rawAuthLoginBegun + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.FlowID = raw.FlowID + if raw.Step != nil { + value, err := unmarshalAuthLoginStep(raw.Step) + if err != nil { + return err + } + r.Step = value + } + return nil +} + +func unmarshalAuthReadValue(data []byte) (AuthReadValue, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind AuthReadValueKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case AuthReadValueKindActiveAccount: + var d AuthReadValueActiveAccount + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthReadValueKindLastErrors: + var d AuthReadValueLastErrors + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case AuthReadValueKindStatus: + var d AuthReadValueStatus + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawAuthReadValueData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawAuthReadValueData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind AuthReadValueKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r AuthReadValueActiveAccount) MarshalJSON() ([]byte, error) { + type alias AuthReadValueActiveAccount + return json.Marshal(struct { + Kind AuthReadValueKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthReadValueLastErrors) MarshalJSON() ([]byte, error) { + type alias AuthReadValueLastErrors + return json.Marshal(struct { + Kind AuthReadValueKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r AuthReadValueStatus) MarshalJSON() ([]byte, error) { + type alias AuthReadValueStatus + return json.Marshal(struct { + Kind AuthReadValueKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func unmarshalBuiltinToolSafeForTelemetry(data []byte) (BuiltinToolSafeForTelemetry, error) { + if string(data) == "null" { + return nil, nil + } + { + var value bool + if err := json.Unmarshal(data, &value); err == nil { + return BuiltinToolSafeForTelemetryBoolean(value), nil + } + } + { + var value BuiltinToolSafeTelemetryFields + if err := json.Unmarshal(data, &value); err == nil { + return &value, nil + } + } + return nil, errors.New("data did not match any union variant for BuiltinToolSafeForTelemetry") +} + +func (r *BuiltinToolDescriptor) UnmarshalJSON(data []byte) error { + type rawBuiltinToolDescriptor struct { + Description string `json:"description"` + Format *BuiltinToolFormat `json:"format"` + HasSummariseIntention bool `json:"hasSummariseIntention"` + InputSchema *BuiltinToolInputSchema `json:"inputSchema"` + Instructions *string `json:"instructions"` + IsTerminal bool `json:"isTerminal"` + Name string `json:"name"` + SafeForTelemetry json.RawMessage `json:"safeForTelemetry"` + Title *string `json:"title"` + Type *string `json:"type"` + } + var raw rawBuiltinToolDescriptor if err := json.Unmarshal(data, &raw); err != nil { return err } @@ -2310,273 +2898,24 @@ func unmarshalExternalToolResult(data []byte) (ExternalToolResult, error) { return nil, errors.New("data did not match any union variant for ExternalToolResult") } -func unmarshalFactoryRunFailure(data []byte) (FactoryRunFailure, error) { +func unmarshalFilterMapping(data []byte) (FilterMapping, error) { if string(data) == "null" { return nil, nil } - type rawUnion struct { - Type FactoryRunFailureType `json:"type"` + { + var value FilterMappingEnumMap + if err := json.Unmarshal(data, &value); err == nil { + return value, nil + } } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err + { + var value ContentFilterMode + if err := json.Unmarshal(data, &value); err == nil { + return value, nil + } } - - switch raw.Type { - case FactoryRunFailureTypeFactoryAccountingIncomplete: - var d FactoryRunFailureFactoryAccountingIncomplete - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case FactoryRunFailureTypeFactoryDurableFailure: - var d FactoryRunFailureFactoryDurableFailure - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case FactoryRunFailureTypeFactoryLimitReached: - var d FactoryRunFailureFactoryLimitReached - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case FactoryRunFailureTypeFactoryProviderDisconnected: - var d FactoryRunFailureFactoryProviderDisconnected - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case FactoryRunFailureTypeFactoryResumeDeclined: - var d FactoryRunFailureFactoryResumeDeclined - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawFactoryRunFailureData{Discriminator: raw.Type, Raw: data}, nil - } -} - -func (r RawFactoryRunFailureData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } - return json.Marshal(struct { - Type FactoryRunFailureType `json:"type"` - }{ - Type: r.Discriminator, - }) -} - -func (r FactoryRunFailureFactoryAccountingIncomplete) MarshalJSON() ([]byte, error) { - type alias FactoryRunFailureFactoryAccountingIncomplete - return json.Marshal(struct { - Type FactoryRunFailureType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r FactoryRunFailureFactoryDurableFailure) MarshalJSON() ([]byte, error) { - type alias FactoryRunFailureFactoryDurableFailure - return json.Marshal(struct { - Type FactoryRunFailureType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r FactoryRunFailureFactoryLimitReached) MarshalJSON() ([]byte, error) { - type alias FactoryRunFailureFactoryLimitReached - return json.Marshal(struct { - Type FactoryRunFailureType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r FactoryRunFailureFactoryProviderDisconnected) MarshalJSON() ([]byte, error) { - type alias FactoryRunFailureFactoryProviderDisconnected - return json.Marshal(struct { - Type FactoryRunFailureType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r FactoryRunFailureFactoryResumeDeclined) MarshalJSON() ([]byte, error) { - type alias FactoryRunFailureFactoryResumeDeclined - return json.Marshal(struct { - Type FactoryRunFailureType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func unmarshalFactoryPauseInfo(data []byte) (FactoryPauseInfo, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Type FactoryPauseInfoType `json:"type"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Type { - case FactoryPauseInfoTypeCheckpoint: - var d FactoryPauseInfoCheckpoint - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case FactoryPauseInfoTypeUser: - var d FactoryPauseInfoUser - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawFactoryPauseInfoData{Discriminator: raw.Type, Raw: data}, nil - } -} - -func (r RawFactoryPauseInfoData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } - return json.Marshal(struct { - Type FactoryPauseInfoType `json:"type"` - }{ - Type: r.Discriminator, - }) -} - -func (r FactoryPauseInfoCheckpoint) MarshalJSON() ([]byte, error) { - type alias FactoryPauseInfoCheckpoint - return json.Marshal(struct { - Type FactoryPauseInfoType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r FactoryPauseInfoUser) MarshalJSON() ([]byte, error) { - type alias FactoryPauseInfoUser - return json.Marshal(struct { - Type FactoryPauseInfoType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r *FactoryRunTerminal) UnmarshalJSON(data []byte) error { - type rawFactoryRunTerminal struct { - Error *string `json:"error,omitempty"` - Failure json.RawMessage `json:"failure,omitempty"` - PauseInfo json.RawMessage `json:"pauseInfo"` - Reason *string `json:"reason,omitempty"` - ResultPreview *string `json:"resultPreview,omitempty"` - } - var raw rawFactoryRunTerminal - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - r.Error = raw.Error - if raw.Failure != nil { - value, err := unmarshalFactoryRunFailure(raw.Failure) - if err != nil { - return err - } - r.Failure = value - } - if raw.PauseInfo != nil { - value, err := unmarshalFactoryPauseInfo(raw.PauseInfo) - if err != nil { - return err - } - r.PauseInfo = value - } - r.Reason = raw.Reason - r.ResultPreview = raw.ResultPreview - return nil -} - -func (r *FactoryRunResult) UnmarshalJSON(data []byte) error { - type rawFactoryRunResult struct { - Attempt *int64 `json:"attempt,omitempty"` - Error *string `json:"error,omitempty"` - Failure json.RawMessage `json:"failure,omitempty"` - PauseInfo json.RawMessage `json:"pauseInfo,omitempty"` - Reason *string `json:"reason,omitempty"` - Result any `json:"result,omitempty"` - RunID string `json:"runId"` - Snapshot any `json:"snapshot,omitempty"` - Status FactoryRunStatus `json:"status"` - } - var raw rawFactoryRunResult - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - r.Attempt = raw.Attempt - r.Error = raw.Error - if raw.Failure != nil { - value, err := unmarshalFactoryRunFailure(raw.Failure) - if err != nil { - return err - } - r.Failure = value - } - if raw.PauseInfo != nil { - value, err := unmarshalFactoryPauseInfo(raw.PauseInfo) - if err != nil { - return err - } - r.PauseInfo = value - } - r.Reason = raw.Reason - r.Result = raw.Result - r.RunID = raw.RunID - r.Snapshot = raw.Snapshot - r.Status = raw.Status - return nil -} - -func unmarshalFilterMapping(data []byte) (FilterMapping, error) { - if string(data) == "null" { - return nil, nil - } - { - var value FilterMappingEnumMap - if err := json.Unmarshal(data, &value); err == nil { - return value, nil - } - } - { - var value ContentFilterMode - if err := json.Unmarshal(data, &value); err == nil { - return value, nil - } - } - return nil, errors.New("data did not match any union variant for FilterMapping") -} + return nil, errors.New("data did not match any union variant for FilterMapping") +} func (r *FleetStartRequest) UnmarshalJSON(data []byte) error { type rawFleetStartRequest struct { @@ -2668,28 +3007,6 @@ func (r GitHubTokenAcquireResultToken) MarshalJSON() ([]byte, error) { }) } -func (r *HandlePendingToolCallRequest) UnmarshalJSON(data []byte) error { - type rawHandlePendingToolCallRequest struct { - Error *string `json:"error,omitempty"` - RequestID string `json:"requestId"` - Result json.RawMessage `json:"result,omitempty"` - } - var raw rawHandlePendingToolCallRequest - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - r.Error = raw.Error - r.RequestID = raw.RequestID - if raw.Result != nil { - value, err := unmarshalExternalToolResult(raw.Result) - if err != nil { - return err - } - r.Result = value - } - return nil -} - func (r InstalledPluginSource) MarshalJSON() ([]byte, error) { if r.InstalledPluginSourceGitHub != nil { return json.Marshal(r.InstalledPluginSourceGitHub) @@ -2742,686 +3059,898 @@ func (r *InstalledPluginSource) UnmarshalJSON(data []byte) error { return errors.New("data did not match any union variant for InstalledPluginSource") } -func matchesMCPSerializableServerConfigMCPServerConfigHTTP(data []byte) bool { - var rawGroup0 struct { - Command json.RawMessage `json:"command"` - URL json.RawMessage `json:"url"` +func (r *HandlePendingToolCallRequest) UnmarshalJSON(data []byte) error { + type rawHandlePendingToolCallRequest struct { + Error *string `json:"error,omitempty"` + RequestID string `json:"requestId"` + Result json.RawMessage `json:"result,omitempty"` } - if err := json.Unmarshal(data, &rawGroup0); err != nil { - return false + var raw rawHandlePendingToolCallRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err } - if rawGroup0.URL == nil { - return false + r.Error = raw.Error + r.RequestID = raw.RequestID + if raw.Result != nil { + value, err := unmarshalExternalToolResult(raw.Result) + if err != nil { + return err + } + r.Result = value } - return rawGroup0.Command == nil + return nil } -func matchesMCPSerializableServerConfigMCPServerConfigStdio(data []byte) bool { - var rawGroup0 struct { - Command json.RawMessage `json:"command"` - URL json.RawMessage `json:"url"` +func (r *InstallationCatalogueIdentity) UnmarshalJSON(data []byte) error { + type rawInstallationCatalogueIdentity struct { + Description *string `json:"description,omitempty"` + DisplayName string `json:"displayName"` + ItemURL *string `json:"itemUrl,omitempty"` + Publisher *string `json:"publisher,omitempty"` + ResourceID *string `json:"resourceId,omitempty"` + Source string `json:"source"` + TrustAtInstall json.RawMessage `json:"trustAtInstall,omitempty"` + Version *string `json:"version,omitempty"` } - if err := json.Unmarshal(data, &rawGroup0); err != nil { - return false + var raw rawInstallationCatalogueIdentity + if err := json.Unmarshal(data, &raw); err != nil { + return err } - if rawGroup0.Command == nil { - return false + r.Description = raw.Description + r.DisplayName = raw.DisplayName + r.ItemURL = raw.ItemURL + r.Publisher = raw.Publisher + r.ResourceID = raw.ResourceID + r.Source = raw.Source + if raw.TrustAtInstall != nil { + value, err := unmarshalCatalogTrustSnapshot(raw.TrustAtInstall) + if err != nil { + return err + } + r.TrustAtInstall = value } - return rawGroup0.URL == nil + r.Version = raw.Version + return nil } -func unmarshalMCPSerializableServerConfig(data []byte) (MCPSerializableServerConfig, error) { +func unmarshalInstallationReview(data []byte) (InstallationReview, error) { if string(data) == "null" { return nil, nil } - if matchesMCPSerializableServerConfigMCPServerConfigHTTP(data) { - var d MCPServerConfigHTTP + type rawUnion struct { + Resource InstallationReviewResource `json:"resource"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Resource { + case InstallationReviewResourceMCP: + var d InstallationReviewMCP if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - } - if matchesMCPSerializableServerConfigMCPServerConfigStdio(data) { - var d MCPServerConfigStdio + case InstallationReviewResourceSkill: + var d InstallationReviewSkill if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil + default: + return &RawInstallationReviewData{Discriminator: raw.Resource, Raw: data}, nil } - return &RawMCPSerializableServerConfigData{Raw: data}, nil } -func (r RawMCPSerializableServerConfigData) MarshalJSON() ([]byte, error) { +func (r RawInstallationReviewData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } - return []byte("null"), nil + return json.Marshal(struct { + Resource InstallationReviewResource `json:"resource"` + }{ + Resource: r.Discriminator, + }) } -func unmarshalMCPServerAuthConfig(data []byte) (MCPServerAuthConfig, error) { +func unmarshalMCPInstallationReview(data []byte) (MCPInstallationReview, error) { if string(data) == "null" { return nil, nil } - { - var value bool - if err := json.Unmarshal(data, &value); err == nil { - return MCPServerAuthConfigBoolean(value), nil - } + type rawUnion struct { + Action MCPInstallationReviewAction `json:"action"` } - { - var value MCPServerAuthConfigRedirectPort - if err := json.Unmarshal(data, &value); err == nil { - return &value, nil - } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err } - return nil, errors.New("data did not match any union variant for MCPServerAuthConfig") -} -func unmarshalMCPSafeForTelemetry(data []byte) (MCPSafeForTelemetry, error) { - if string(data) == "null" { - return nil, nil - } - { - var value bool - if err := json.Unmarshal(data, &value); err == nil { - return MCPSafeForTelemetryBoolean(value), nil + switch raw.Action { + case MCPInstallationReviewActionInstall: + var d MCPInstallationReviewInstall + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - } - { - var value MCPSafeForTelemetryFields - if err := json.Unmarshal(data, &value); err == nil { - return &value, nil + return &d, nil + case MCPInstallationReviewActionUninstall: + var d MCPInstallationReviewUninstall + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } + return &d, nil + default: + return &RawMCPInstallationReviewData{Discriminator: raw.Action, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for MCPSafeForTelemetry") } -func (r *MCPServerConfigHTTP) UnmarshalJSON(data []byte) error { - type rawMCPServerConfigHTTP struct { - Auth json.RawMessage `json:"auth,omitempty"` - ConfigWarnings []string `json:"configWarnings,omitzero"` - DeferTools *MCPServerConfigDeferTools `json:"deferTools,omitempty"` - DisableSecretMasking *bool `json:"disableSecretMasking,omitempty"` - DisableToolCache *bool `json:"disableToolCache,omitempty"` - DisplayName *string `json:"displayName,omitempty"` - Events []string `json:"events,omitzero"` - ExcludeTools []string `json:"excludeTools,omitzero"` - FilterMapping json.RawMessage `json:"filterMapping,omitempty"` - Headers map[string]string `json:"headers,omitzero"` - HeadersRefreshTtlMs *int64 `json:"headersRefreshTtlMs,omitempty"` - IsDefaultServer *bool `json:"isDefaultServer,omitempty"` - Notifications []string `json:"notifications,omitzero"` - OauthClientID *string `json:"oauthClientId,omitempty"` - OauthGrantType *MCPServerConfigHTTPOauthGrantType `json:"oauthGrantType,omitempty"` - OauthPublicClient *bool `json:"oauthPublicClient,omitempty"` - OauthScopes []string `json:"oauthScopes,omitzero"` - Oidc json.RawMessage `json:"oidc,omitempty"` - SafeForTelemetry json.RawMessage `json:"safeForTelemetry,omitempty"` - SlowConnectionThresholdMs *int64 `json:"slowConnectionThresholdMs,omitempty"` - Source *MCPServerSource `json:"source,omitempty"` - SourcePath *string `json:"sourcePath,omitempty"` - SourcePlugin *string `json:"sourcePlugin,omitempty"` - SourcePluginSpec *bool `json:"sourcePluginSpec,omitempty"` - SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` - Timeout *int64 `json:"timeout,omitempty"` - Tools []string `json:"tools,omitzero"` - Type *MCPServerConfigHTTPType `json:"type,omitempty"` - URL string `json:"url"` +func (r RawMCPInstallationReviewData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } - var raw rawMCPServerConfigHTTP + return json.Marshal(struct { + Action MCPInstallationReviewAction `json:"action"` + }{ + Action: r.Discriminator, + }) +} + +func unmarshalMCPPlanTransportChoice(data []byte) (MCPPlanTransportChoice, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Transport MCPPlanTransportChoiceTransport `json:"transport"` + } + var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { - return err + return nil, err } - if raw.Auth != nil { - value, err := unmarshalMCPServerAuthConfig(raw.Auth) - if err != nil { - return err + + switch raw.Transport { + case MCPPlanTransportChoiceTransportHTTP: + var d MCPPlanTransportChoiceRemote + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - r.Auth = value - } - r.ConfigWarnings = raw.ConfigWarnings - r.DeferTools = raw.DeferTools - r.DisableSecretMasking = raw.DisableSecretMasking - r.DisableToolCache = raw.DisableToolCache - r.DisplayName = raw.DisplayName - r.Events = raw.Events - r.ExcludeTools = raw.ExcludeTools - if raw.FilterMapping != nil { - value, err := unmarshalFilterMapping(raw.FilterMapping) - if err != nil { - return err + return &d, nil + case MCPPlanTransportChoiceTransportSSE: + var d MCPPlanTransportChoiceRemote + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - r.FilterMapping = value - } - r.Headers = raw.Headers - r.HeadersRefreshTtlMs = raw.HeadersRefreshTtlMs - r.IsDefaultServer = raw.IsDefaultServer - r.Notifications = raw.Notifications - r.OauthClientID = raw.OauthClientID - r.OauthGrantType = raw.OauthGrantType - r.OauthPublicClient = raw.OauthPublicClient - r.OauthScopes = raw.OauthScopes - if raw.Oidc != nil { - value, err := unmarshalMCPServerAuthConfig(raw.Oidc) - if err != nil { - return err + return &d, nil + case MCPPlanTransportChoiceTransportStdio: + var d MCPPlanTransportChoicePackage + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - r.Oidc = value - } - if raw.SafeForTelemetry != nil { - value, err := unmarshalMCPSafeForTelemetry(raw.SafeForTelemetry) - if err != nil { - return err + return &d, nil + case MCPPlanTransportChoiceTransportStreamableHTTP: + var d MCPPlanTransportChoiceRemote + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - r.SafeForTelemetry = value + return &d, nil + default: + return &RawMCPPlanTransportChoiceData{Discriminator: raw.Transport, Raw: data}, nil } - r.SlowConnectionThresholdMs = raw.SlowConnectionThresholdMs - r.Source = raw.Source - r.SourcePath = raw.SourcePath - r.SourcePlugin = raw.SourcePlugin - r.SourcePluginSpec = raw.SourcePluginSpec - r.SourcePluginVersion = raw.SourcePluginVersion - r.Timeout = raw.Timeout - r.Tools = raw.Tools - r.Type = raw.Type - r.URL = raw.URL - return nil } -func (r *MCPServerConfigStdio) UnmarshalJSON(data []byte) error { - type rawMCPServerConfigStdio struct { - Args []string `json:"args,omitzero"` - Auth json.RawMessage `json:"auth,omitempty"` - Command string `json:"command"` - ConfigWarnings []string `json:"configWarnings,omitzero"` - Cwd *string `json:"cwd,omitempty"` - DeferTools *MCPServerConfigDeferTools `json:"deferTools,omitempty"` - DisableSecretMasking *bool `json:"disableSecretMasking,omitempty"` - DisableToolCache *bool `json:"disableToolCache,omitempty"` - DisplayName *string `json:"displayName,omitempty"` - Env map[string]string `json:"env,omitzero"` - Events []string `json:"events,omitzero"` - ExcludeTools []string `json:"excludeTools,omitzero"` - FilterMapping json.RawMessage `json:"filterMapping,omitempty"` - IsDefaultServer *bool `json:"isDefaultServer,omitempty"` - Notifications []string `json:"notifications,omitzero"` - Oidc json.RawMessage `json:"oidc,omitempty"` - SafeForTelemetry json.RawMessage `json:"safeForTelemetry,omitempty"` - SlowConnectionThresholdMs *int64 `json:"slowConnectionThresholdMs,omitempty"` - Source *MCPServerSource `json:"source,omitempty"` - SourcePath *string `json:"sourcePath,omitempty"` - SourcePlugin *string `json:"sourcePlugin,omitempty"` - SourcePluginSpec *bool `json:"sourcePluginSpec,omitempty"` - SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` - Timeout *int64 `json:"timeout,omitempty"` - Tools []string `json:"tools,omitzero"` - Type *MCPServerConfigStdioType `json:"type,omitempty"` +func (r RawMCPPlanTransportChoiceData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } - var raw rawMCPServerConfigStdio + return json.Marshal(struct { + Transport MCPPlanTransportChoiceTransport `json:"transport"` + }{ + Transport: r.Discriminator, + }) +} + +func unmarshalMCPPlanRequiredValue(data []byte) (MCPPlanRequiredValue, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind MCPPlanRequiredValueKind `json:"kind"` + } + var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { - return err + return nil, err } - r.Args = raw.Args - if raw.Auth != nil { - value, err := unmarshalMCPServerAuthConfig(raw.Auth) - if err != nil { - return err + + switch raw.Kind { + case MCPPlanRequiredValueKindEnum: + var d MCPPlanRequiredValueEnum + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - r.Auth = value - } - r.Command = raw.Command - r.ConfigWarnings = raw.ConfigWarnings - r.Cwd = raw.Cwd - r.DeferTools = raw.DeferTools - r.DisableSecretMasking = raw.DisableSecretMasking - r.DisableToolCache = raw.DisableToolCache - r.DisplayName = raw.DisplayName - r.Env = raw.Env - r.Events = raw.Events - r.ExcludeTools = raw.ExcludeTools - if raw.FilterMapping != nil { - value, err := unmarshalFilterMapping(raw.FilterMapping) - if err != nil { - return err + return &d, nil + case MCPPlanRequiredValueKindScalar: + var d MCPPlanRequiredValueScalar + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - r.FilterMapping = value + return &d, nil + default: + return &RawMCPPlanRequiredValueData{Discriminator: raw.Kind, Raw: data}, nil } - r.IsDefaultServer = raw.IsDefaultServer - r.Notifications = raw.Notifications - if raw.Oidc != nil { - value, err := unmarshalMCPServerAuthConfig(raw.Oidc) - if err != nil { - return err - } - r.Oidc = value - } - if raw.SafeForTelemetry != nil { - value, err := unmarshalMCPSafeForTelemetry(raw.SafeForTelemetry) - if err != nil { - return err - } - r.SafeForTelemetry = value +} + +func (r RawMCPPlanRequiredValueData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } - r.SlowConnectionThresholdMs = raw.SlowConnectionThresholdMs - r.Source = raw.Source - r.SourcePath = raw.SourcePath - r.SourcePlugin = raw.SourcePlugin - r.SourcePluginSpec = raw.SourcePluginSpec - r.SourcePluginVersion = raw.SourcePluginVersion - r.Timeout = raw.Timeout - r.Tools = raw.Tools - r.Type = raw.Type - return nil + return json.Marshal(struct { + Kind MCPPlanRequiredValueKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) } -func (r *MCPConfigAddRequest) UnmarshalJSON(data []byte) error { - type rawMCPConfigAddRequest struct { - Config json.RawMessage `json:"config"` - Name string `json:"name"` +func (r MCPPlanRequiredValueEnum) MarshalJSON() ([]byte, error) { + type alias MCPPlanRequiredValueEnum + return json.Marshal(struct { + Kind MCPPlanRequiredValueKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r MCPPlanRequiredValueScalar) MarshalJSON() ([]byte, error) { + type alias MCPPlanRequiredValueScalar + return json.Marshal(struct { + Kind MCPPlanRequiredValueKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *MCPPlanTransportChoicePackage) UnmarshalJSON(data []byte) error { + type rawMCPPlanTransportChoicePackage struct { + ChoiceID string `json:"choiceId"` + InstallMethod MCPPlanPackageInstallMethod `json:"installMethod"` + PackageIdentifier string `json:"packageIdentifier"` + PackageType string `json:"packageType"` + RequiredValues []json.RawMessage `json:"requiredValues"` + SecretPlaceholders []MCPPlanSecretPlaceholder `json:"secretPlaceholders"` } - var raw rawMCPConfigAddRequest + var raw rawMCPPlanTransportChoicePackage if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Config != nil { - value, err := unmarshalMCPSerializableServerConfig(raw.Config) - if err != nil { - return err + r.ChoiceID = raw.ChoiceID + r.InstallMethod = raw.InstallMethod + r.PackageIdentifier = raw.PackageIdentifier + r.PackageType = raw.PackageType + if raw.RequiredValues != nil { + r.RequiredValues = make([]MCPPlanRequiredValue, 0, len(raw.RequiredValues)) + for _, rawItem := range raw.RequiredValues { + value, err := unmarshalMCPPlanRequiredValue(rawItem) + if err != nil { + return err + } + r.RequiredValues = append(r.RequiredValues, value) } - r.Config = value } - r.Name = raw.Name + r.SecretPlaceholders = raw.SecretPlaceholders return nil } -func (r *MCPConfigList) UnmarshalJSON(data []byte) error { - type rawMCPConfigList struct { - Servers map[string]json.RawMessage `json:"servers"` +func (r MCPPlanTransportChoicePackage) MarshalJSON() ([]byte, error) { + type alias MCPPlanTransportChoicePackage + return json.Marshal(struct { + Transport MCPPlanTransportChoiceTransport `json:"transport"` + alias + }{ + Transport: r.Transport(), + alias: alias(r), + }) +} + +func (r *MCPPlanTransportChoiceRemote) UnmarshalJSON(data []byte) error { + type rawMCPPlanTransportChoiceRemote struct { + ChoiceID string `json:"choiceId"` + Endpoint string `json:"endpoint"` + InstallMethod MCPPlanRemoteInstallMethod `json:"installMethod"` + RequiredValues []json.RawMessage `json:"requiredValues"` + SecretPlaceholders []MCPPlanSecretPlaceholder `json:"secretPlaceholders"` + Discriminator MCPPlanRemoteTransport `json:"transport,omitempty"` } - var raw rawMCPConfigList + var raw rawMCPPlanTransportChoiceRemote if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Servers != nil { - r.Servers = make(map[string]MCPSerializableServerConfig, len(raw.Servers)) - for key, rawValue := range raw.Servers { - value, err := unmarshalMCPSerializableServerConfig(rawValue) + r.ChoiceID = raw.ChoiceID + r.Endpoint = raw.Endpoint + r.InstallMethod = raw.InstallMethod + if raw.RequiredValues != nil { + r.RequiredValues = make([]MCPPlanRequiredValue, 0, len(raw.RequiredValues)) + for _, rawItem := range raw.RequiredValues { + value, err := unmarshalMCPPlanRequiredValue(rawItem) if err != nil { return err } - r.Servers[key] = value + r.RequiredValues = append(r.RequiredValues, value) + } + } + r.SecretPlaceholders = raw.SecretPlaceholders + r.Discriminator = raw.Discriminator + return nil +} + +func (r MCPPlanTransportChoiceRemote) MarshalJSON() ([]byte, error) { + type alias MCPPlanTransportChoiceRemote + return json.Marshal(struct { + Transport MCPPlanTransportChoiceTransport `json:"transport"` + alias + }{ + Transport: r.Transport(), + alias: alias(r), + }) +} + +func (r *MCPInstallationReviewInstall) UnmarshalJSON(data []byte) error { + type rawMCPInstallationReviewInstall struct { + Catalogue *InstallationCatalogueIdentity `json:"catalogue,omitempty"` + CatalogueTrust json.RawMessage `json:"catalogueTrust,omitempty"` + ConfigurationChange MCPPlanConfigurationChange `json:"configurationChange"` + EffectiveConfiguration *MCPInstallationRemoteConfiguration `json:"effectiveConfiguration,omitempty"` + Identity MCPPlanResourceIdentity `json:"identity"` + Inputs []MCPInstallationInput `json:"inputs"` + Policy MCPPlanPolicyResult `json:"policy"` + Provenance MCPPlanProvenance `json:"provenance"` + SecretStorage MCPInstallationSecretStorage `json:"secretStorage"` + SelectedChoice json.RawMessage `json:"selectedChoice"` + SuppliedSecrets []string `json:"suppliedSecrets"` + Target MCPPlanTarget `json:"target"` + } + var raw rawMCPInstallationReviewInstall + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.Catalogue = raw.Catalogue + if raw.CatalogueTrust != nil { + value, err := unmarshalCatalogTrustSnapshot(raw.CatalogueTrust) + if err != nil { + return err + } + r.CatalogueTrust = value + } + r.ConfigurationChange = raw.ConfigurationChange + r.EffectiveConfiguration = raw.EffectiveConfiguration + r.Identity = raw.Identity + r.Inputs = raw.Inputs + r.Policy = raw.Policy + r.Provenance = raw.Provenance + r.SecretStorage = raw.SecretStorage + if raw.SelectedChoice != nil { + value, err := unmarshalMCPPlanTransportChoice(raw.SelectedChoice) + if err != nil { + return err } + r.SelectedChoice = value } + r.SuppliedSecrets = raw.SuppliedSecrets + r.Target = raw.Target return nil } -func (r *MCPConfigUpdateRequest) UnmarshalJSON(data []byte) error { - type rawMCPConfigUpdateRequest struct { - Config json.RawMessage `json:"config"` - Name string `json:"name"` +func (r MCPInstallationReviewInstall) MarshalJSON() ([]byte, error) { + type alias MCPInstallationReviewInstall + return json.Marshal(struct { + Action MCPInstallationReviewAction `json:"action"` + alias + }{ + Action: r.Action(), + alias: alias(r), + }) +} + +func (r MCPInstallationReviewUninstall) MarshalJSON() ([]byte, error) { + type alias MCPInstallationReviewUninstall + return json.Marshal(struct { + Action MCPInstallationReviewAction `json:"action"` + alias + }{ + Action: r.Action(), + alias: alias(r), + }) +} + +func (r *InstallationReviewMCP) UnmarshalJSON(data []byte) error { + type rawInstallationReviewMCP struct { + Review json.RawMessage `json:"review"` } - var raw rawMCPConfigUpdateRequest + var raw rawInstallationReviewMCP if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Config != nil { - value, err := unmarshalMCPSerializableServerConfig(raw.Config) + if raw.Review != nil { + value, err := unmarshalMCPInstallationReview(raw.Review) if err != nil { return err } - r.Config = value + r.Review = value } - r.Name = raw.Name return nil } -func unmarshalMCPHeadersHandlePendingHeadersRefreshRequest(data []byte) (MCPHeadersHandlePendingHeadersRefreshRequest, error) { +func (r InstallationReviewMCP) MarshalJSON() ([]byte, error) { + type alias InstallationReviewMCP + return json.Marshal(struct { + Resource InstallationReviewResource `json:"resource"` + alias + }{ + Resource: r.Resource(), + alias: alias(r), + }) +} + +func unmarshalSkillInstallationReview(data []byte) (SkillInstallationReview, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + Action SkillInstallationReviewAction `json:"action"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - switch raw.Kind { - case MCPHeadersHandlePendingHeadersRefreshRequestKindError: - var d MCPHeadersHandlePendingHeadersRefreshRequestError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPHeadersHandlePendingHeadersRefreshRequestKindHeaders: - var d MCPHeadersHandlePendingHeadersRefreshRequestHeaders + switch raw.Action { + case SkillInstallationReviewActionInstall: + var d SkillInstallationReviewInstall if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPHeadersHandlePendingHeadersRefreshRequestKindNone: - var d MCPHeadersHandlePendingHeadersRefreshRequestNone + case SkillInstallationReviewActionUninstall: + var d SkillInstallationReviewUninstall if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawMCPHeadersHandlePendingHeadersRefreshRequestData{Discriminator: raw.Kind, Raw: data}, nil + return &RawSkillInstallationReviewData{Discriminator: raw.Action, Raw: data}, nil } } -func (r RawMCPHeadersHandlePendingHeadersRefreshRequestData) MarshalJSON() ([]byte, error) { +func (r RawSkillInstallationReviewData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + Action SkillInstallationReviewAction `json:"action"` }{ - Kind: r.Discriminator, + Action: r.Discriminator, }) } -func (r MCPHeadersHandlePendingHeadersRefreshRequestError) MarshalJSON() ([]byte, error) { - type alias MCPHeadersHandlePendingHeadersRefreshRequestError +func (r SkillInstallationReviewInstall) MarshalJSON() ([]byte, error) { + type alias SkillInstallationReviewInstall return json.Marshal(struct { - Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + Action SkillInstallationReviewAction `json:"action"` alias }{ - Kind: r.Kind(), - alias: alias(r), + Action: r.Action(), + alias: alias(r), }) } -func (r MCPHeadersHandlePendingHeadersRefreshRequestHeaders) MarshalJSON() ([]byte, error) { - type alias MCPHeadersHandlePendingHeadersRefreshRequestHeaders +func (r SkillInstallationReviewUninstall) MarshalJSON() ([]byte, error) { + type alias SkillInstallationReviewUninstall return json.Marshal(struct { - Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + Action SkillInstallationReviewAction `json:"action"` alias }{ - Kind: r.Kind(), - alias: alias(r), + Action: r.Action(), + alias: alias(r), }) } -func (r MCPHeadersHandlePendingHeadersRefreshRequestNone) MarshalJSON() ([]byte, error) { - type alias MCPHeadersHandlePendingHeadersRefreshRequestNone +func (r *InstallationReviewSkill) UnmarshalJSON(data []byte) error { + type rawInstallationReviewSkill struct { + Review json.RawMessage `json:"review"` + } + var raw rawInstallationReviewSkill + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Review != nil { + value, err := unmarshalSkillInstallationReview(raw.Review) + if err != nil { + return err + } + r.Review = value + } + return nil +} + +func (r InstallationReviewSkill) MarshalJSON() ([]byte, error) { + type alias InstallationReviewSkill return json.Marshal(struct { - Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + Resource InstallationReviewResource `json:"resource"` alias }{ - Kind: r.Kind(), - alias: alias(r), + Resource: r.Resource(), + alias: alias(r), }) } -func (r *MCPHeadersHandlePendingHeadersRefreshRequestRequest) UnmarshalJSON(data []byte) error { - type rawMCPHeadersHandlePendingHeadersRefreshRequestRequest struct { - RequestID string `json:"requestId"` - Result json.RawMessage `json:"result"` +func (r *InstallationConfirmationRequest) UnmarshalJSON(data []byte) error { + type rawInstallationConfirmationRequest struct { + ConfirmationID string `json:"confirmationId"` + ExpiresAt string `json:"expiresAt"` + OperationID string `json:"operationId"` + PolicySessionID *string `json:"policySessionId,omitempty"` + Review json.RawMessage `json:"review"` + ReviewFingerprint string `json:"reviewFingerprint"` } - var raw rawMCPHeadersHandlePendingHeadersRefreshRequestRequest + var raw rawInstallationConfirmationRequest if err := json.Unmarshal(data, &raw); err != nil { return err } - r.RequestID = raw.RequestID - if raw.Result != nil { - value, err := unmarshalMCPHeadersHandlePendingHeadersRefreshRequest(raw.Result) + r.ConfirmationID = raw.ConfirmationID + r.ExpiresAt = raw.ExpiresAt + r.OperationID = raw.OperationID + r.PolicySessionID = raw.PolicySessionID + if raw.Review != nil { + value, err := unmarshalInstallationReview(raw.Review) if err != nil { return err } - r.Result = value + r.Review = value } + r.ReviewFingerprint = raw.ReviewFingerprint return nil } -func unmarshalMCPPlanTransportChoice(data []byte) (MCPPlanTransportChoice, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Transport MCPPlanTransportChoiceTransport `json:"transport"` +func matchesMCPSerializableServerConfigMCPServerConfigHTTP(data []byte) bool { + var rawGroup0 struct { + Command json.RawMessage `json:"command"` + URL json.RawMessage `json:"url"` } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err + if err := json.Unmarshal(data, &rawGroup0); err != nil { + return false } - - switch raw.Transport { - case MCPPlanTransportChoiceTransportHTTP: - var d MCPPlanTransportChoiceRemote - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanTransportChoiceTransportSSE: - var d MCPPlanTransportChoiceRemote - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanTransportChoiceTransportStdio: - var d MCPPlanTransportChoicePackage - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanTransportChoiceTransportStreamableHTTP: - var d MCPPlanTransportChoiceRemote - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawMCPPlanTransportChoiceData{Discriminator: raw.Transport, Raw: data}, nil + if rawGroup0.URL == nil { + return false } + return rawGroup0.Command == nil } -func (r RawMCPPlanTransportChoiceData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil +func matchesMCPSerializableServerConfigMCPServerConfigStdio(data []byte) bool { + var rawGroup0 struct { + Command json.RawMessage `json:"command"` + URL json.RawMessage `json:"url"` } - return json.Marshal(struct { - Transport MCPPlanTransportChoiceTransport `json:"transport"` - }{ - Transport: r.Discriminator, - }) + if err := json.Unmarshal(data, &rawGroup0); err != nil { + return false + } + if rawGroup0.Command == nil { + return false + } + return rawGroup0.URL == nil } -func unmarshalMCPPlanRequiredValue(data []byte) (MCPPlanRequiredValue, error) { +func unmarshalMCPSerializableServerConfig(data []byte) (MCPSerializableServerConfig, error) { if string(data) == "null" { return nil, nil } - type rawUnion struct { - Kind MCPPlanRequiredValueKind `json:"kind"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Kind { - case MCPPlanRequiredValueKindEnum: - var d MCPPlanRequiredValueEnum + if matchesMCPSerializableServerConfigMCPServerConfigHTTP(data) { + var d MCPServerConfigHTTP if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPPlanRequiredValueKindScalar: - var d MCPPlanRequiredValueScalar + } + if matchesMCPSerializableServerConfigMCPServerConfigStdio(data) { + var d MCPServerConfigStdio if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - default: - return &RawMCPPlanRequiredValueData{Discriminator: raw.Kind, Raw: data}, nil } + return &RawMCPSerializableServerConfigData{Raw: data}, nil } -func (r RawMCPPlanRequiredValueData) MarshalJSON() ([]byte, error) { +func (r RawMCPSerializableServerConfigData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } - return json.Marshal(struct { - Kind MCPPlanRequiredValueKind `json:"kind"` - }{ - Kind: r.Discriminator, - }) -} - -func (r MCPPlanRequiredValueEnum) MarshalJSON() ([]byte, error) { - type alias MCPPlanRequiredValueEnum - return json.Marshal(struct { - Kind MCPPlanRequiredValueKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r MCPPlanRequiredValueScalar) MarshalJSON() ([]byte, error) { - type alias MCPPlanRequiredValueScalar - return json.Marshal(struct { - Kind MCPPlanRequiredValueKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) + return []byte("null"), nil } -func (r *MCPPlanTransportChoicePackage) UnmarshalJSON(data []byte) error { - type rawMCPPlanTransportChoicePackage struct { - ChoiceID string `json:"choiceId"` - InstallMethod MCPPlanPackageInstallMethod `json:"installMethod"` - PackageIdentifier string `json:"packageIdentifier"` - PackageType string `json:"packageType"` - RequiredValues []json.RawMessage `json:"requiredValues"` - SecretPlaceholders []MCPPlanSecretPlaceholder `json:"secretPlaceholders"` +func unmarshalMCPServerAuthConfig(data []byte) (MCPServerAuthConfig, error) { + if string(data) == "null" { + return nil, nil } - var raw rawMCPPlanTransportChoicePackage - if err := json.Unmarshal(data, &raw); err != nil { - return err + { + var value bool + if err := json.Unmarshal(data, &value); err == nil { + return MCPServerAuthConfigBoolean(value), nil + } } - r.ChoiceID = raw.ChoiceID - r.InstallMethod = raw.InstallMethod - r.PackageIdentifier = raw.PackageIdentifier - r.PackageType = raw.PackageType - if raw.RequiredValues != nil { - r.RequiredValues = make([]MCPPlanRequiredValue, 0, len(raw.RequiredValues)) - for _, rawItem := range raw.RequiredValues { - value, err := unmarshalMCPPlanRequiredValue(rawItem) - if err != nil { - return err - } - r.RequiredValues = append(r.RequiredValues, value) + { + var value MCPServerAuthConfigRedirectPort + if err := json.Unmarshal(data, &value); err == nil { + return &value, nil } } - r.SecretPlaceholders = raw.SecretPlaceholders - return nil -} - -func (r MCPPlanTransportChoicePackage) MarshalJSON() ([]byte, error) { - type alias MCPPlanTransportChoicePackage - return json.Marshal(struct { - Transport MCPPlanTransportChoiceTransport `json:"transport"` - alias - }{ - Transport: r.Transport(), - alias: alias(r), - }) + return nil, errors.New("data did not match any union variant for MCPServerAuthConfig") } -func (r *MCPPlanTransportChoiceRemote) UnmarshalJSON(data []byte) error { - type rawMCPPlanTransportChoiceRemote struct { - ChoiceID string `json:"choiceId"` - Endpoint string `json:"endpoint"` - InstallMethod MCPPlanRemoteInstallMethod `json:"installMethod"` - RequiredValues []json.RawMessage `json:"requiredValues"` - SecretPlaceholders []MCPPlanSecretPlaceholder `json:"secretPlaceholders"` - Discriminator MCPPlanRemoteTransport `json:"transport,omitempty"` +func unmarshalMCPSafeForTelemetry(data []byte) (MCPSafeForTelemetry, error) { + if string(data) == "null" { + return nil, nil } - var raw rawMCPPlanTransportChoiceRemote - if err := json.Unmarshal(data, &raw); err != nil { - return err + { + var value bool + if err := json.Unmarshal(data, &value); err == nil { + return MCPSafeForTelemetryBoolean(value), nil + } } - r.ChoiceID = raw.ChoiceID - r.Endpoint = raw.Endpoint - r.InstallMethod = raw.InstallMethod - if raw.RequiredValues != nil { - r.RequiredValues = make([]MCPPlanRequiredValue, 0, len(raw.RequiredValues)) - for _, rawItem := range raw.RequiredValues { - value, err := unmarshalMCPPlanRequiredValue(rawItem) - if err != nil { - return err - } - r.RequiredValues = append(r.RequiredValues, value) + { + var value MCPSafeForTelemetryFields + if err := json.Unmarshal(data, &value); err == nil { + return &value, nil } } - r.SecretPlaceholders = raw.SecretPlaceholders - r.Discriminator = raw.Discriminator - return nil -} - -func (r MCPPlanTransportChoiceRemote) MarshalJSON() ([]byte, error) { - type alias MCPPlanTransportChoiceRemote - return json.Marshal(struct { - Transport MCPPlanTransportChoiceTransport `json:"transport"` - alias - }{ - Transport: r.Transport(), - alias: alias(r), - }) + return nil, errors.New("data did not match any union variant for MCPSafeForTelemetry") } -func (r *MCPInstallPlan) UnmarshalJSON(data []byte) error { - type rawMCPInstallPlan struct { - ConfigurationChanges []MCPPlanConfigurationChange `json:"configurationChanges"` - Identity MCPPlanResourceIdentity `json:"identity"` - PlanHandle string `json:"planHandle"` - PlanHandleExpiresAt string `json:"planHandleExpiresAt"` - Policy MCPPlanPolicyResult `json:"policy"` - Provenance MCPPlanProvenance `json:"provenance"` - RecommendedTransportChoiceID *string `json:"recommendedTransportChoiceId,omitempty"` - ReloadRequired bool `json:"reloadRequired"` - RequiresInteractiveConfiguration bool `json:"requiresInteractiveConfiguration"` - Target MCPPlanTarget `json:"target"` - TransportChoices []json.RawMessage `json:"transportChoices"` +func (r *MCPServerConfigHTTP) UnmarshalJSON(data []byte) error { + type rawMCPServerConfigHTTP struct { + Auth json.RawMessage `json:"auth,omitempty"` + ConfigWarnings []string `json:"configWarnings,omitzero"` + DeferTools *MCPServerConfigDeferTools `json:"deferTools,omitempty"` + DisableSecretMasking *bool `json:"disableSecretMasking,omitempty"` + DisableToolCache *bool `json:"disableToolCache,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + Events []string `json:"events,omitzero"` + ExcludeTools []string `json:"excludeTools,omitzero"` + FilterMapping json.RawMessage `json:"filterMapping,omitempty"` + Headers map[string]string `json:"headers,omitzero"` + HeadersRefreshTtlMs *int64 `json:"headersRefreshTtlMs,omitempty"` + IsDefaultServer *bool `json:"isDefaultServer,omitempty"` + Notifications []string `json:"notifications,omitzero"` + OauthClientID *string `json:"oauthClientId,omitempty"` + OauthGrantType *MCPServerConfigHTTPOauthGrantType `json:"oauthGrantType,omitempty"` + OauthPublicClient *bool `json:"oauthPublicClient,omitempty"` + OauthScopes []string `json:"oauthScopes,omitzero"` + Oidc json.RawMessage `json:"oidc,omitempty"` + SafeForTelemetry json.RawMessage `json:"safeForTelemetry,omitempty"` + SlowConnectionThresholdMs *int64 `json:"slowConnectionThresholdMs,omitempty"` + Source *MCPServerSource `json:"source,omitempty"` + SourcePath *string `json:"sourcePath,omitempty"` + SourcePlugin *string `json:"sourcePlugin,omitempty"` + SourcePluginSpec *bool `json:"sourcePluginSpec,omitempty"` + SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` + Timeout *int64 `json:"timeout,omitempty"` + Tools []string `json:"tools,omitzero"` + Type *MCPServerConfigHTTPType `json:"type,omitempty"` + URL string `json:"url"` } - var raw rawMCPInstallPlan + var raw rawMCPServerConfigHTTP if err := json.Unmarshal(data, &raw); err != nil { return err } - r.ConfigurationChanges = raw.ConfigurationChanges - r.Identity = raw.Identity - r.PlanHandle = raw.PlanHandle - r.PlanHandleExpiresAt = raw.PlanHandleExpiresAt - r.Policy = raw.Policy - r.Provenance = raw.Provenance - r.RecommendedTransportChoiceID = raw.RecommendedTransportChoiceID - r.ReloadRequired = raw.ReloadRequired - r.RequiresInteractiveConfiguration = raw.RequiresInteractiveConfiguration - r.Target = raw.Target - if raw.TransportChoices != nil { - r.TransportChoices = make([]MCPPlanTransportChoice, 0, len(raw.TransportChoices)) - for _, rawItem := range raw.TransportChoices { - value, err := unmarshalMCPPlanTransportChoice(rawItem) + if raw.Auth != nil { + value, err := unmarshalMCPServerAuthConfig(raw.Auth) + if err != nil { + return err + } + r.Auth = value + } + r.ConfigWarnings = raw.ConfigWarnings + r.DeferTools = raw.DeferTools + r.DisableSecretMasking = raw.DisableSecretMasking + r.DisableToolCache = raw.DisableToolCache + r.DisplayName = raw.DisplayName + r.Events = raw.Events + r.ExcludeTools = raw.ExcludeTools + if raw.FilterMapping != nil { + value, err := unmarshalFilterMapping(raw.FilterMapping) + if err != nil { + return err + } + r.FilterMapping = value + } + r.Headers = raw.Headers + r.HeadersRefreshTtlMs = raw.HeadersRefreshTtlMs + r.IsDefaultServer = raw.IsDefaultServer + r.Notifications = raw.Notifications + r.OauthClientID = raw.OauthClientID + r.OauthGrantType = raw.OauthGrantType + r.OauthPublicClient = raw.OauthPublicClient + r.OauthScopes = raw.OauthScopes + if raw.Oidc != nil { + value, err := unmarshalMCPServerAuthConfig(raw.Oidc) + if err != nil { + return err + } + r.Oidc = value + } + if raw.SafeForTelemetry != nil { + value, err := unmarshalMCPSafeForTelemetry(raw.SafeForTelemetry) + if err != nil { + return err + } + r.SafeForTelemetry = value + } + r.SlowConnectionThresholdMs = raw.SlowConnectionThresholdMs + r.Source = raw.Source + r.SourcePath = raw.SourcePath + r.SourcePlugin = raw.SourcePlugin + r.SourcePluginSpec = raw.SourcePluginSpec + r.SourcePluginVersion = raw.SourcePluginVersion + r.Timeout = raw.Timeout + r.Tools = raw.Tools + r.Type = raw.Type + r.URL = raw.URL + return nil +} + +func (r *MCPServerConfigStdio) UnmarshalJSON(data []byte) error { + type rawMCPServerConfigStdio struct { + Args []string `json:"args,omitzero"` + Auth json.RawMessage `json:"auth,omitempty"` + Command string `json:"command"` + ConfigWarnings []string `json:"configWarnings,omitzero"` + Cwd *string `json:"cwd,omitempty"` + DeferTools *MCPServerConfigDeferTools `json:"deferTools,omitempty"` + DisableSecretMasking *bool `json:"disableSecretMasking,omitempty"` + DisableToolCache *bool `json:"disableToolCache,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + Env map[string]string `json:"env,omitzero"` + Events []string `json:"events,omitzero"` + ExcludeTools []string `json:"excludeTools,omitzero"` + FilterMapping json.RawMessage `json:"filterMapping,omitempty"` + IsDefaultServer *bool `json:"isDefaultServer,omitempty"` + Notifications []string `json:"notifications,omitzero"` + Oidc json.RawMessage `json:"oidc,omitempty"` + SafeForTelemetry json.RawMessage `json:"safeForTelemetry,omitempty"` + SlowConnectionThresholdMs *int64 `json:"slowConnectionThresholdMs,omitempty"` + Source *MCPServerSource `json:"source,omitempty"` + SourcePath *string `json:"sourcePath,omitempty"` + SourcePlugin *string `json:"sourcePlugin,omitempty"` + SourcePluginSpec *bool `json:"sourcePluginSpec,omitempty"` + SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` + Timeout *int64 `json:"timeout,omitempty"` + Tools []string `json:"tools,omitzero"` + Type *MCPServerConfigStdioType `json:"type,omitempty"` + } + var raw rawMCPServerConfigStdio + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.Args = raw.Args + if raw.Auth != nil { + value, err := unmarshalMCPServerAuthConfig(raw.Auth) + if err != nil { + return err + } + r.Auth = value + } + r.Command = raw.Command + r.ConfigWarnings = raw.ConfigWarnings + r.Cwd = raw.Cwd + r.DeferTools = raw.DeferTools + r.DisableSecretMasking = raw.DisableSecretMasking + r.DisableToolCache = raw.DisableToolCache + r.DisplayName = raw.DisplayName + r.Env = raw.Env + r.Events = raw.Events + r.ExcludeTools = raw.ExcludeTools + if raw.FilterMapping != nil { + value, err := unmarshalFilterMapping(raw.FilterMapping) + if err != nil { + return err + } + r.FilterMapping = value + } + r.IsDefaultServer = raw.IsDefaultServer + r.Notifications = raw.Notifications + if raw.Oidc != nil { + value, err := unmarshalMCPServerAuthConfig(raw.Oidc) + if err != nil { + return err + } + r.Oidc = value + } + if raw.SafeForTelemetry != nil { + value, err := unmarshalMCPSafeForTelemetry(raw.SafeForTelemetry) + if err != nil { + return err + } + r.SafeForTelemetry = value + } + r.SlowConnectionThresholdMs = raw.SlowConnectionThresholdMs + r.Source = raw.Source + r.SourcePath = raw.SourcePath + r.SourcePlugin = raw.SourcePlugin + r.SourcePluginSpec = raw.SourcePluginSpec + r.SourcePluginVersion = raw.SourcePluginVersion + r.Timeout = raw.Timeout + r.Tools = raw.Tools + r.Type = raw.Type + return nil +} + +func (r *MCPConfigAddRequest) UnmarshalJSON(data []byte) error { + type rawMCPConfigAddRequest struct { + Config json.RawMessage `json:"config"` + Name string `json:"name"` + } + var raw rawMCPConfigAddRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Config != nil { + value, err := unmarshalMCPSerializableServerConfig(raw.Config) + if err != nil { + return err + } + r.Config = value + } + r.Name = raw.Name + return nil +} + +func (r *MCPConfigList) UnmarshalJSON(data []byte) error { + type rawMCPConfigList struct { + Servers map[string]json.RawMessage `json:"servers"` + } + var raw rawMCPConfigList + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Servers != nil { + r.Servers = make(map[string]MCPSerializableServerConfig, len(raw.Servers)) + for key, rawValue := range raw.Servers { + value, err := unmarshalMCPSerializableServerConfig(rawValue) if err != nil { return err } - r.TransportChoices = append(r.TransportChoices, value) + r.Servers[key] = value } } return nil } -func unmarshalMCPOauthPendingRequestResponse(data []byte) (MCPOauthPendingRequestResponse, error) { +func (r *MCPConfigUpdateRequest) UnmarshalJSON(data []byte) error { + type rawMCPConfigUpdateRequest struct { + Config json.RawMessage `json:"config"` + Name string `json:"name"` + } + var raw rawMCPConfigUpdateRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Config != nil { + value, err := unmarshalMCPSerializableServerConfig(raw.Config) + if err != nil { + return err + } + r.Config = value + } + r.Name = raw.Name + return nil +} + +func unmarshalMCPHeadersHandlePendingHeadersRefreshRequest(data []byte) (MCPHeadersHandlePendingHeadersRefreshRequest, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind MCPOauthPendingRequestResponseKind `json:"kind"` + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -3429,38 +3958,44 @@ func unmarshalMCPOauthPendingRequestResponse(data []byte) (MCPOauthPendingReques } switch raw.Kind { - case MCPOauthPendingRequestResponseKindCancelled: - var d MCPOauthPendingRequestResponseCancelled + case MCPHeadersHandlePendingHeadersRefreshRequestKindError: + var d MCPHeadersHandlePendingHeadersRefreshRequestError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPOauthPendingRequestResponseKindToken: - var d MCPOauthPendingRequestResponseToken + case MCPHeadersHandlePendingHeadersRefreshRequestKindHeaders: + var d MCPHeadersHandlePendingHeadersRefreshRequestHeaders + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPHeadersHandlePendingHeadersRefreshRequestKindNone: + var d MCPHeadersHandlePendingHeadersRefreshRequestNone if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawMCPOauthPendingRequestResponseData{Discriminator: raw.Kind, Raw: data}, nil + return &RawMCPHeadersHandlePendingHeadersRefreshRequestData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawMCPOauthPendingRequestResponseData) MarshalJSON() ([]byte, error) { +func (r RawMCPHeadersHandlePendingHeadersRefreshRequestData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind MCPOauthPendingRequestResponseKind `json:"kind"` + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` }{ Kind: r.Discriminator, }) } -func (r MCPOauthPendingRequestResponseCancelled) MarshalJSON() ([]byte, error) { - type alias MCPOauthPendingRequestResponseCancelled +func (r MCPHeadersHandlePendingHeadersRefreshRequestError) MarshalJSON() ([]byte, error) { + type alias MCPHeadersHandlePendingHeadersRefreshRequestError return json.Marshal(struct { - Kind MCPOauthPendingRequestResponseKind `json:"kind"` + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -3468,10 +4003,10 @@ func (r MCPOauthPendingRequestResponseCancelled) MarshalJSON() ([]byte, error) { }) } -func (r MCPOauthPendingRequestResponseToken) MarshalJSON() ([]byte, error) { - type alias MCPOauthPendingRequestResponseToken +func (r MCPHeadersHandlePendingHeadersRefreshRequestHeaders) MarshalJSON() ([]byte, error) { + type alias MCPHeadersHandlePendingHeadersRefreshRequestHeaders return json.Marshal(struct { - Kind MCPOauthPendingRequestResponseKind `json:"kind"` + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -3479,18 +4014,29 @@ func (r MCPOauthPendingRequestResponseToken) MarshalJSON() ([]byte, error) { }) } -func (r *MCPOauthHandlePendingRequest) UnmarshalJSON(data []byte) error { - type rawMCPOauthHandlePendingRequest struct { - RequestID string `json:"requestId"` - Result json.RawMessage `json:"result"` - } - var raw rawMCPOauthHandlePendingRequest - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - r.RequestID = raw.RequestID +func (r MCPHeadersHandlePendingHeadersRefreshRequestNone) MarshalJSON() ([]byte, error) { + type alias MCPHeadersHandlePendingHeadersRefreshRequestNone + return json.Marshal(struct { + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *MCPHeadersHandlePendingHeadersRefreshRequestRequest) UnmarshalJSON(data []byte) error { + type rawMCPHeadersHandlePendingHeadersRefreshRequestRequest struct { + RequestID string `json:"requestId"` + Result json.RawMessage `json:"result"` + } + var raw rawMCPHeadersHandlePendingHeadersRefreshRequestRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.RequestID = raw.RequestID if raw.Result != nil { - value, err := unmarshalMCPOauthPendingRequestResponse(raw.Result) + value, err := unmarshalMCPHeadersHandlePendingHeadersRefreshRequest(raw.Result) if err != nil { return err } @@ -3499,161 +4045,192 @@ func (r *MCPOauthHandlePendingRequest) UnmarshalJSON(data []byte) error { return nil } -func unmarshalMCPOauthProbeResult(data []byte) (MCPOauthProbeResult, error) { +func unmarshalMCPInstallationManagementOutcome(data []byte) (MCPInstallationManagementOutcome, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Status MCPOauthProbeResultStatus `json:"status"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - switch raw.Status { - case MCPOauthProbeResultStatusAuthenticated: - var d MCPOauthProbeResultAuthenticated + switch raw.Kind { + case MCPInstallationManagementOutcomeKindInstallPrepared: + var d MCPInstallationManagementOutcomeInstallPrepared if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPOauthProbeResultStatusFailed: - var d MCPOauthProbeResultFailed + case MCPInstallationManagementOutcomeKindListed: + var d MCPInstallationManagementOutcomeListed if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPOauthProbeResultStatusNeedsAuth: - var d MCPOauthProbeResultNeedsAuth + case MCPInstallationManagementOutcomeKindOperation: + var d MCPInstallationManagementOutcomeOperation if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPOauthProbeResultStatusNoAuthRequired: - var d MCPOauthProbeResultNoAuthRequired + case MCPInstallationManagementOutcomeKindRecovered: + var d MCPInstallationManagementOutcomeRecovered + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationManagementOutcomeKindRecoveryRequired: + var d MCPInstallationManagementOutcomeRecoveryRequired + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationManagementOutcomeKindRefused: + var d MCPInstallationManagementOutcomeRefused + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationManagementOutcomeKindUninstallPlanned: + var d MCPInstallationManagementOutcomeUninstallPlanned if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawMCPOauthProbeResultData{Discriminator: raw.Status, Raw: data}, nil + return &RawMCPInstallationManagementOutcomeData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawMCPOauthProbeResultData) MarshalJSON() ([]byte, error) { +func (r RawMCPInstallationManagementOutcomeData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Status MCPOauthProbeResultStatus `json:"status"` - }{ - Status: r.Discriminator, - }) -} - -func (r MCPOauthProbeResultAuthenticated) MarshalJSON() ([]byte, error) { - type alias MCPOauthProbeResultAuthenticated - return json.Marshal(struct { - Status MCPOauthProbeResultStatus `json:"status"` - alias - }{ - Status: r.Status(), - alias: alias(r), - }) -} - -func (r MCPOauthProbeResultFailed) MarshalJSON() ([]byte, error) { - type alias MCPOauthProbeResultFailed - return json.Marshal(struct { - Status MCPOauthProbeResultStatus `json:"status"` - alias + Kind MCPInstallationManagementOutcomeKind `json:"kind"` }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Discriminator, }) } -func (r MCPOauthProbeResultNeedsAuth) MarshalJSON() ([]byte, error) { - type alias MCPOauthProbeResultNeedsAuth +func (r MCPInstallationManagementOutcomeInstallPrepared) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementOutcomeInstallPrepared return json.Marshal(struct { - Status MCPOauthProbeResultStatus `json:"status"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` alias }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Kind(), + alias: alias(r), }) } -func (r MCPOauthProbeResultNoAuthRequired) MarshalJSON() ([]byte, error) { - type alias MCPOauthProbeResultNoAuthRequired +func (r MCPInstallationManagementOutcomeListed) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementOutcomeListed return json.Marshal(struct { - Status MCPOauthProbeResultStatus `json:"status"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` alias }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Kind(), + alias: alias(r), }) } -func unmarshalMCPPlanInstallSource(data []byte) (MCPPlanInstallSource, error) { +func unmarshalMCPInstallationOperationStatus(data []byte) (MCPInstallationOperationStatus, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind MCPPlanInstallSourceKind `json:"kind"` + Phase MCPInstallationOperationStatusPhase `json:"phase"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - switch raw.Kind { - case MCPPlanInstallSourceKindCandidate: - var d MCPPlanInstallSourceCandidate + switch raw.Phase { + case MCPInstallationOperationStatusPhaseApplying: + var d MCPInstallationOperationStatusApplying if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPPlanInstallSourceKindCard: - var d MCPPlanInstallSourceCard + case MCPInstallationOperationStatusPhaseAwaitingConfirmation: + var d MCPInstallationOperationStatusAwaitingConfirmation + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOperationStatusPhaseCompleted: + var d MCPInstallationOperationStatusCompleted + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOperationStatusPhasePrepared: + var d MCPInstallationOperationStatusPrepared + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOperationStatusPhasePreparing: + var d MCPInstallationOperationStatusPreparing + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOperationStatusPhaseRevalidating: + var d MCPInstallationOperationStatusRevalidating if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawMCPPlanInstallSourceData{Discriminator: raw.Kind, Raw: data}, nil + return &RawMCPInstallationOperationStatusData{Discriminator: raw.Phase, Raw: data}, nil } } -func (r RawMCPPlanInstallSourceData) MarshalJSON() ([]byte, error) { +func (r RawMCPInstallationOperationStatusData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind MCPPlanInstallSourceKind `json:"kind"` + Phase MCPInstallationOperationStatusPhase `json:"phase"` }{ - Kind: r.Discriminator, + Phase: r.Discriminator, }) } -func (r MCPPlanInstallSourceCandidate) MarshalJSON() ([]byte, error) { - type alias MCPPlanInstallSourceCandidate +func (r MCPInstallationOperationStatusApplying) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOperationStatusApplying return json.Marshal(struct { - Kind MCPPlanInstallSourceKind `json:"kind"` + Phase MCPInstallationOperationStatusPhase `json:"phase"` alias }{ - Kind: r.Kind(), + Phase: r.Phase(), alias: alias(r), }) } -func unmarshalMCPServerCardReference(data []byte) (MCPServerCardReference, error) { +func (r MCPInstallationOperationStatusAwaitingConfirmation) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOperationStatusAwaitingConfirmation + return json.Marshal(struct { + Phase MCPInstallationOperationStatusPhase `json:"phase"` + alias + }{ + Phase: r.Phase(), + alias: alias(r), + }) +} + +func unmarshalMCPInstallationOutcome(data []byte) (MCPInstallationOutcome, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind MCPServerCardReferenceKind `json:"kind"` + Kind MCPInstallationOutcomeKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -3661,38 +4238,68 @@ func unmarshalMCPServerCardReference(data []byte) (MCPServerCardReference, error } switch raw.Kind { - case MCPServerCardReferenceKindEmbedded: - var d MCPServerCardEmbedded + case MCPInstallationOutcomeKindCancelled: + var d MCPInstallationOutcomeCancelled if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case MCPServerCardReferenceKindURL: - var d MCPServerCardURL + case MCPInstallationOutcomeKindDeclined: + var d MCPInstallationOutcomeDeclined + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOutcomeKindInstalled: + var d MCPInstallationOutcomeInstalled + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOutcomeKindRecoveryRequired: + var d MCPInstallationOutcomeRecoveryRequired + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOutcomeKindRefused: + var d MCPInstallationOutcomeRefused + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOutcomeKindRolledBack: + var d MCPInstallationOutcomeRolledBack + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationOutcomeKindUninstalled: + var d MCPInstallationOutcomeUninstalled if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawMCPServerCardReferenceData{Discriminator: raw.Kind, Raw: data}, nil + return &RawMCPInstallationOutcomeData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawMCPServerCardReferenceData) MarshalJSON() ([]byte, error) { +func (r RawMCPInstallationOutcomeData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind MCPServerCardReferenceKind `json:"kind"` + Kind MCPInstallationOutcomeKind `json:"kind"` }{ Kind: r.Discriminator, }) } -func (r MCPServerCardEmbedded) MarshalJSON() ([]byte, error) { - type alias MCPServerCardEmbedded +func (r MCPInstallationOutcomeCancelled) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOutcomeCancelled return json.Marshal(struct { - Kind MCPServerCardReferenceKind `json:"kind"` + Kind MCPInstallationOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -3700,10 +4307,10 @@ func (r MCPServerCardEmbedded) MarshalJSON() ([]byte, error) { }) } -func (r MCPServerCardURL) MarshalJSON() ([]byte, error) { - type alias MCPServerCardURL +func (r MCPInstallationOutcomeDeclined) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOutcomeDeclined return json.Marshal(struct { - Kind MCPServerCardReferenceKind `json:"kind"` + Kind MCPInstallationOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -3711,611 +4318,583 @@ func (r MCPServerCardURL) MarshalJSON() ([]byte, error) { }) } -func (r *MCPPlanInstallSourceCard) UnmarshalJSON(data []byte) error { - type rawMCPPlanInstallSourceCard struct { - Card json.RawMessage `json:"card"` +func (r MCPInstallationOutcomeInstalled) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOutcomeInstalled + return json.Marshal(struct { + Kind MCPInstallationOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r MCPInstallationOutcomeRecoveryRequired) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOutcomeRecoveryRequired + return json.Marshal(struct { + Kind MCPInstallationOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r MCPInstallationOutcomeRefused) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOutcomeRefused + return json.Marshal(struct { + Kind MCPInstallationOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r MCPInstallationOutcomeRolledBack) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOutcomeRolledBack + return json.Marshal(struct { + Kind MCPInstallationOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r MCPInstallationOutcomeUninstalled) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOutcomeUninstalled + return json.Marshal(struct { + Kind MCPInstallationOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *MCPInstallationOperationStatusCompleted) UnmarshalJSON(data []byte) error { + type rawMCPInstallationOperationStatusCompleted struct { + CancellationRequested bool `json:"cancellationRequested"` + OperationID string `json:"operationId"` + Outcome json.RawMessage `json:"outcome"` } - var raw rawMCPPlanInstallSourceCard + var raw rawMCPInstallationOperationStatusCompleted if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Card != nil { - value, err := unmarshalMCPServerCardReference(raw.Card) + r.CancellationRequested = raw.CancellationRequested + r.OperationID = raw.OperationID + if raw.Outcome != nil { + value, err := unmarshalMCPInstallationOutcome(raw.Outcome) if err != nil { return err } - r.Card = value + r.Outcome = value } return nil } -func (r MCPPlanInstallSourceCard) MarshalJSON() ([]byte, error) { - type alias MCPPlanInstallSourceCard +func (r MCPInstallationOperationStatusCompleted) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOperationStatusCompleted return json.Marshal(struct { - Kind MCPPlanInstallSourceKind `json:"kind"` + Phase MCPInstallationOperationStatusPhase `json:"phase"` alias }{ - Kind: r.Kind(), + Phase: r.Phase(), alias: alias(r), }) } -func (r *MCPPlanInstallRequest) UnmarshalJSON(data []byte) error { - type rawMCPPlanInstallRequest struct { - Contract CatalogClientContract `json:"contract"` - Scope *MCPPlanScope `json:"scope,omitempty"` - Source json.RawMessage `json:"source"` +func (r MCPInstallationOperationStatusPrepared) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOperationStatusPrepared + return json.Marshal(struct { + Phase MCPInstallationOperationStatusPhase `json:"phase"` + alias + }{ + Phase: r.Phase(), + alias: alias(r), + }) +} + +func (r MCPInstallationOperationStatusPreparing) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOperationStatusPreparing + return json.Marshal(struct { + Phase MCPInstallationOperationStatusPhase `json:"phase"` + alias + }{ + Phase: r.Phase(), + alias: alias(r), + }) +} + +func (r MCPInstallationOperationStatusRevalidating) MarshalJSON() ([]byte, error) { + type alias MCPInstallationOperationStatusRevalidating + return json.Marshal(struct { + Phase MCPInstallationOperationStatusPhase `json:"phase"` + alias + }{ + Phase: r.Phase(), + alias: alias(r), + }) +} + +func (r *MCPInstallationManagementOutcomeOperation) UnmarshalJSON(data []byte) error { + type rawMCPInstallationManagementOutcomeOperation struct { + Operation json.RawMessage `json:"operation"` } - var raw rawMCPPlanInstallRequest + var raw rawMCPInstallationManagementOutcomeOperation if err := json.Unmarshal(data, &raw); err != nil { return err } - r.Contract = raw.Contract - r.Scope = raw.Scope - if raw.Source != nil { - value, err := unmarshalMCPPlanInstallSource(raw.Source) + if raw.Operation != nil { + value, err := unmarshalMCPInstallationOperationStatus(raw.Operation) if err != nil { return err } - r.Source = value + r.Operation = value } return nil } -func unmarshalMCPPlanInstallResult(data []byte) (MCPPlanInstallResult, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Kind MCPPlanInstallResultKind `json:"kind"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Kind { - case MCPPlanInstallResultKindAuthenticationRequired: - var d CatalogAuthenticationRequiredError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindContractViolation: - var d CatalogContractViolationError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindHandleRejected: - var d CatalogHandleRejectedError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindInvalidRequest: - var d CatalogInvalidRequestError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindMalformedCard: - var d CatalogMalformedCardError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindNegotiationRefused: - var d CatalogNegotiationRefusedError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindNetworkFailure: - var d CatalogNetworkFailureError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindNotInstallable: - var d CatalogNotInstallableError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindPlanned: - var d MCPPlanInstallPlanned - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindPolicyRejected: - var d CatalogPolicyRejectedError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindUnavailable: - var d CatalogUnavailableError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindUnavailableTransport: - var d CatalogUnavailableTransportError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case MCPPlanInstallResultKindUnsafeRetrieval: - var d CatalogUnsafeRetrievalError - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawMCPPlanInstallResultData{Discriminator: raw.Kind, Raw: data}, nil - } -} - -func (r RawMCPPlanInstallResultData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } +func (r MCPInstallationManagementOutcomeOperation) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementOutcomeOperation return json.Marshal(struct { - Kind MCPPlanInstallResultKind `json:"kind"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` + alias }{ - Kind: r.Discriminator, + Kind: r.Kind(), + alias: alias(r), }) } -func (r CatalogHandleRejectedError) MarshalJSON() ([]byte, error) { - type alias CatalogHandleRejectedError +func (r MCPInstallationManagementOutcomeRecovered) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementOutcomeRecovered return json.Marshal(struct { - Kind MCPPlanInstallResultKind `json:"kind"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` alias }{ - Kind: r.mcpPlanInstallResultKind(), + Kind: r.Kind(), alias: alias(r), }) } -func (r CatalogNotInstallableError) MarshalJSON() ([]byte, error) { - type alias CatalogNotInstallableError +func (r MCPInstallationManagementOutcomeRecoveryRequired) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementOutcomeRecoveryRequired return json.Marshal(struct { - Kind MCPPlanInstallResultKind `json:"kind"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` alias }{ - Kind: r.mcpPlanInstallResultKind(), + Kind: r.Kind(), alias: alias(r), }) } -func (r CatalogUnavailableTransportError) MarshalJSON() ([]byte, error) { - type alias CatalogUnavailableTransportError +func (r MCPInstallationManagementOutcomeRefused) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementOutcomeRefused return json.Marshal(struct { - Kind MCPPlanInstallResultKind `json:"kind"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` alias }{ - Kind: r.mcpPlanInstallResultKind(), + Kind: r.Kind(), alias: alias(r), }) } -func (r MCPPlanInstallPlanned) MarshalJSON() ([]byte, error) { - type alias MCPPlanInstallPlanned +func (r MCPInstallationManagementOutcomeUninstallPlanned) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementOutcomeUninstallPlanned return json.Marshal(struct { - Kind MCPPlanInstallResultKind `json:"kind"` + Kind MCPInstallationManagementOutcomeKind `json:"kind"` alias }{ - Kind: r.mcpPlanInstallResultKind(), + Kind: r.Kind(), alias: alias(r), }) } -func matchesMCPServerConfigHTTP(data []byte) bool { - var rawGroup0 struct { - Command json.RawMessage `json:"command"` - ServerInstance json.RawMessage `json:"serverInstance"` - Type json.RawMessage `json:"type"` - URL json.RawMessage `json:"url"` - } - if err := json.Unmarshal(data, &rawGroup0); err != nil { - return false - } - if rawGroup0.URL == nil { - return false +func unmarshalMCPInstallationManagementResult(data []byte) (MCPInstallationManagementResult, error) { + if string(data) == "null" { + return nil, nil } - if rawGroup0.Command != nil { - return false + type rawUnion struct { + Kind MCPInstallationManagementResultKind `json:"kind"` } - if rawGroup0.ServerInstance != nil { - return false + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err } - return rawGroup0.Type == nil -} -func matchesMCPServerConfigMemory(data []byte) bool { - var rawGroup0 struct { - Command json.RawMessage `json:"command"` - ServerInstance json.RawMessage `json:"serverInstance"` - Type json.RawMessage `json:"type"` - URL json.RawMessage `json:"url"` - } - if err := json.Unmarshal(data, &rawGroup0); err != nil { - return false - } - if rawGroup0.ServerInstance == nil { - return false - } - if rawGroup0.Type == nil { - return false - } - var rawGroup0Term1String string - if err := json.Unmarshal(rawGroup0.Type, &rawGroup0Term1String); err != nil { - return false - } - switch rawGroup0Term1String { - case "memory": + switch raw.Kind { + case MCPInstallationManagementResultKindInvalidRequest: + var d CatalogInvalidRequestError + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationManagementResultKindNegotiationRefused: + var d CatalogNegotiationRefusedError + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPInstallationManagementResultKindOutcome: + var d MCPInstallationManagementResultOutcome + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil default: - return false + return &RawMCPInstallationManagementResultData{Discriminator: raw.Kind, Raw: data}, nil } - if rawGroup0.Command != nil { - return false - } - return rawGroup0.URL == nil } -func matchesMCPServerConfigStdio(data []byte) bool { - var rawGroup0 struct { - Command json.RawMessage `json:"command"` - ServerInstance json.RawMessage `json:"serverInstance"` - Type json.RawMessage `json:"type"` - URL json.RawMessage `json:"url"` - } - if err := json.Unmarshal(data, &rawGroup0); err != nil { - return false +func (r RawMCPInstallationManagementResultData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } - if rawGroup0.Command == nil { - return false + return json.Marshal(struct { + Kind MCPInstallationManagementResultKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r *MCPInstallationManagementResultOutcome) UnmarshalJSON(data []byte) error { + type rawMCPInstallationManagementResultOutcome struct { + Negotiated CatalogNegotiatedContract `json:"negotiated"` + Outcome json.RawMessage `json:"outcome"` } - if rawGroup0.ServerInstance != nil { - return false + var raw rawMCPInstallationManagementResultOutcome + if err := json.Unmarshal(data, &raw); err != nil { + return err } - if rawGroup0.Type != nil { - return false + r.Negotiated = raw.Negotiated + if raw.Outcome != nil { + value, err := unmarshalMCPInstallationManagementOutcome(raw.Outcome) + if err != nil { + return err + } + r.Outcome = value } - return rawGroup0.URL == nil + return nil } -func unmarshalMCPServerConfig(data []byte) (MCPServerConfig, error) { +func (r MCPInstallationManagementResultOutcome) MarshalJSON() ([]byte, error) { + type alias MCPInstallationManagementResultOutcome + return json.Marshal(struct { + Kind MCPInstallationManagementResultKind `json:"kind"` + alias + }{ + Kind: r.mcpInstallationManagementResultKind(), + alias: alias(r), + }) +} + +func unmarshalMCPInstallationResult(data []byte) (MCPInstallationResult, error) { if string(data) == "null" { return nil, nil } - if matchesMCPServerConfigHTTP(data) { - var d MCPServerConfigHTTP + type rawUnion struct { + Kind MCPInstallationResultKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case MCPInstallationResultKindInvalidRequest: + var d CatalogInvalidRequestError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - } - if matchesMCPServerConfigMemory(data) { - var d MCPServerConfigMemory + case MCPInstallationResultKindNegotiationRefused: + var d CatalogNegotiationRefusedError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - } - if matchesMCPServerConfigStdio(data) { - var d MCPServerConfigStdio + case MCPInstallationResultKindOutcome: + var d MCPInstallationResultOutcome if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil + default: + return &RawMCPInstallationResultData{Discriminator: raw.Kind, Raw: data}, nil } - return &RawMCPServerConfigData{Raw: data}, nil } -func (r RawMCPServerConfigData) MarshalJSON() ([]byte, error) { +func (r RawMCPInstallationResultData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } - return []byte("null"), nil + return json.Marshal(struct { + Kind MCPInstallationResultKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) } -func (r *MCPServerConfigMemory) UnmarshalJSON(data []byte) error { - type rawMCPServerConfigMemory struct { - ConfigWarnings []string `json:"configWarnings,omitzero"` - DeferTools *MCPServerConfigDeferTools `json:"deferTools,omitempty"` - DisableSecretMasking *bool `json:"disableSecretMasking,omitempty"` - DisableToolCache *bool `json:"disableToolCache,omitempty"` - DisplayName *string `json:"displayName,omitempty"` - Events []string `json:"events,omitzero"` - ExcludeTools []string `json:"excludeTools,omitzero"` - FilterMapping json.RawMessage `json:"filterMapping,omitempty"` - IsDefaultServer *bool `json:"isDefaultServer,omitempty"` - Notifications []string `json:"notifications,omitzero"` - Oidc json.RawMessage `json:"oidc,omitempty"` - SafeForTelemetry json.RawMessage `json:"safeForTelemetry,omitempty"` - ServerInstance any `json:"serverInstance"` - SlowConnectionThresholdMs *int64 `json:"slowConnectionThresholdMs,omitempty"` - Source *MCPServerSource `json:"source,omitempty"` - SourcePath *string `json:"sourcePath,omitempty"` - SourcePlugin *string `json:"sourcePlugin,omitempty"` - SourcePluginSpec *bool `json:"sourcePluginSpec,omitempty"` - SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` - Timeout *int64 `json:"timeout,omitempty"` - Tools []string `json:"tools,omitzero"` - Type MCPServerConfigMemoryType `json:"type"` +func (r *MCPInstallationResultOutcome) UnmarshalJSON(data []byte) error { + type rawMCPInstallationResultOutcome struct { + Negotiated CatalogNegotiatedContract `json:"negotiated"` + Outcome json.RawMessage `json:"outcome"` } - var raw rawMCPServerConfigMemory + var raw rawMCPInstallationResultOutcome if err := json.Unmarshal(data, &raw); err != nil { return err } - r.ConfigWarnings = raw.ConfigWarnings - r.DeferTools = raw.DeferTools - r.DisableSecretMasking = raw.DisableSecretMasking - r.DisableToolCache = raw.DisableToolCache - r.DisplayName = raw.DisplayName - r.Events = raw.Events - r.ExcludeTools = raw.ExcludeTools - if raw.FilterMapping != nil { - value, err := unmarshalFilterMapping(raw.FilterMapping) - if err != nil { - return err - } - r.FilterMapping = value - } - r.IsDefaultServer = raw.IsDefaultServer - r.Notifications = raw.Notifications - if raw.Oidc != nil { - value, err := unmarshalMCPServerAuthConfig(raw.Oidc) - if err != nil { - return err - } - r.Oidc = value - } - if raw.SafeForTelemetry != nil { - value, err := unmarshalMCPSafeForTelemetry(raw.SafeForTelemetry) + r.Negotiated = raw.Negotiated + if raw.Outcome != nil { + value, err := unmarshalMCPInstallationOutcome(raw.Outcome) if err != nil { return err } - r.SafeForTelemetry = value + r.Outcome = value } - r.ServerInstance = raw.ServerInstance - r.SlowConnectionThresholdMs = raw.SlowConnectionThresholdMs - r.Source = raw.Source - r.SourcePath = raw.SourcePath - r.SourcePlugin = raw.SourcePlugin - r.SourcePluginSpec = raw.SourcePluginSpec - r.SourcePluginVersion = raw.SourcePluginVersion - r.Timeout = raw.Timeout - r.Tools = raw.Tools - r.Type = raw.Type return nil } -func (r *MCPReloadConfig) UnmarshalJSON(data []byte) error { - type rawMCPReloadConfig struct { - ActiveGitHubToken *string `json:"activeGitHubToken,omitempty"` - CLIEnabledServers []string `json:"cliEnabledServers,omitzero"` - ConfigFilter any `json:"configFilter,omitempty"` - DisabledServers []string `json:"disabledServers,omitzero"` - EnabledServers []string `json:"enabledServers,omitzero"` - ForceRestart *bool `json:"forceRestart,omitempty"` - GitHubMCPToolOptions any `json:"githubMcpToolOptions,omitempty"` - GitHubMCPUserOverride *bool `json:"githubMcpUserOverride,omitempty"` - IncludeWorkspaceSources *bool `json:"includeWorkspaceSources,omitempty"` - Mcp3pEnabled *bool `json:"mcp3pEnabled,omitempty"` - MCPServers map[string]json.RawMessage `json:"mcpServers"` - SecretStore any `json:"secretStore,omitempty"` - UseCachedToolSnapshots *bool `json:"useCachedToolSnapshots,omitempty"` +func (r MCPInstallationResultOutcome) MarshalJSON() ([]byte, error) { + type alias MCPInstallationResultOutcome + return json.Marshal(struct { + Kind MCPInstallationResultKind `json:"kind"` + alias + }{ + Kind: r.mcpInstallationResultKind(), + alias: alias(r), + }) +} + +func (r *MCPInstallPlan) UnmarshalJSON(data []byte) error { + type rawMCPInstallPlan struct { + ConfigurationChanges []MCPPlanConfigurationChange `json:"configurationChanges"` + Identity MCPPlanResourceIdentity `json:"identity"` + PlanHandle string `json:"planHandle"` + PlanHandleExpiresAt string `json:"planHandleExpiresAt"` + Policy MCPPlanPolicyResult `json:"policy"` + Provenance MCPPlanProvenance `json:"provenance"` + RecommendedTransportChoiceID *string `json:"recommendedTransportChoiceId,omitempty"` + ReloadRequired bool `json:"reloadRequired"` + RequiresInteractiveConfiguration bool `json:"requiresInteractiveConfiguration"` + Target MCPPlanTarget `json:"target"` + TransportChoices []json.RawMessage `json:"transportChoices"` } - var raw rawMCPReloadConfig + var raw rawMCPInstallPlan if err := json.Unmarshal(data, &raw); err != nil { return err } - r.ActiveGitHubToken = raw.ActiveGitHubToken - r.CLIEnabledServers = raw.CLIEnabledServers - r.ConfigFilter = raw.ConfigFilter - r.DisabledServers = raw.DisabledServers - r.EnabledServers = raw.EnabledServers - r.ForceRestart = raw.ForceRestart - r.GitHubMCPToolOptions = raw.GitHubMCPToolOptions - r.GitHubMCPUserOverride = raw.GitHubMCPUserOverride - r.IncludeWorkspaceSources = raw.IncludeWorkspaceSources - r.Mcp3pEnabled = raw.Mcp3pEnabled - if raw.MCPServers != nil { - r.MCPServers = make(map[string]MCPServerConfig, len(raw.MCPServers)) - for key, rawValue := range raw.MCPServers { - value, err := unmarshalMCPServerConfig(rawValue) + r.ConfigurationChanges = raw.ConfigurationChanges + r.Identity = raw.Identity + r.PlanHandle = raw.PlanHandle + r.PlanHandleExpiresAt = raw.PlanHandleExpiresAt + r.Policy = raw.Policy + r.Provenance = raw.Provenance + r.RecommendedTransportChoiceID = raw.RecommendedTransportChoiceID + r.ReloadRequired = raw.ReloadRequired + r.RequiresInteractiveConfiguration = raw.RequiresInteractiveConfiguration + r.Target = raw.Target + if raw.TransportChoices != nil { + r.TransportChoices = make([]MCPPlanTransportChoice, 0, len(raw.TransportChoices)) + for _, rawItem := range raw.TransportChoices { + value, err := unmarshalMCPPlanTransportChoice(rawItem) if err != nil { return err } - r.MCPServers[key] = value + r.TransportChoices = append(r.TransportChoices, value) } } - r.SecretStore = raw.SecretStore - r.UseCachedToolSnapshots = raw.UseCachedToolSnapshots return nil } -func (r *MCPRestartServerRequest) UnmarshalJSON(data []byte) error { - type rawMCPRestartServerRequest struct { - Config json.RawMessage `json:"config,omitempty"` - ServerName string `json:"serverName"` +func unmarshalMCPOauthPendingRequestResponse(data []byte) (MCPOauthPendingRequestResponse, error) { + if string(data) == "null" { + return nil, nil } - var raw rawMCPRestartServerRequest + type rawUnion struct { + Kind MCPOauthPendingRequestResponseKind `json:"kind"` + } + var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { - return err + return nil, err } - if raw.Config != nil { - value, err := unmarshalMCPSerializableServerConfig(raw.Config) - if err != nil { - return err + + switch raw.Kind { + case MCPOauthPendingRequestResponseKindCancelled: + var d MCPOauthPendingRequestResponseCancelled + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } - r.Config = value + return &d, nil + case MCPOauthPendingRequestResponseKindToken: + var d MCPOauthPendingRequestResponseToken + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawMCPOauthPendingRequestResponseData{Discriminator: raw.Kind, Raw: data}, nil } - r.ServerName = raw.ServerName - return nil } -func (r *MCPStartServerRequest) UnmarshalJSON(data []byte) error { - type rawMCPStartServerRequest struct { - Config json.RawMessage `json:"config,omitempty"` - ServerName string `json:"serverName"` +func (r RawMCPOauthPendingRequestResponseData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } - var raw rawMCPStartServerRequest + return json.Marshal(struct { + Kind MCPOauthPendingRequestResponseKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r MCPOauthPendingRequestResponseCancelled) MarshalJSON() ([]byte, error) { + type alias MCPOauthPendingRequestResponseCancelled + return json.Marshal(struct { + Kind MCPOauthPendingRequestResponseKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r MCPOauthPendingRequestResponseToken) MarshalJSON() ([]byte, error) { + type alias MCPOauthPendingRequestResponseToken + return json.Marshal(struct { + Kind MCPOauthPendingRequestResponseKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *MCPOauthHandlePendingRequest) UnmarshalJSON(data []byte) error { + type rawMCPOauthHandlePendingRequest struct { + RequestID string `json:"requestId"` + Result json.RawMessage `json:"result"` + } + var raw rawMCPOauthHandlePendingRequest if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Config != nil { - value, err := unmarshalMCPSerializableServerConfig(raw.Config) + r.RequestID = raw.RequestID + if raw.Result != nil { + value, err := unmarshalMCPOauthPendingRequestResponse(raw.Result) if err != nil { return err } - r.Config = value + r.Result = value } - r.ServerName = raw.ServerName return nil } -func unmarshalPermissionDecision(data []byte) (PermissionDecision, error) { +func unmarshalMCPOauthProbeResult(data []byte) (MCPOauthProbeResult, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind PermissionDecisionKind `json:"kind"` + Status MCPOauthProbeResultStatus `json:"status"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - switch raw.Kind { - case PermissionDecisionKindApproveForLocation: - var d PermissionDecisionApproveForLocation - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindApproveForSession: - var d PermissionDecisionApproveForSession - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindApproveOnce: - var d PermissionDecisionApproveOnce - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindApprovePermanently: - var d PermissionDecisionApprovePermanently - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindApproved: - var d PermissionDecisionApproved - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindApprovedForLocation: - var d PermissionDecisionApprovedForLocation + switch raw.Status { + case MCPOauthProbeResultStatusAuthenticated: + var d MCPOauthProbeResultAuthenticated if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionKindApprovedForSession: - var d PermissionDecisionApprovedForSession + case MCPOauthProbeResultStatusFailed: + var d MCPOauthProbeResultFailed if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionKindCancelled: - var d PermissionDecisionCancelled + case MCPOauthProbeResultStatusNeedsAuth: + var d MCPOauthProbeResultNeedsAuth if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionKindDeniedByContentExclusionPolicy: - var d PermissionDecisionDeniedByContentExclusionPolicy - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindDeniedByPermissionRequestHook: - var d PermissionDecisionDeniedByPermissionRequestHook - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindDeniedByRules: - var d PermissionDecisionDeniedByRules - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindDeniedInteractivelyByUser: - var d PermissionDecisionDeniedInteractivelyByUser - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindDeniedNoApprovalRuleAndCouldNotRequestFromUser: - var d PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindReject: - var d PermissionDecisionReject - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionKindUserNotAvailable: - var d PermissionDecisionUserNotAvailable + case MCPOauthProbeResultStatusNoAuthRequired: + var d MCPOauthProbeResultNoAuthRequired if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawPermissionDecisionData{Discriminator: raw.Kind, Raw: data}, nil + return &RawMCPOauthProbeResultData{Discriminator: raw.Status, Raw: data}, nil } } -func (r RawPermissionDecisionData) MarshalJSON() ([]byte, error) { +func (r RawMCPOauthProbeResultData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Status MCPOauthProbeResultStatus `json:"status"` }{ - Kind: r.Discriminator, + Status: r.Discriminator, }) } -func (r PermissionDecisionApproved) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproved +func (r MCPOauthProbeResultAuthenticated) MarshalJSON() ([]byte, error) { + type alias MCPOauthProbeResultAuthenticated return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Status MCPOauthProbeResultStatus `json:"status"` alias }{ - Kind: r.Kind(), - alias: alias(r), + Status: r.Status(), + alias: alias(r), }) } -func unmarshalUserToolSessionApproval(data []byte) (UserToolSessionApproval, error) { +func (r MCPOauthProbeResultFailed) MarshalJSON() ([]byte, error) { + type alias MCPOauthProbeResultFailed + return json.Marshal(struct { + Status MCPOauthProbeResultStatus `json:"status"` + alias + }{ + Status: r.Status(), + alias: alias(r), + }) +} + +func (r MCPOauthProbeResultNeedsAuth) MarshalJSON() ([]byte, error) { + type alias MCPOauthProbeResultNeedsAuth + return json.Marshal(struct { + Status MCPOauthProbeResultStatus `json:"status"` + alias + }{ + Status: r.Status(), + alias: alias(r), + }) +} + +func (r MCPOauthProbeResultNoAuthRequired) MarshalJSON() ([]byte, error) { + type alias MCPOauthProbeResultNoAuthRequired + return json.Marshal(struct { + Status MCPOauthProbeResultStatus `json:"status"` + alias + }{ + Status: r.Status(), + alias: alias(r), + }) +} + +func unmarshalMCPPlanInstallSource(data []byte) (MCPPlanInstallSource, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind UserToolSessionApprovalKind `json:"kind"` + Kind MCPPlanInstallSourceKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -4323,86 +4902,38 @@ func unmarshalUserToolSessionApproval(data []byte) (UserToolSessionApproval, err } switch raw.Kind { - case UserToolSessionApprovalKindCommands: - var d UserToolSessionApprovalCommands - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindCustomTool: - var d UserToolSessionApprovalCustomTool - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindExtensionEnvAccess: - var d UserToolSessionApprovalExtensionEnvAccess - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindExtensionManagement: - var d UserToolSessionApprovalExtensionManagement - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindExtensionPermissionAccess: - var d UserToolSessionApprovalExtensionPermissionAccess - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindFactory: - var d UserToolSessionApprovalFactory - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindMCP: - var d UserToolSessionApprovalMCP - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindMemory: - var d UserToolSessionApprovalMemory - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case UserToolSessionApprovalKindRead: - var d UserToolSessionApprovalRead + case MCPPlanInstallSourceKindCandidate: + var d MCPPlanInstallSourceCandidate if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case UserToolSessionApprovalKindWrite: - var d UserToolSessionApprovalWrite + case MCPPlanInstallSourceKindCard: + var d MCPPlanInstallSourceCard if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawUserToolSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil + return &RawMCPPlanInstallSourceData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawUserToolSessionApprovalData) MarshalJSON() ([]byte, error) { +func (r RawMCPPlanInstallSourceData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` + Kind MCPPlanInstallSourceKind `json:"kind"` }{ Kind: r.Discriminator, }) } -func (r UserToolSessionApprovalCommands) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalCommands +func (r MCPPlanInstallSourceCandidate) MarshalJSON() ([]byte, error) { + type alias MCPPlanInstallSourceCandidate return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` + Kind MCPPlanInstallSourceKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -4410,43 +4941,51 @@ func (r UserToolSessionApprovalCommands) MarshalJSON() ([]byte, error) { }) } -func (r UserToolSessionApprovalCustomTool) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalCustomTool - return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} +func unmarshalMCPServerCardReference(data []byte) (MCPServerCardReference, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind MCPServerCardReferenceKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } -func (r UserToolSessionApprovalExtensionEnvAccess) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalExtensionEnvAccess - return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) + switch raw.Kind { + case MCPServerCardReferenceKindEmbedded: + var d MCPServerCardEmbedded + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPServerCardReferenceKindURL: + var d MCPServerCardURL + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawMCPServerCardReferenceData{Discriminator: raw.Kind, Raw: data}, nil + } } -func (r UserToolSessionApprovalExtensionManagement) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalExtensionManagement +func (r RawMCPServerCardReferenceData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` - alias + Kind MCPServerCardReferenceKind `json:"kind"` }{ - Kind: r.Kind(), - alias: alias(r), + Kind: r.Discriminator, }) } -func (r UserToolSessionApprovalExtensionPermissionAccess) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalExtensionPermissionAccess +func (r MCPServerCardEmbedded) MarshalJSON() ([]byte, error) { + type alias MCPServerCardEmbedded return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` + Kind MCPServerCardReferenceKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -4454,10 +4993,10 @@ func (r UserToolSessionApprovalExtensionPermissionAccess) MarshalJSON() ([]byte, }) } -func (r UserToolSessionApprovalFactory) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalFactory +func (r MCPServerCardURL) MarshalJSON() ([]byte, error) { + type alias MCPServerCardURL return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` + Kind MCPServerCardReferenceKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -4465,21 +5004,28 @@ func (r UserToolSessionApprovalFactory) MarshalJSON() ([]byte, error) { }) } -func (r UserToolSessionApprovalMCP) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalMCP - return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) +func (r *MCPPlanInstallSourceCard) UnmarshalJSON(data []byte) error { + type rawMCPPlanInstallSourceCard struct { + Card json.RawMessage `json:"card"` + } + var raw rawMCPPlanInstallSourceCard + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Card != nil { + value, err := unmarshalMCPServerCardReference(raw.Card) + if err != nil { + return err + } + r.Card = value + } + return nil } -func (r UserToolSessionApprovalMemory) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalMemory +func (r MCPPlanInstallSourceCard) MarshalJSON() ([]byte, error) { + type alias MCPPlanInstallSourceCard return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` + Kind MCPPlanInstallSourceKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -4487,94 +5033,36 @@ func (r UserToolSessionApprovalMemory) MarshalJSON() ([]byte, error) { }) } -func (r UserToolSessionApprovalRead) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalRead - return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r UserToolSessionApprovalWrite) MarshalJSON() ([]byte, error) { - type alias UserToolSessionApprovalWrite - return json.Marshal(struct { - Kind UserToolSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r *PermissionDecisionApprovedForLocation) UnmarshalJSON(data []byte) error { - type rawPermissionDecisionApprovedForLocation struct { - Approval json.RawMessage `json:"approval"` - LocationKey string `json:"locationKey"` - } - var raw rawPermissionDecisionApprovedForLocation - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.Approval != nil { - value, err := unmarshalUserToolSessionApproval(raw.Approval) - if err != nil { - return err - } - r.Approval = value - } - r.LocationKey = raw.LocationKey - return nil -} - -func (r PermissionDecisionApprovedForLocation) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApprovedForLocation - return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r *PermissionDecisionApprovedForSession) UnmarshalJSON(data []byte) error { - type rawPermissionDecisionApprovedForSession struct { - Approval json.RawMessage `json:"approval"` +func (r *MCPPlanInstallRequest) UnmarshalJSON(data []byte) error { + type rawMCPPlanInstallRequest struct { + Contract CatalogClientContract `json:"contract"` + PolicySessionID *string `json:"policySessionId,omitempty"` + Scope *MCPPlanScope `json:"scope,omitempty"` + Source json.RawMessage `json:"source"` } - var raw rawPermissionDecisionApprovedForSession + var raw rawMCPPlanInstallRequest if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Approval != nil { - value, err := unmarshalUserToolSessionApproval(raw.Approval) + r.Contract = raw.Contract + r.PolicySessionID = raw.PolicySessionID + r.Scope = raw.Scope + if raw.Source != nil { + value, err := unmarshalMCPPlanInstallSource(raw.Source) if err != nil { return err } - r.Approval = value + r.Source = value } return nil } -func (r PermissionDecisionApprovedForSession) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApprovedForSession - return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func unmarshalPermissionDecisionApproveForLocationApproval(data []byte) (PermissionDecisionApproveForLocationApproval, error) { +func unmarshalMCPPlanInstallResult(data []byte) (MCPPlanInstallResult, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` + Kind MCPPlanInstallResultKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -4582,500 +5070,669 @@ func unmarshalPermissionDecisionApproveForLocationApproval(data []byte) (Permiss } switch raw.Kind { - case PermissionDecisionApproveForLocationApprovalKindCommands: - var d PermissionDecisionApproveForLocationApprovalCommands + case MCPPlanInstallResultKindAuthenticationRequired: + var d CatalogAuthenticationRequiredError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindCustomTool: - var d PermissionDecisionApproveForLocationApprovalCustomTool + case MCPPlanInstallResultKindContractViolation: + var d CatalogContractViolationError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindExtensionEnvAccess: - var d PermissionDecisionApproveForLocationApprovalExtensionEnvAccess + case MCPPlanInstallResultKindHandleRejected: + var d CatalogHandleRejectedError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindExtensionManagement: - var d PermissionDecisionApproveForLocationApprovalExtensionManagement + case MCPPlanInstallResultKindInvalidRequest: + var d CatalogInvalidRequestError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindExtensionPermissionAccess: - var d PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess + case MCPPlanInstallResultKindMalformedCard: + var d CatalogMalformedCardError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindFactory: - var d PermissionDecisionApproveForLocationApprovalFactory + case MCPPlanInstallResultKindNegotiationRefused: + var d CatalogNegotiationRefusedError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindMCP: - var d PermissionDecisionApproveForLocationApprovalMCP + case MCPPlanInstallResultKindNetworkFailure: + var d CatalogNetworkFailureError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindMCPSampling: - var d PermissionDecisionApproveForLocationApprovalMCPSampling + case MCPPlanInstallResultKindNotInstallable: + var d CatalogNotInstallableError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindMemory: - var d PermissionDecisionApproveForLocationApprovalMemory + case MCPPlanInstallResultKindPlanned: + var d MCPPlanInstallPlanned if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindRead: - var d PermissionDecisionApproveForLocationApprovalRead + case MCPPlanInstallResultKindPolicyRejected: + var d CatalogPolicyRejectedError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForLocationApprovalKindWrite: - var d PermissionDecisionApproveForLocationApprovalWrite + case MCPPlanInstallResultKindUnavailable: + var d CatalogUnavailableError + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPPlanInstallResultKindUnavailableTransport: + var d CatalogUnavailableTransportError + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPPlanInstallResultKindUnsafeRetrieval: + var d CatalogUnsafeRetrievalError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawPermissionDecisionApproveForLocationApprovalData{Discriminator: raw.Kind, Raw: data}, nil + return &RawMCPPlanInstallResultData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawPermissionDecisionApproveForLocationApprovalData) MarshalJSON() ([]byte, error) { +func (r RawMCPPlanInstallResultData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` + Kind MCPPlanInstallResultKind `json:"kind"` }{ Kind: r.Discriminator, }) } -func (r PermissionDecisionApproveForLocationApprovalCommands) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalCommands - return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForLocationApprovalCustomTool) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalCustomTool - return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForLocationApprovalExtensionEnvAccess) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalExtensionEnvAccess - return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForLocationApprovalExtensionManagement) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalExtensionManagement - return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess - return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForLocationApprovalFactory) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalFactory - return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForLocationApprovalMCP) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalMCP - return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForLocationApprovalMCPSampling) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalMCPSampling +func (r CatalogHandleRejectedError) MarshalJSON() ([]byte, error) { + type alias CatalogHandleRejectedError return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` + Kind MCPPlanInstallResultKind `json:"kind"` alias }{ - Kind: r.Kind(), + Kind: r.mcpPlanInstallResultKind(), alias: alias(r), }) } -func (r PermissionDecisionApproveForLocationApprovalMemory) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalMemory +func (r CatalogNotInstallableError) MarshalJSON() ([]byte, error) { + type alias CatalogNotInstallableError return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` + Kind MCPPlanInstallResultKind `json:"kind"` alias }{ - Kind: r.Kind(), + Kind: r.mcpPlanInstallResultKind(), alias: alias(r), }) } -func (r PermissionDecisionApproveForLocationApprovalRead) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalRead +func (r CatalogUnavailableTransportError) MarshalJSON() ([]byte, error) { + type alias CatalogUnavailableTransportError return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` + Kind MCPPlanInstallResultKind `json:"kind"` alias }{ - Kind: r.Kind(), + Kind: r.mcpPlanInstallResultKind(), alias: alias(r), }) } -func (r PermissionDecisionApproveForLocationApprovalWrite) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocationApprovalWrite +func (r MCPPlanInstallPlanned) MarshalJSON() ([]byte, error) { + type alias MCPPlanInstallPlanned return json.Marshal(struct { - Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` + Kind MCPPlanInstallResultKind `json:"kind"` alias }{ - Kind: r.Kind(), + Kind: r.mcpPlanInstallResultKind(), alias: alias(r), }) } -func (r *PermissionDecisionApproveForLocation) UnmarshalJSON(data []byte) error { - type rawPermissionDecisionApproveForLocation struct { - Approval json.RawMessage `json:"approval"` - LocationKey string `json:"locationKey"` +func (r *MCPPrepareInstallRequest) UnmarshalJSON(data []byte) error { + type rawMCPPrepareInstallRequest struct { + ChoiceID string `json:"choiceId"` + Contract CatalogClientContract `json:"contract"` + Inputs []MCPInstallationInput `json:"inputs"` + PlanHandle string `json:"planHandle"` + PolicySessionID string `json:"policySessionId"` + Secrets []MCPInstallationSecret `json:"secrets"` + SecretStorage MCPInstallationSecretStorage `json:"secretStorage"` + Source json.RawMessage `json:"source"` } - var raw rawPermissionDecisionApproveForLocation + var raw rawMCPPrepareInstallRequest if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Approval != nil { - value, err := unmarshalPermissionDecisionApproveForLocationApproval(raw.Approval) - if err != nil { - return err + r.ChoiceID = raw.ChoiceID + r.Contract = raw.Contract + r.Inputs = raw.Inputs + r.PlanHandle = raw.PlanHandle + r.PolicySessionID = raw.PolicySessionID + r.Secrets = raw.Secrets + r.SecretStorage = raw.SecretStorage + if raw.Source != nil { + value, err := unmarshalMCPServerCardReference(raw.Source) + if err != nil { + return err } - r.Approval = value + r.Source = value } - r.LocationKey = raw.LocationKey return nil } -func (r PermissionDecisionApproveForLocation) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForLocation - return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) +func matchesMCPServerConfigHTTP(data []byte) bool { + var rawGroup0 struct { + Command json.RawMessage `json:"command"` + ServerInstance json.RawMessage `json:"serverInstance"` + Type json.RawMessage `json:"type"` + URL json.RawMessage `json:"url"` + } + if err := json.Unmarshal(data, &rawGroup0); err != nil { + return false + } + if rawGroup0.URL == nil { + return false + } + if rawGroup0.Command != nil { + return false + } + if rawGroup0.ServerInstance != nil { + return false + } + return rawGroup0.Type == nil } -func unmarshalPermissionDecisionApproveForSessionApproval(data []byte) (PermissionDecisionApproveForSessionApproval, error) { - if string(data) == "null" { - return nil, nil +func matchesMCPServerConfigMemory(data []byte) bool { + var rawGroup0 struct { + Command json.RawMessage `json:"command"` + ServerInstance json.RawMessage `json:"serverInstance"` + Type json.RawMessage `json:"type"` + URL json.RawMessage `json:"url"` } - type rawUnion struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + if err := json.Unmarshal(data, &rawGroup0); err != nil { + return false } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err + if rawGroup0.ServerInstance == nil { + return false + } + if rawGroup0.Type == nil { + return false + } + var rawGroup0Term1String string + if err := json.Unmarshal(rawGroup0.Type, &rawGroup0Term1String); err != nil { + return false + } + switch rawGroup0Term1String { + case "memory": + default: + return false } + if rawGroup0.Command != nil { + return false + } + return rawGroup0.URL == nil +} - switch raw.Kind { - case PermissionDecisionApproveForSessionApprovalKindCommands: - var d PermissionDecisionApproveForSessionApprovalCommands - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindCustomTool: - var d PermissionDecisionApproveForSessionApprovalCustomTool - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindExtensionEnvAccess: - var d PermissionDecisionApproveForSessionApprovalExtensionEnvAccess - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindExtensionManagement: - var d PermissionDecisionApproveForSessionApprovalExtensionManagement - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindExtensionPermissionAccess: - var d PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindFactory: - var d PermissionDecisionApproveForSessionApprovalFactory - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindMCP: - var d PermissionDecisionApproveForSessionApprovalMCP - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindMCPSampling: - var d PermissionDecisionApproveForSessionApprovalMCPSampling - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case PermissionDecisionApproveForSessionApprovalKindMemory: - var d PermissionDecisionApproveForSessionApprovalMemory +func matchesMCPServerConfigStdio(data []byte) bool { + var rawGroup0 struct { + Command json.RawMessage `json:"command"` + ServerInstance json.RawMessage `json:"serverInstance"` + Type json.RawMessage `json:"type"` + URL json.RawMessage `json:"url"` + } + if err := json.Unmarshal(data, &rawGroup0); err != nil { + return false + } + if rawGroup0.Command == nil { + return false + } + if rawGroup0.ServerInstance != nil { + return false + } + if rawGroup0.Type != nil { + return false + } + return rawGroup0.URL == nil +} + +func unmarshalMCPServerConfig(data []byte) (MCPServerConfig, error) { + if string(data) == "null" { + return nil, nil + } + if matchesMCPServerConfigHTTP(data) { + var d MCPServerConfigHTTP if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForSessionApprovalKindRead: - var d PermissionDecisionApproveForSessionApprovalRead + } + if matchesMCPServerConfigMemory(data) { + var d MCPServerConfigMemory if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionDecisionApproveForSessionApprovalKindWrite: - var d PermissionDecisionApproveForSessionApprovalWrite + } + if matchesMCPServerConfigStdio(data) { + var d MCPServerConfigStdio if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - default: - return &RawPermissionDecisionApproveForSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil } + return &RawMCPServerConfigData{Raw: data}, nil } -func (r RawPermissionDecisionApproveForSessionApprovalData) MarshalJSON() ([]byte, error) { +func (r RawMCPServerConfigData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - }{ - Kind: r.Discriminator, - }) + return []byte("null"), nil } -func (r PermissionDecisionApproveForSessionApprovalCommands) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalCommands - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) +func (r *MCPServerConfigMemory) UnmarshalJSON(data []byte) error { + type rawMCPServerConfigMemory struct { + ConfigWarnings []string `json:"configWarnings,omitzero"` + DeferTools *MCPServerConfigDeferTools `json:"deferTools,omitempty"` + DisableSecretMasking *bool `json:"disableSecretMasking,omitempty"` + DisableToolCache *bool `json:"disableToolCache,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + Events []string `json:"events,omitzero"` + ExcludeTools []string `json:"excludeTools,omitzero"` + FilterMapping json.RawMessage `json:"filterMapping,omitempty"` + IsDefaultServer *bool `json:"isDefaultServer,omitempty"` + Notifications []string `json:"notifications,omitzero"` + Oidc json.RawMessage `json:"oidc,omitempty"` + SafeForTelemetry json.RawMessage `json:"safeForTelemetry,omitempty"` + ServerInstance any `json:"serverInstance"` + SlowConnectionThresholdMs *int64 `json:"slowConnectionThresholdMs,omitempty"` + Source *MCPServerSource `json:"source,omitempty"` + SourcePath *string `json:"sourcePath,omitempty"` + SourcePlugin *string `json:"sourcePlugin,omitempty"` + SourcePluginSpec *bool `json:"sourcePluginSpec,omitempty"` + SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` + Timeout *int64 `json:"timeout,omitempty"` + Tools []string `json:"tools,omitzero"` + Type MCPServerConfigMemoryType `json:"type"` + } + var raw rawMCPServerConfigMemory + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.ConfigWarnings = raw.ConfigWarnings + r.DeferTools = raw.DeferTools + r.DisableSecretMasking = raw.DisableSecretMasking + r.DisableToolCache = raw.DisableToolCache + r.DisplayName = raw.DisplayName + r.Events = raw.Events + r.ExcludeTools = raw.ExcludeTools + if raw.FilterMapping != nil { + value, err := unmarshalFilterMapping(raw.FilterMapping) + if err != nil { + return err + } + r.FilterMapping = value + } + r.IsDefaultServer = raw.IsDefaultServer + r.Notifications = raw.Notifications + if raw.Oidc != nil { + value, err := unmarshalMCPServerAuthConfig(raw.Oidc) + if err != nil { + return err + } + r.Oidc = value + } + if raw.SafeForTelemetry != nil { + value, err := unmarshalMCPSafeForTelemetry(raw.SafeForTelemetry) + if err != nil { + return err + } + r.SafeForTelemetry = value + } + r.ServerInstance = raw.ServerInstance + r.SlowConnectionThresholdMs = raw.SlowConnectionThresholdMs + r.Source = raw.Source + r.SourcePath = raw.SourcePath + r.SourcePlugin = raw.SourcePlugin + r.SourcePluginSpec = raw.SourcePluginSpec + r.SourcePluginVersion = raw.SourcePluginVersion + r.Timeout = raw.Timeout + r.Tools = raw.Tools + r.Type = raw.Type + return nil } -func (r PermissionDecisionApproveForSessionApprovalCustomTool) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalCustomTool - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - -func (r PermissionDecisionApproveForSessionApprovalExtensionEnvAccess) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalExtensionEnvAccess - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) +func (r *MCPReloadConfig) UnmarshalJSON(data []byte) error { + type rawMCPReloadConfig struct { + ActiveGitHubToken *string `json:"activeGitHubToken,omitempty"` + CLIEnabledServers []string `json:"cliEnabledServers,omitzero"` + ConfigFilter any `json:"configFilter,omitempty"` + DisabledServers []string `json:"disabledServers,omitzero"` + EnabledServers []string `json:"enabledServers,omitzero"` + ForceRestart *bool `json:"forceRestart,omitempty"` + GitHubMCPToolOptions any `json:"githubMcpToolOptions,omitempty"` + GitHubMCPUserOverride *bool `json:"githubMcpUserOverride,omitempty"` + IncludeWorkspaceSources *bool `json:"includeWorkspaceSources,omitempty"` + Mcp3pEnabled *bool `json:"mcp3pEnabled,omitempty"` + MCPServers map[string]json.RawMessage `json:"mcpServers"` + SecretStore any `json:"secretStore,omitempty"` + UseCachedToolSnapshots *bool `json:"useCachedToolSnapshots,omitempty"` + } + var raw rawMCPReloadConfig + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.ActiveGitHubToken = raw.ActiveGitHubToken + r.CLIEnabledServers = raw.CLIEnabledServers + r.ConfigFilter = raw.ConfigFilter + r.DisabledServers = raw.DisabledServers + r.EnabledServers = raw.EnabledServers + r.ForceRestart = raw.ForceRestart + r.GitHubMCPToolOptions = raw.GitHubMCPToolOptions + r.GitHubMCPUserOverride = raw.GitHubMCPUserOverride + r.IncludeWorkspaceSources = raw.IncludeWorkspaceSources + r.Mcp3pEnabled = raw.Mcp3pEnabled + if raw.MCPServers != nil { + r.MCPServers = make(map[string]MCPServerConfig, len(raw.MCPServers)) + for key, rawValue := range raw.MCPServers { + value, err := unmarshalMCPServerConfig(rawValue) + if err != nil { + return err + } + r.MCPServers[key] = value + } + } + r.SecretStore = raw.SecretStore + r.UseCachedToolSnapshots = raw.UseCachedToolSnapshots + return nil } -func (r PermissionDecisionApproveForSessionApprovalExtensionManagement) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalExtensionManagement - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) +func (r *MCPRestartServerRequest) UnmarshalJSON(data []byte) error { + type rawMCPRestartServerRequest struct { + Config json.RawMessage `json:"config,omitempty"` + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` + ServerName string `json:"serverName"` + } + var raw rawMCPRestartServerRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Config != nil { + value, err := unmarshalMCPSerializableServerConfig(raw.Config) + if err != nil { + return err + } + r.Config = value + } + r.ExpectedInstallationID = raw.ExpectedInstallationID + r.ServerName = raw.ServerName + return nil } -func (r PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) +func (r *MCPStartServerRequest) UnmarshalJSON(data []byte) error { + type rawMCPStartServerRequest struct { + Config json.RawMessage `json:"config,omitempty"` + ExpectedInstallationID *string `json:"expectedInstallationId,omitempty"` + ServerName string `json:"serverName"` + } + var raw rawMCPStartServerRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Config != nil { + value, err := unmarshalMCPSerializableServerConfig(raw.Config) + if err != nil { + return err + } + r.Config = value + } + r.ExpectedInstallationID = raw.ExpectedInstallationID + r.ServerName = raw.ServerName + return nil } -func (r PermissionDecisionApproveForSessionApprovalFactory) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalFactory - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} +func unmarshalProtocolMarkerSectionOverride(data []byte) (ProtocolMarkerSectionOverride, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } -func (r PermissionDecisionApproveForSessionApprovalMCP) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalMCP - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) + switch raw.Action { + case ProtocolMarkerSectionOverrideActionPreserve: + var d ProtocolMarkerSectionOverridePreserve + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case ProtocolMarkerSectionOverrideActionTransform: + var d ProtocolMarkerSectionOverrideTransform + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawProtocolMarkerSectionOverrideData{Discriminator: raw.Action, Raw: data}, nil + } } -func (r PermissionDecisionApproveForSessionApprovalMCPSampling) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalMCPSampling +func (r RawProtocolMarkerSectionOverrideData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias + Action ProtocolMarkerSectionOverrideAction `json:"action"` }{ - Kind: r.Kind(), - alias: alias(r), + Action: r.Discriminator, }) } -func (r PermissionDecisionApproveForSessionApprovalMemory) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalMemory +func (r ProtocolMarkerSectionOverridePreserve) MarshalJSON() ([]byte, error) { + type alias ProtocolMarkerSectionOverridePreserve return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + Action ProtocolMarkerSectionOverrideAction `json:"action"` alias }{ - Kind: r.Kind(), - alias: alias(r), + Action: r.Action(), + alias: alias(r), }) } -func (r PermissionDecisionApproveForSessionApprovalRead) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalRead +func (r ProtocolMarkerSectionOverrideTransform) MarshalJSON() ([]byte, error) { + type alias ProtocolMarkerSectionOverrideTransform return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + Action ProtocolMarkerSectionOverrideAction `json:"action"` alias }{ - Kind: r.Kind(), - alias: alias(r), + Action: r.Action(), + alias: alias(r), }) } -func (r PermissionDecisionApproveForSessionApprovalWrite) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSessionApprovalWrite - return json.Marshal(struct { - Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) +func (r ProtocolSectionOverride) MarshalJSON() ([]byte, error) { + if r.ProtocolMarkerSectionOverride != nil { + return json.Marshal(r.ProtocolMarkerSectionOverride) + } + if r.ProtocolStaticSectionOverride != nil { + return json.Marshal(r.ProtocolStaticSectionOverride) + } + return []byte("null"), nil } -func (r *PermissionDecisionApproveForSession) UnmarshalJSON(data []byte) error { - type rawPermissionDecisionApproveForSession struct { - Approval json.RawMessage `json:"approval,omitempty"` - Domain *string `json:"domain,omitempty"` +func (r *ProtocolSectionOverride) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + *r = ProtocolSectionOverride{} + return nil } - var raw rawPermissionDecisionApproveForSession - if err := json.Unmarshal(data, &raw); err != nil { - return err + { + value, err := unmarshalProtocolMarkerSectionOverride(data) + if err == nil { + *r = ProtocolSectionOverride{ProtocolMarkerSectionOverride: value} + return nil + } } - if raw.Approval != nil { - value, err := unmarshalPermissionDecisionApproveForSessionApproval(raw.Approval) - if err != nil { - return err + { + var value ProtocolStaticSectionOverride + if err := json.Unmarshal(data, &value); err == nil { + *r = ProtocolSectionOverride{ProtocolStaticSectionOverride: &value} + return nil } - r.Approval = value } - r.Domain = raw.Domain - return nil + return errors.New("data did not match any union variant for ProtocolSectionOverride") } -func (r PermissionDecisionApproveForSession) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveForSession - return json.Marshal(struct { +func unmarshalPermissionDecision(data []byte) (PermissionDecision, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { Kind PermissionDecisionKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } -func (r PermissionDecisionApproveOnce) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApproveOnce - return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), + switch raw.Kind { + case PermissionDecisionKindApproveForLocation: + var d PermissionDecisionApproveForLocation + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindApproveForSession: + var d PermissionDecisionApproveForSession + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindApproveOnce: + var d PermissionDecisionApproveOnce + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindApprovePermanently: + var d PermissionDecisionApprovePermanently + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindApproveReadOnlyForSession: + var d PermissionDecisionApproveReadOnlyForSession + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindApproved: + var d PermissionDecisionApproved + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindApprovedForLocation: + var d PermissionDecisionApprovedForLocation + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindApprovedForSession: + var d PermissionDecisionApprovedForSession + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindCancelled: + var d PermissionDecisionCancelled + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindDeniedByContentExclusionPolicy: + var d PermissionDecisionDeniedByContentExclusionPolicy + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindDeniedByPermissionRequestHook: + var d PermissionDecisionDeniedByPermissionRequestHook + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindDeniedByRules: + var d PermissionDecisionDeniedByRules + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindDeniedInteractivelyByUser: + var d PermissionDecisionDeniedInteractivelyByUser + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindDeniedNoApprovalRuleAndCouldNotRequestFromUser: + var d PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindReject: + var d PermissionDecisionReject + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionDecisionKindUserNotAvailable: + var d PermissionDecisionUserNotAvailable + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawPermissionDecisionData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawPermissionDecisionData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + }{ + Kind: r.Discriminator, }) } -func (r PermissionDecisionApprovePermanently) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionApprovePermanently +func (r PermissionDecisionApproved) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproved return json.Marshal(struct { Kind PermissionDecisionKind `json:"kind"` alias @@ -5085,10 +5742,99 @@ func (r PermissionDecisionApprovePermanently) MarshalJSON() ([]byte, error) { }) } -func (r PermissionDecisionCancelled) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionCancelled +func unmarshalUserToolSessionApproval(data []byte) (UserToolSessionApproval, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind UserToolSessionApprovalKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case UserToolSessionApprovalKindCommands: + var d UserToolSessionApprovalCommands + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindCustomTool: + var d UserToolSessionApprovalCustomTool + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindExtensionEnvAccess: + var d UserToolSessionApprovalExtensionEnvAccess + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindExtensionManagement: + var d UserToolSessionApprovalExtensionManagement + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindExtensionPermissionAccess: + var d UserToolSessionApprovalExtensionPermissionAccess + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindMCP: + var d UserToolSessionApprovalMCP + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindMemory: + var d UserToolSessionApprovalMemory + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindRead: + var d UserToolSessionApprovalRead + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindWorkflow: + var d UserToolSessionApprovalWorkflow + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case UserToolSessionApprovalKindWrite: + var d UserToolSessionApprovalWrite + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawUserToolSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawUserToolSessionApprovalData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r UserToolSessionApprovalCommands) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalCommands + return json.Marshal(struct { + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5096,10 +5842,10 @@ func (r PermissionDecisionCancelled) MarshalJSON() ([]byte, error) { }) } -func (r PermissionDecisionDeniedByContentExclusionPolicy) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionDeniedByContentExclusionPolicy +func (r UserToolSessionApprovalCustomTool) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalCustomTool return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5107,10 +5853,10 @@ func (r PermissionDecisionDeniedByContentExclusionPolicy) MarshalJSON() ([]byte, }) } -func (r PermissionDecisionDeniedByPermissionRequestHook) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionDeniedByPermissionRequestHook +func (r UserToolSessionApprovalExtensionEnvAccess) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalExtensionEnvAccess return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5118,10 +5864,10 @@ func (r PermissionDecisionDeniedByPermissionRequestHook) MarshalJSON() ([]byte, }) } -func (r PermissionDecisionDeniedByRules) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionDeniedByRules +func (r UserToolSessionApprovalExtensionManagement) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalExtensionManagement return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5129,10 +5875,10 @@ func (r PermissionDecisionDeniedByRules) MarshalJSON() ([]byte, error) { }) } -func (r PermissionDecisionDeniedInteractivelyByUser) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionDeniedInteractivelyByUser +func (r UserToolSessionApprovalExtensionPermissionAccess) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalExtensionPermissionAccess return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5140,10 +5886,10 @@ func (r PermissionDecisionDeniedInteractivelyByUser) MarshalJSON() ([]byte, erro }) } -func (r PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser +func (r UserToolSessionApprovalMCP) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalMCP return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5151,10 +5897,10 @@ func (r PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser) Marsha }) } -func (r PermissionDecisionReject) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionReject +func (r UserToolSessionApprovalMemory) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalMemory return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5162,10 +5908,10 @@ func (r PermissionDecisionReject) MarshalJSON() ([]byte, error) { }) } -func (r PermissionDecisionUserNotAvailable) MarshalJSON() ([]byte, error) { - type alias PermissionDecisionUserNotAvailable +func (r UserToolSessionApprovalRead) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalRead return json.Marshal(struct { - Kind PermissionDecisionKind `json:"kind"` + Kind UserToolSessionApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5173,34 +5919,94 @@ func (r PermissionDecisionUserNotAvailable) MarshalJSON() ([]byte, error) { }) } -func (r *PermissionDecisionRequest) UnmarshalJSON(data []byte) error { - type rawPermissionDecisionRequest struct { - DecisionContext *PermissionDecisionContext `json:"decisionContext,omitempty"` - RequestID string `json:"requestId"` - Result json.RawMessage `json:"result"` - } - var raw rawPermissionDecisionRequest - if err := json.Unmarshal(data, &raw); err != nil { +func (r UserToolSessionApprovalWorkflow) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalWorkflow + return json.Marshal(struct { + Kind UserToolSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r UserToolSessionApprovalWrite) MarshalJSON() ([]byte, error) { + type alias UserToolSessionApprovalWrite + return json.Marshal(struct { + Kind UserToolSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *PermissionDecisionApprovedForLocation) UnmarshalJSON(data []byte) error { + type rawPermissionDecisionApprovedForLocation struct { + Approval json.RawMessage `json:"approval"` + LocationKey string `json:"locationKey"` + } + var raw rawPermissionDecisionApprovedForLocation + if err := json.Unmarshal(data, &raw); err != nil { return err } - r.DecisionContext = raw.DecisionContext - r.RequestID = raw.RequestID - if raw.Result != nil { - value, err := unmarshalPermissionDecision(raw.Result) + if raw.Approval != nil { + value, err := unmarshalUserToolSessionApproval(raw.Approval) if err != nil { return err } - r.Result = value + r.Approval = value } + r.LocationKey = raw.LocationKey return nil } -func unmarshalPermissionsLocationsAddToolApprovalDetails(data []byte) (PermissionsLocationsAddToolApprovalDetails, error) { +func (r PermissionDecisionApprovedForLocation) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApprovedForLocation + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *PermissionDecisionApprovedForSession) UnmarshalJSON(data []byte) error { + type rawPermissionDecisionApprovedForSession struct { + Approval json.RawMessage `json:"approval"` + } + var raw rawPermissionDecisionApprovedForSession + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Approval != nil { + value, err := unmarshalUserToolSessionApproval(raw.Approval) + if err != nil { + return err + } + r.Approval = value + } + return nil +} + +func (r PermissionDecisionApprovedForSession) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApprovedForSession + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func unmarshalPermissionDecisionApproveForLocationApproval(data []byte) (PermissionDecisionApproveForLocationApproval, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -5208,92 +6014,92 @@ func unmarshalPermissionsLocationsAddToolApprovalDetails(data []byte) (Permissio } switch raw.Kind { - case PermissionsLocationsAddToolApprovalDetailsKindCommands: - var d PermissionsLocationsAddToolApprovalDetailsCommands + case PermissionDecisionApproveForLocationApprovalKindCommands: + var d PermissionDecisionApproveForLocationApprovalCommands if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindCustomTool: - var d PermissionsLocationsAddToolApprovalDetailsCustomTool + case PermissionDecisionApproveForLocationApprovalKindCustomTool: + var d PermissionDecisionApproveForLocationApprovalCustomTool if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindExtensionEnvAccess: - var d PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess + case PermissionDecisionApproveForLocationApprovalKindExtensionEnvAccess: + var d PermissionDecisionApproveForLocationApprovalExtensionEnvAccess if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindExtensionManagement: - var d PermissionsLocationsAddToolApprovalDetailsExtensionManagement + case PermissionDecisionApproveForLocationApprovalKindExtensionManagement: + var d PermissionDecisionApproveForLocationApprovalExtensionManagement if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindExtensionPermissionAccess: - var d PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess + case PermissionDecisionApproveForLocationApprovalKindExtensionPermissionAccess: + var d PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindFactory: - var d PermissionsLocationsAddToolApprovalDetailsFactory + case PermissionDecisionApproveForLocationApprovalKindMCP: + var d PermissionDecisionApproveForLocationApprovalMCP if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindMCP: - var d PermissionsLocationsAddToolApprovalDetailsMCP + case PermissionDecisionApproveForLocationApprovalKindMCPSampling: + var d PermissionDecisionApproveForLocationApprovalMCPSampling if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindMCPSampling: - var d PermissionsLocationsAddToolApprovalDetailsMCPSampling + case PermissionDecisionApproveForLocationApprovalKindMemory: + var d PermissionDecisionApproveForLocationApprovalMemory if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindMemory: - var d PermissionsLocationsAddToolApprovalDetailsMemory + case PermissionDecisionApproveForLocationApprovalKindRead: + var d PermissionDecisionApproveForLocationApprovalRead if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindRead: - var d PermissionsLocationsAddToolApprovalDetailsRead + case PermissionDecisionApproveForLocationApprovalKindWorkflow: + var d PermissionDecisionApproveForLocationApprovalWorkflow if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PermissionsLocationsAddToolApprovalDetailsKindWrite: - var d PermissionsLocationsAddToolApprovalDetailsWrite + case PermissionDecisionApproveForLocationApprovalKindWrite: + var d PermissionDecisionApproveForLocationApprovalWrite if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawPermissionsLocationsAddToolApprovalDetailsData{Discriminator: raw.Kind, Raw: data}, nil + return &RawPermissionDecisionApproveForLocationApprovalData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawPermissionsLocationsAddToolApprovalDetailsData) MarshalJSON() ([]byte, error) { +func (r RawPermissionDecisionApproveForLocationApprovalData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` }{ Kind: r.Discriminator, }) } -func (r PermissionsLocationsAddToolApprovalDetailsCommands) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsCommands +func (r PermissionDecisionApproveForLocationApprovalCommands) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalCommands return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5301,10 +6107,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsCommands) MarshalJSON() ([]byt }) } -func (r PermissionsLocationsAddToolApprovalDetailsCustomTool) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsCustomTool +func (r PermissionDecisionApproveForLocationApprovalCustomTool) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalCustomTool return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5312,10 +6118,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsCustomTool) MarshalJSON() ([]b }) } -func (r PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess +func (r PermissionDecisionApproveForLocationApprovalExtensionEnvAccess) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalExtensionEnvAccess return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5323,10 +6129,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess) MarshalJSO }) } -func (r PermissionsLocationsAddToolApprovalDetailsExtensionManagement) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsExtensionManagement +func (r PermissionDecisionApproveForLocationApprovalExtensionManagement) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalExtensionManagement return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5334,10 +6140,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsExtensionManagement) MarshalJS }) } -func (r PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess +func (r PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5345,10 +6151,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess) Mar }) } -func (r PermissionsLocationsAddToolApprovalDetailsFactory) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsFactory +func (r PermissionDecisionApproveForLocationApprovalMCP) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalMCP return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5356,10 +6162,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsFactory) MarshalJSON() ([]byte }) } -func (r PermissionsLocationsAddToolApprovalDetailsMCP) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsMCP +func (r PermissionDecisionApproveForLocationApprovalMCPSampling) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalMCPSampling return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5367,10 +6173,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsMCP) MarshalJSON() ([]byte, er }) } -func (r PermissionsLocationsAddToolApprovalDetailsMCPSampling) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsMCPSampling +func (r PermissionDecisionApproveForLocationApprovalMemory) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalMemory return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5378,10 +6184,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsMCPSampling) MarshalJSON() ([] }) } -func (r PermissionsLocationsAddToolApprovalDetailsMemory) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsMemory +func (r PermissionDecisionApproveForLocationApprovalRead) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalRead return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5389,10 +6195,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsMemory) MarshalJSON() ([]byte, }) } -func (r PermissionsLocationsAddToolApprovalDetailsRead) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsRead +func (r PermissionDecisionApproveForLocationApprovalWorkflow) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalWorkflow return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5400,10 +6206,10 @@ func (r PermissionsLocationsAddToolApprovalDetailsRead) MarshalJSON() ([]byte, e }) } -func (r PermissionsLocationsAddToolApprovalDetailsWrite) MarshalJSON() ([]byte, error) { - type alias PermissionsLocationsAddToolApprovalDetailsWrite +func (r PermissionDecisionApproveForLocationApprovalWrite) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocationApprovalWrite return json.Marshal(struct { - Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + Kind PermissionDecisionApproveForLocationApprovalKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -5411,17 +6217,17 @@ func (r PermissionsLocationsAddToolApprovalDetailsWrite) MarshalJSON() ([]byte, }) } -func (r *PermissionLocationAddToolApprovalParams) UnmarshalJSON(data []byte) error { - type rawPermissionLocationAddToolApprovalParams struct { +func (r *PermissionDecisionApproveForLocation) UnmarshalJSON(data []byte) error { + type rawPermissionDecisionApproveForLocation struct { Approval json.RawMessage `json:"approval"` LocationKey string `json:"locationKey"` } - var raw rawPermissionLocationAddToolApprovalParams + var raw rawPermissionDecisionApproveForLocation if err := json.Unmarshal(data, &raw); err != nil { return err } if raw.Approval != nil { - value, err := unmarshalPermissionsLocationsAddToolApprovalDetails(raw.Approval) + value, err := unmarshalPermissionDecisionApproveForLocationApproval(raw.Approval) if err != nil { return err } @@ -5431,811 +6237,2025 @@ func (r *PermissionLocationAddToolApprovalParams) UnmarshalJSON(data []byte) err return nil } -func unmarshalProtocolMarkerSectionOverride(data []byte) (ProtocolMarkerSectionOverride, error) { +func (r PermissionDecisionApproveForLocation) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForLocation + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func unmarshalPermissionDecisionApproveForSessionApproval(data []byte) (PermissionDecisionApproveForSessionApproval, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - switch raw.Action { - case ProtocolMarkerSectionOverrideActionPreserve: - var d ProtocolMarkerSectionOverridePreserve + switch raw.Kind { + case PermissionDecisionApproveForSessionApprovalKindCommands: + var d PermissionDecisionApproveForSessionApprovalCommands if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case ProtocolMarkerSectionOverrideActionTransform: - var d ProtocolMarkerSectionOverrideTransform + case PermissionDecisionApproveForSessionApprovalKindCustomTool: + var d PermissionDecisionApproveForSessionApprovalCustomTool if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - default: - return &RawProtocolMarkerSectionOverrideData{Discriminator: raw.Action, Raw: data}, nil - } -} - -func (r RawProtocolMarkerSectionOverrideData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - }{ - Action: r.Discriminator, - }) -} - -func (r ProtocolMarkerSectionOverridePreserve) MarshalJSON() ([]byte, error) { - type alias ProtocolMarkerSectionOverridePreserve - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - alias - }{ - Action: r.Action(), - alias: alias(r), - }) -} - -func (r ProtocolMarkerSectionOverrideTransform) MarshalJSON() ([]byte, error) { - type alias ProtocolMarkerSectionOverrideTransform - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - alias - }{ - Action: r.Action(), - alias: alias(r), - }) -} - -func (r ProtocolSectionOverride) MarshalJSON() ([]byte, error) { - if r.ProtocolMarkerSectionOverride != nil { - return json.Marshal(r.ProtocolMarkerSectionOverride) - } - if r.ProtocolStaticSectionOverride != nil { - return json.Marshal(r.ProtocolStaticSectionOverride) - } - return []byte("null"), nil -} - -func (r *ProtocolSectionOverride) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - *r = ProtocolSectionOverride{} - return nil - } - { - value, err := unmarshalProtocolMarkerSectionOverride(data) - if err == nil { - *r = ProtocolSectionOverride{ProtocolMarkerSectionOverride: value} - return nil - } - } - { - var value ProtocolStaticSectionOverride - if err := json.Unmarshal(data, &value); err == nil { - *r = ProtocolSectionOverride{ProtocolStaticSectionOverride: &value} - return nil - } - } - return errors.New("data did not match any union variant for ProtocolSectionOverride") -} - -func unmarshalPushAttachment(data []byte) (PushAttachment, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Type PushAttachmentType `json:"type"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Type { - case PushAttachmentTypeBlob: - var d PushAttachmentBlob + case PermissionDecisionApproveForSessionApprovalKindExtensionEnvAccess: + var d PermissionDecisionApproveForSessionApprovalExtensionEnvAccess if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeDirectory: - var d PushAttachmentDirectory + case PermissionDecisionApproveForSessionApprovalKindExtensionManagement: + var d PermissionDecisionApproveForSessionApprovalExtensionManagement if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeExtensionContext: - var d ExtensionContextPushInput + case PermissionDecisionApproveForSessionApprovalKindExtensionPermissionAccess: + var d PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeFile: - var d PushAttachmentFile + case PermissionDecisionApproveForSessionApprovalKindMCP: + var d PermissionDecisionApproveForSessionApprovalMCP if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubActionsJob: - var d PushAttachmentGitHubActionsJob + case PermissionDecisionApproveForSessionApprovalKindMCPSampling: + var d PermissionDecisionApproveForSessionApprovalMCPSampling if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubCommit: - var d PushAttachmentGitHubCommit + case PermissionDecisionApproveForSessionApprovalKindMemory: + var d PermissionDecisionApproveForSessionApprovalMemory if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubFile: - var d PushAttachmentGitHubFile + case PermissionDecisionApproveForSessionApprovalKindRead: + var d PermissionDecisionApproveForSessionApprovalRead if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubFileDiff: - var d PushAttachmentGitHubFileDiff + case PermissionDecisionApproveForSessionApprovalKindWorkflow: + var d PermissionDecisionApproveForSessionApprovalWorkflow if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubReference: - var d PushAttachmentGitHubReference + case PermissionDecisionApproveForSessionApprovalKindWrite: + var d PermissionDecisionApproveForSessionApprovalWrite if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubRelease: - var d PushAttachmentGitHubRelease + default: + return &RawPermissionDecisionApproveForSessionApprovalData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawPermissionDecisionApproveForSessionApprovalData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r PermissionDecisionApproveForSessionApprovalCommands) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalCommands + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalCustomTool) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalCustomTool + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalExtensionEnvAccess) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalExtensionEnvAccess + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalExtensionManagement) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalExtensionManagement + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalMCP) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalMCP + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalMCPSampling) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalMCPSampling + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalMemory) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalMemory + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalRead) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalRead + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalWorkflow) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalWorkflow + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveForSessionApprovalWrite) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSessionApprovalWrite + return json.Marshal(struct { + Kind PermissionDecisionApproveForSessionApprovalKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *PermissionDecisionApproveForSession) UnmarshalJSON(data []byte) error { + type rawPermissionDecisionApproveForSession struct { + Approval json.RawMessage `json:"approval,omitempty"` + Domain *string `json:"domain,omitempty"` + } + var raw rawPermissionDecisionApproveForSession + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Approval != nil { + value, err := unmarshalPermissionDecisionApproveForSessionApproval(raw.Approval) + if err != nil { + return err + } + r.Approval = value + } + r.Domain = raw.Domain + return nil +} + +func (r PermissionDecisionApproveForSession) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveForSession + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveOnce) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveOnce + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApprovePermanently) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApprovePermanently + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionApproveReadOnlyForSession) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionApproveReadOnlyForSession + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionCancelled) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionCancelled + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionDeniedByContentExclusionPolicy) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionDeniedByContentExclusionPolicy + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionDeniedByPermissionRequestHook) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionDeniedByPermissionRequestHook + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionDeniedByRules) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionDeniedByRules + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionDeniedInteractivelyByUser) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionDeniedInteractivelyByUser + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionReject) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionReject + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionDecisionUserNotAvailable) MarshalJSON() ([]byte, error) { + type alias PermissionDecisionUserNotAvailable + return json.Marshal(struct { + Kind PermissionDecisionKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *PermissionDecisionRequest) UnmarshalJSON(data []byte) error { + type rawPermissionDecisionRequest struct { + DecisionContext *PermissionDecisionContext `json:"decisionContext,omitempty"` + RequestID string `json:"requestId"` + Result json.RawMessage `json:"result"` + } + var raw rawPermissionDecisionRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.DecisionContext = raw.DecisionContext + r.RequestID = raw.RequestID + if raw.Result != nil { + value, err := unmarshalPermissionDecision(raw.Result) + if err != nil { + return err + } + r.Result = value + } + return nil +} + +func unmarshalPermissionsLocationsAddToolApprovalDetails(data []byte) (PermissionsLocationsAddToolApprovalDetails, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case PermissionsLocationsAddToolApprovalDetailsKindCommands: + var d PermissionsLocationsAddToolApprovalDetailsCommands + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindCustomTool: + var d PermissionsLocationsAddToolApprovalDetailsCustomTool + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindExtensionEnvAccess: + var d PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindExtensionManagement: + var d PermissionsLocationsAddToolApprovalDetailsExtensionManagement + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindExtensionPermissionAccess: + var d PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindMCP: + var d PermissionsLocationsAddToolApprovalDetailsMCP + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindMCPSampling: + var d PermissionsLocationsAddToolApprovalDetailsMCPSampling + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindMemory: + var d PermissionsLocationsAddToolApprovalDetailsMemory + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindRead: + var d PermissionsLocationsAddToolApprovalDetailsRead + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindWorkflow: + var d PermissionsLocationsAddToolApprovalDetailsWorkflow + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PermissionsLocationsAddToolApprovalDetailsKindWrite: + var d PermissionsLocationsAddToolApprovalDetailsWrite + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawPermissionsLocationsAddToolApprovalDetailsData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawPermissionsLocationsAddToolApprovalDetailsData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsCommands) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsCommands + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsCustomTool) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsCustomTool + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsExtensionManagement) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsExtensionManagement + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsMCP) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsMCP + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsMCPSampling) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsMCPSampling + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsMemory) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsMemory + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsRead) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsRead + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsWorkflow) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsWorkflow + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r PermissionsLocationsAddToolApprovalDetailsWrite) MarshalJSON() ([]byte, error) { + type alias PermissionsLocationsAddToolApprovalDetailsWrite + return json.Marshal(struct { + Kind PermissionsLocationsAddToolApprovalDetailsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *PermissionLocationAddToolApprovalParams) UnmarshalJSON(data []byte) error { + type rawPermissionLocationAddToolApprovalParams struct { + Approval json.RawMessage `json:"approval"` + LocationKey string `json:"locationKey"` + } + var raw rawPermissionLocationAddToolApprovalParams + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Approval != nil { + value, err := unmarshalPermissionsLocationsAddToolApprovalDetails(raw.Approval) + if err != nil { + return err + } + r.Approval = value + } + r.LocationKey = raw.LocationKey + return nil +} + +func unmarshalPushAttachment(data []byte) (PushAttachment, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Type PushAttachmentType `json:"type"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Type { + case PushAttachmentTypeBlob: + var d PushAttachmentBlob + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeDirectory: + var d PushAttachmentDirectory + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeExtensionContext: + var d ExtensionContextPushInput + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeFile: + var d PushAttachmentFile + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubActionsJob: + var d PushAttachmentGitHubActionsJob + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubCommit: + var d PushAttachmentGitHubCommit + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubFile: + var d PushAttachmentGitHubFile + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubFileDiff: + var d PushAttachmentGitHubFileDiff + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubReference: + var d PushAttachmentGitHubReference + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubRelease: + var d PushAttachmentGitHubRelease + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubRepository: + var d PushAttachmentGitHubRepository + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubSnippet: + var d PushAttachmentGitHubSnippet + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubTreeComparison: + var d PushAttachmentGitHubTreeComparison + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeGitHubURL: + var d PushAttachmentGitHubURL + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case PushAttachmentTypeSelection: + var d PushAttachmentSelection + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawPushAttachmentData{Discriminator: raw.Type, Raw: data}, nil + } +} + +func (r RawPushAttachmentData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + }{ + Type: r.Discriminator, + }) +} + +func (r ExtensionContextPushInput) MarshalJSON() ([]byte, error) { + type alias ExtensionContextPushInput + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentBlob) MarshalJSON() ([]byte, error) { + type alias PushAttachmentBlob + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentDirectory) MarshalJSON() ([]byte, error) { + type alias PushAttachmentDirectory + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentFile) MarshalJSON() ([]byte, error) { + type alias PushAttachmentFile + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubActionsJob) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubActionsJob + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubCommit) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubCommit + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubFile) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubFile + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubFileDiff) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubFileDiff + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubReference) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubReference + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubRelease) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubRelease + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubRepository) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubRepository + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubSnippet) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubSnippet + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubTreeComparison) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubTreeComparison + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentGitHubURL) MarshalJSON() ([]byte, error) { + type alias PushAttachmentGitHubURL + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r PushAttachmentSelection) MarshalJSON() ([]byte, error) { + type alias PushAttachmentSelection + return json.Marshal(struct { + Type PushAttachmentType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r *QueueAppendSteeringRequest) UnmarshalJSON(data []byte) error { + type rawQueueAppendSteeringRequest struct { + AgentMode SendAgentMode `json:"agentMode"` + Attachments []json.RawMessage `json:"attachments"` + DisplayPrompt string `json:"displayPrompt"` + ExpectedPrompt string `json:"expectedPrompt"` + MessageID string `json:"messageId"` + Prompt string `json:"prompt"` + } + var raw rawQueueAppendSteeringRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.AgentMode = raw.AgentMode + if raw.Attachments != nil { + r.Attachments = make([]Attachment, 0, len(raw.Attachments)) + for _, rawItem := range raw.Attachments { + value, err := unmarshalAttachment(rawItem) + if err != nil { + return err + } + r.Attachments = append(r.Attachments, value) + } + } + r.DisplayPrompt = raw.DisplayPrompt + r.ExpectedPrompt = raw.ExpectedPrompt + r.MessageID = raw.MessageID + r.Prompt = raw.Prompt + return nil +} + +func (r *QueueInsertMessage) UnmarshalJSON(data []byte) error { + type rawQueueInsertMessage struct { + AgentMode *SendAgentMode `json:"agentMode,omitempty"` + Attachments []json.RawMessage `json:"attachments,omitzero"` + Billable *bool `json:"billable,omitempty"` + Delivery *string `json:"delivery,omitempty"` + DisplayPrompt *string `json:"displayPrompt,omitempty"` + Mode *SendMode `json:"mode,omitempty"` + Prepend *bool `json:"prepend,omitempty"` + Prompt string `json:"prompt"` + RequestHeaders map[string]string `json:"requestHeaders,omitzero"` + RequiredTool *string `json:"requiredTool,omitempty"` + Source *string `json:"source,omitempty"` + Wait *bool `json:"wait,omitempty"` + } + var raw rawQueueInsertMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.AgentMode = raw.AgentMode + if raw.Attachments != nil { + r.Attachments = make([]Attachment, 0, len(raw.Attachments)) + for _, rawItem := range raw.Attachments { + value, err := unmarshalAttachment(rawItem) + if err != nil { + return err + } + r.Attachments = append(r.Attachments, value) + } + } + r.Billable = raw.Billable + r.Delivery = raw.Delivery + r.DisplayPrompt = raw.DisplayPrompt + r.Mode = raw.Mode + r.Prepend = raw.Prepend + r.Prompt = raw.Prompt + r.RequestHeaders = raw.RequestHeaders + r.RequiredTool = raw.RequiredTool + r.Source = raw.Source + r.Wait = raw.Wait + return nil +} + +func unmarshalRemoteControlStatus(data []byte) (RemoteControlStatus, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + State RemoteControlStatusState `json:"state"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.State { + case RemoteControlStatusStateActive: + var d RemoteControlStatusActive + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case RemoteControlStatusStateConnecting: + var d RemoteControlStatusConnecting + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case RemoteControlStatusStateError: + var d RemoteControlStatusError + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case RemoteControlStatusStateOff: + var d RemoteControlStatusOff + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawRemoteControlStatusData{Discriminator: raw.State, Raw: data}, nil + } +} + +func (r RawRemoteControlStatusData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + State RemoteControlStatusState `json:"state"` + }{ + State: r.Discriminator, + }) +} + +func (r RemoteControlStatusActive) MarshalJSON() ([]byte, error) { + type alias RemoteControlStatusActive + return json.Marshal(struct { + State RemoteControlStatusState `json:"state"` + alias + }{ + State: r.State(), + alias: alias(r), + }) +} + +func (r RemoteControlStatusConnecting) MarshalJSON() ([]byte, error) { + type alias RemoteControlStatusConnecting + return json.Marshal(struct { + State RemoteControlStatusState `json:"state"` + alias + }{ + State: r.State(), + alias: alias(r), + }) +} + +func (r RemoteControlStatusError) MarshalJSON() ([]byte, error) { + type alias RemoteControlStatusError + return json.Marshal(struct { + State RemoteControlStatusState `json:"state"` + alias + }{ + State: r.State(), + alias: alias(r), + }) +} + +func (r RemoteControlStatusOff) MarshalJSON() ([]byte, error) { + type alias RemoteControlStatusOff + return json.Marshal(struct { + State RemoteControlStatusState `json:"state"` + alias + }{ + State: r.State(), + alias: alias(r), + }) +} + +func (r *RemoteControlStatusResult) UnmarshalJSON(data []byte) error { + type rawRemoteControlStatusResult struct { + Status json.RawMessage `json:"status"` + } + var raw rawRemoteControlStatusResult + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Status != nil { + value, err := unmarshalRemoteControlStatus(raw.Status) + if err != nil { + return err + } + r.Status = value + } + return nil +} + +func (r *RemoteControlStopResult) UnmarshalJSON(data []byte) error { + type rawRemoteControlStopResult struct { + Status json.RawMessage `json:"status"` + Stopped bool `json:"stopped"` + } + var raw rawRemoteControlStopResult + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Status != nil { + value, err := unmarshalRemoteControlStatus(raw.Status) + if err != nil { + return err + } + r.Status = value + } + r.Stopped = raw.Stopped + return nil +} + +func (r *RemoteControlTransferResult) UnmarshalJSON(data []byte) error { + type rawRemoteControlTransferResult struct { + Status json.RawMessage `json:"status"` + Transferred bool `json:"transferred"` + } + var raw rawRemoteControlTransferResult + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Status != nil { + value, err := unmarshalRemoteControlStatus(raw.Status) + if err != nil { + return err + } + r.Status = value + } + r.Transferred = raw.Transferred + return nil +} + +func (r *SendAttachmentsToMessageParams) UnmarshalJSON(data []byte) error { + type rawSendAttachmentsToMessageParams struct { + Attachments []json.RawMessage `json:"attachments"` + InstanceID *string `json:"instanceId,omitempty"` + } + var raw rawSendAttachmentsToMessageParams + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Attachments != nil { + r.Attachments = make([]PushAttachment, 0, len(raw.Attachments)) + for _, rawItem := range raw.Attachments { + value, err := unmarshalPushAttachment(rawItem) + if err != nil { + return err + } + r.Attachments = append(r.Attachments, value) + } + } + r.InstanceID = raw.InstanceID + return nil +} + +func (r *SendMessageItem) UnmarshalJSON(data []byte) error { + type rawSendMessageItem struct { + Attachments []json.RawMessage `json:"attachments,omitzero"` + Billable *bool `json:"billable,omitempty"` + DisplayPrompt *string `json:"displayPrompt,omitempty"` + Prompt string `json:"prompt"` + RequiredTool *string `json:"requiredTool,omitempty"` + Source *string `json:"source,omitempty"` + } + var raw rawSendMessageItem + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Attachments != nil { + r.Attachments = make([]Attachment, 0, len(raw.Attachments)) + for _, rawItem := range raw.Attachments { + value, err := unmarshalAttachment(rawItem) + if err != nil { + return err + } + r.Attachments = append(r.Attachments, value) + } + } + r.Billable = raw.Billable + r.DisplayPrompt = raw.DisplayPrompt + r.Prompt = raw.Prompt + r.RequiredTool = raw.RequiredTool + r.Source = raw.Source + return nil +} + +func (r *SendRequest) UnmarshalJSON(data []byte) error { + type rawSendRequest struct { + AgentMode *SendAgentMode `json:"agentMode,omitempty"` + Attachments []json.RawMessage `json:"attachments,omitzero"` + Billable *bool `json:"billable,omitempty"` + DisplayPrompt *string `json:"displayPrompt,omitempty"` + Mode *SendMode `json:"mode,omitempty"` + Prepend *bool `json:"prepend,omitempty"` + Prompt string `json:"prompt"` + RequestHeaders map[string]string `json:"requestHeaders,omitzero"` + RequiredTool *string `json:"requiredTool,omitempty"` + ResponseFormat *ResponseFormat `json:"responseFormat,omitempty"` + Source *string `json:"source,omitempty"` + Traceparent *string `json:"traceparent,omitempty"` + Tracestate *string `json:"tracestate,omitempty"` + Wait *bool `json:"wait,omitempty"` + } + var raw rawSendRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.AgentMode = raw.AgentMode + if raw.Attachments != nil { + r.Attachments = make([]Attachment, 0, len(raw.Attachments)) + for _, rawItem := range raw.Attachments { + value, err := unmarshalAttachment(rawItem) + if err != nil { + return err + } + r.Attachments = append(r.Attachments, value) + } + } + r.Billable = raw.Billable + r.DisplayPrompt = raw.DisplayPrompt + r.Mode = raw.Mode + r.Prepend = raw.Prepend + r.Prompt = raw.Prompt + r.RequestHeaders = raw.RequestHeaders + r.RequiredTool = raw.RequiredTool + r.ResponseFormat = raw.ResponseFormat + r.Source = raw.Source + r.Traceparent = raw.Traceparent + r.Tracestate = raw.Tracestate + r.Wait = raw.Wait + return nil +} + +func (r *SessionAuthLogoutUserRequest) UnmarshalJSON(data []byte) error { + type rawSessionAuthLogoutUserRequest struct { + AuthInfo json.RawMessage `json:"authInfo"` + } + var raw rawSessionAuthLogoutUserRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.AuthInfo != nil { + value, err := unmarshalAuthInfo(raw.AuthInfo) + if err != nil { + return err + } + r.AuthInfo = value + } + return nil +} + +func (r *SessionAuthSwitchRequest) UnmarshalJSON(data []byte) error { + type rawSessionAuthSwitchRequest struct { + AuthInfo json.RawMessage `json:"authInfo"` + Token *string `json:"token,omitempty"` + } + var raw rawSessionAuthSwitchRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.AuthInfo != nil { + value, err := unmarshalAuthInfo(raw.AuthInfo) + if err != nil { + return err + } + r.AuthInfo = value + } + r.Token = raw.Token + return nil +} + +func (r SessionInstalledPluginSource) MarshalJSON() ([]byte, error) { + if r.SessionInstalledPluginSourceGitHub != nil { + return json.Marshal(r.SessionInstalledPluginSourceGitHub) + } + if r.SessionInstalledPluginSourceLocal != nil { + return json.Marshal(r.SessionInstalledPluginSourceLocal) + } + if r.SessionInstalledPluginSourceURL != nil { + return json.Marshal(r.SessionInstalledPluginSourceURL) + } + if r.String != nil { + return json.Marshal(r.String) + } + return []byte("null"), nil +} + +func (r *SessionInstalledPluginSource) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + *r = SessionInstalledPluginSource{} + return nil + } + { + var value SessionInstalledPluginSourceGitHub + if err := json.Unmarshal(data, &value); err == nil { + *r = SessionInstalledPluginSource{SessionInstalledPluginSourceGitHub: &value} + return nil + } + } + { + var value SessionInstalledPluginSourceLocal + if err := json.Unmarshal(data, &value); err == nil { + *r = SessionInstalledPluginSource{SessionInstalledPluginSourceLocal: &value} + return nil + } + } + { + var value SessionInstalledPluginSourceURL + if err := json.Unmarshal(data, &value); err == nil { + *r = SessionInstalledPluginSource{SessionInstalledPluginSourceURL: &value} + return nil + } + } + { + var value string + if err := json.Unmarshal(data, &value); err == nil { + *r = SessionInstalledPluginSource{String: &value} + return nil + } + } + return errors.New("data did not match any union variant for SessionInstalledPluginSource") +} + +func unmarshalSessionLimitPredictionResult(data []byte) (SessionLimitPredictionResult, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind SessionLimitPredictionResultKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case SessionLimitPredictionResultKindAvailable: + var d SessionLimitPredictionResultAvailable + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SessionLimitPredictionResultKindUnavailable: + var d SessionLimitPredictionResultUnavailable + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawSessionLimitPredictionResultData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawSessionLimitPredictionResultData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind SessionLimitPredictionResultKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r SessionLimitPredictionResultAvailable) MarshalJSON() ([]byte, error) { + type alias SessionLimitPredictionResultAvailable + return json.Marshal(struct { + Kind SessionLimitPredictionResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r SessionLimitPredictionResultUnavailable) MarshalJSON() ([]byte, error) { + type alias SessionLimitPredictionResultUnavailable + return json.Marshal(struct { + Kind SessionLimitPredictionResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func unmarshalSessionListEntry(data []byte) (SessionListEntry, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + IsRemote *bool `json:"isRemote"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if raw.IsRemote == nil { + return nil, errors.New("data did not match any union variant for SessionListEntry") + } + + switch *raw.IsRemote { + case false: + var d LocalSessionMetadataValue + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case true: + var d RemoteSessionMetadataValue + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + } + return nil, errors.New("data did not match any union variant for SessionListEntry") +} + +func (r LocalSessionMetadataValue) MarshalJSON() ([]byte, error) { + type alias LocalSessionMetadataValue + return json.Marshal(struct { + IsRemote bool `json:"isRemote"` + alias + }{ + IsRemote: r.sessionListEntryIsRemote(), + alias: alias(r), + }) +} + +func (r RemoteSessionMetadataValue) MarshalJSON() ([]byte, error) { + type alias RemoteSessionMetadataValue + return json.Marshal(struct { + IsRemote bool `json:"isRemote"` + alias + }{ + IsRemote: r.sessionListEntryIsRemote(), + alias: alias(r), + }) +} + +func (r *SessionList) UnmarshalJSON(data []byte) error { + type rawSessionList struct { + Sessions []json.RawMessage `json:"sessions"` + } + var raw rawSessionList + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Sessions != nil { + r.Sessions = make([]SessionListEntry, 0, len(raw.Sessions)) + for _, rawItem := range raw.Sessions { + value, err := unmarshalSessionListEntry(rawItem) + if err != nil { + return err + } + r.Sessions = append(r.Sessions, value) + } + } + return nil +} + +func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { + type rawSessionOpenOptions struct { + AdditionalContentExclusionPolicies []SessionOpenOptionsAdditionalContentExclusionPolicy `json:"additionalContentExclusionPolicies,omitzero"` + AdditionalDirectories []string `json:"additionalDirectories,omitzero"` + AgentContext *string `json:"agentContext,omitempty"` + AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` + AskUserDisabled *bool `json:"askUserDisabled,omitempty"` + AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` + AuthInfo json.RawMessage `json:"authInfo,omitempty"` + AvailableTools []string `json:"availableTools,omitzero"` + Capi *CapiSessionOptions `json:"capi,omitempty"` + ClientKind *string `json:"clientKind,omitempty"` + ClientName *string `json:"clientName,omitempty"` + CoauthorEnabled *bool `json:"coauthorEnabled,omitempty"` + ConfigDir *string `json:"configDir,omitempty"` + ContinueOnAutoMode *bool `json:"continueOnAutoMode,omitempty"` + CopilotURL *string `json:"copilotUrl,omitempty"` + CustomAgentsLocalOnly *bool `json:"customAgentsLocalOnly,omitempty"` + DetachedFromSpawningParentEngagementID *string `json:"detachedFromSpawningParentEngagementId,omitempty"` + DetachedFromSpawningParentSessionID *string `json:"detachedFromSpawningParentSessionId,omitempty"` + DisabledInstructionSources []string `json:"disabledInstructionSources,omitzero"` + DisabledMCPServers []string `json:"disabledMcpServers,omitzero"` + DisabledSkills []string `json:"disabledSkills,omitzero"` + EnableCitations *bool `json:"enableCitations,omitempty"` + EnableFileChangeTracking *bool `json:"enableFileChangeTracking,omitempty"` + EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` + EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` + EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` + EnableSkills *bool `json:"enableSkills,omitempty"` + EnableStreaming *bool `json:"enableStreaming,omitempty"` + EnvValueMode *SessionOpenOptionsEnvValueMode `json:"envValueMode,omitempty"` + EventsLogDirectory *string `json:"eventsLogDirectory,omitempty"` + EventsLogIncludesSubagents *bool `json:"eventsLogIncludesSubagents,omitempty"` + ExcludedBuiltinAgents []string `json:"excludedBuiltinAgents,omitzero"` + ExcludedTools []string `json:"excludedTools,omitzero"` + ExpAssignments any `json:"expAssignments,omitempty"` + FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` + IgnoredSkillsLocations []string `json:"ignoredSkillsLocations,omitzero"` + IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` + IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` + InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` + IntegrationID *string `json:"integrationId,omitempty"` + IsExperimentalMode *bool `json:"isExperimentalMode,omitempty"` + LogInteractiveShells *bool `json:"logInteractiveShells,omitempty"` + LspClientName *string `json:"lspClientName,omitempty"` + ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitzero"` + ManagedSettings *SessionManagedSettings `json:"managedSettings,omitempty"` + MaxInlineBinaryBytes *int64 `json:"maxInlineBinaryBytes,omitempty"` + Memory *MemoryConfiguration `json:"memory,omitempty"` + Model *string `json:"model,omitempty"` + ModelCapabilitiesOverrides *ModelCapabilitiesOverride `json:"modelCapabilitiesOverrides,omitempty"` + Models []ProviderModelConfig `json:"models,omitzero"` + Name *string `json:"name,omitempty"` + Provider *ProviderConfig `json:"provider,omitempty"` + Providers []NamedProviderConfig `json:"providers,omitzero"` + ReasoningEffort *string `json:"reasoningEffort,omitempty"` + ReasoningSummary *SessionOpenOptionsReasoningSummary `json:"reasoningSummary,omitempty"` + RefreshCustomInstructions *bool `json:"refreshCustomInstructions,omitempty"` + RemoteDefaultedOn *bool `json:"remoteDefaultedOn,omitempty"` + RemoteExporting *bool `json:"remoteExporting,omitempty"` + RemoteSteerable *bool `json:"remoteSteerable,omitempty"` + RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` + SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` + SandboxConfigSource *SandboxConfigSource `json:"sandboxConfigSource,omitempty"` + SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` + SessionID *string `json:"sessionId,omitempty"` + SessionLimits *SessionLimitsConfig `json:"sessionLimits,omitempty"` + Shell *ShellOptions `json:"shell,omitempty"` + ShellInitProfile *string `json:"shellInitProfile,omitempty"` + ShellProcessFlags []string `json:"shellProcessFlags,omitzero"` + SkillDirectories []string `json:"skillDirectories,omitzero"` + SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` + TrajectoryFile *string `json:"trajectoryFile,omitempty"` + Verbosity *Verbosity `json:"verbosity,omitempty"` + WorkingDirectory *string `json:"workingDirectory,omitempty"` + WorkingDirectoryContext *SessionContext `json:"workingDirectoryContext,omitempty"` + } + var raw rawSessionOpenOptions + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.AdditionalContentExclusionPolicies = raw.AdditionalContentExclusionPolicies + r.AdditionalDirectories = raw.AdditionalDirectories + r.AgentContext = raw.AgentContext + r.AllowAllMCPServerInstructions = raw.AllowAllMCPServerInstructions + r.AskUserDisabled = raw.AskUserDisabled + r.AuthClientIDMetadataURL = raw.AuthClientIDMetadataURL + if raw.AuthInfo != nil { + value, err := unmarshalAuthInfo(raw.AuthInfo) + if err != nil { + return err + } + r.AuthInfo = value + } + r.AvailableTools = raw.AvailableTools + r.Capi = raw.Capi + r.ClientKind = raw.ClientKind + r.ClientName = raw.ClientName + r.CoauthorEnabled = raw.CoauthorEnabled + r.ConfigDir = raw.ConfigDir + r.ContinueOnAutoMode = raw.ContinueOnAutoMode + r.CopilotURL = raw.CopilotURL + r.CustomAgentsLocalOnly = raw.CustomAgentsLocalOnly + r.DetachedFromSpawningParentEngagementID = raw.DetachedFromSpawningParentEngagementID + r.DetachedFromSpawningParentSessionID = raw.DetachedFromSpawningParentSessionID + r.DisabledInstructionSources = raw.DisabledInstructionSources + r.DisabledMCPServers = raw.DisabledMCPServers + r.DisabledSkills = raw.DisabledSkills + r.EnableCitations = raw.EnableCitations + r.EnableFileChangeTracking = raw.EnableFileChangeTracking + r.EnableManagedSettings = raw.EnableManagedSettings + r.EnableOnDemandInstructionDiscovery = raw.EnableOnDemandInstructionDiscovery + r.EnableScriptSafety = raw.EnableScriptSafety + r.EnableSkills = raw.EnableSkills + r.EnableStreaming = raw.EnableStreaming + r.EnvValueMode = raw.EnvValueMode + r.EventsLogDirectory = raw.EventsLogDirectory + r.EventsLogIncludesSubagents = raw.EventsLogIncludesSubagents + r.ExcludedBuiltinAgents = raw.ExcludedBuiltinAgents + r.ExcludedTools = raw.ExcludedTools + r.ExpAssignments = raw.ExpAssignments + r.FeatureFlags = raw.FeatureFlags + r.HasSkillProvider = raw.HasSkillProvider + r.IgnoredSkillsLocations = raw.IgnoredSkillsLocations + r.IncludedBuiltinAgents = raw.IncludedBuiltinAgents + r.IncludedBuiltinSkills = raw.IncludedBuiltinSkills + r.InstalledPlugins = raw.InstalledPlugins + r.IntegrationID = raw.IntegrationID + r.IsExperimentalMode = raw.IsExperimentalMode + r.LogInteractiveShells = raw.LogInteractiveShells + r.LspClientName = raw.LspClientName + r.ManagedMCPServers = raw.ManagedMCPServers + r.ManagedSettings = raw.ManagedSettings + r.MaxInlineBinaryBytes = raw.MaxInlineBinaryBytes + r.Memory = raw.Memory + r.Model = raw.Model + r.ModelCapabilitiesOverrides = raw.ModelCapabilitiesOverrides + r.Models = raw.Models + r.Name = raw.Name + r.Provider = raw.Provider + r.Providers = raw.Providers + r.ReasoningEffort = raw.ReasoningEffort + r.ReasoningSummary = raw.ReasoningSummary + r.RefreshCustomInstructions = raw.RefreshCustomInstructions + r.RemoteDefaultedOn = raw.RemoteDefaultedOn + r.RemoteExporting = raw.RemoteExporting + r.RemoteSteerable = raw.RemoteSteerable + r.RunningInInteractiveMode = raw.RunningInInteractiveMode + r.SandboxConfig = raw.SandboxConfig + r.SandboxConfigSource = raw.SandboxConfigSource + r.SessionCapabilities = raw.SessionCapabilities + r.SessionID = raw.SessionID + r.SessionLimits = raw.SessionLimits + r.Shell = raw.Shell + r.ShellInitProfile = raw.ShellInitProfile + r.ShellProcessFlags = raw.ShellProcessFlags + r.SkillDirectories = raw.SkillDirectories + r.SkipCustomInstructions = raw.SkipCustomInstructions + r.TrajectoryFile = raw.TrajectoryFile + r.Verbosity = raw.Verbosity + r.WorkingDirectory = raw.WorkingDirectory + r.WorkingDirectoryContext = raw.WorkingDirectoryContext + return nil +} + +func unmarshalSessionOpenParams(data []byte) (SessionOpenParams, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind SessionOpenParamsKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case SessionOpenParamsKindAttach: + var d SessionsOpenAttach + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SessionOpenParamsKindCloud: + var d SessionsOpenCloud if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubRepository: - var d PushAttachmentGitHubRepository + case SessionOpenParamsKindCreate: + var d SessionsOpenCreate if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubSnippet: - var d PushAttachmentGitHubSnippet + case SessionOpenParamsKindHandoff: + var d SessionsOpenHandoff if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubTreeComparison: - var d PushAttachmentGitHubTreeComparison + case SessionOpenParamsKindRemote: + var d SessionsOpenRemote if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeGitHubURL: - var d PushAttachmentGitHubURL + case SessionOpenParamsKindResume: + var d SessionsOpenResume if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case PushAttachmentTypeSelection: - var d PushAttachmentSelection + case SessionOpenParamsKindResumeLast: + var d SessionsOpenResumeLast if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawPushAttachmentData{Discriminator: raw.Type, Raw: data}, nil + return &RawSessionOpenParamsData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawPushAttachmentData) MarshalJSON() ([]byte, error) { +func (r RawSessionOpenParamsData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - }{ - Type: r.Discriminator, - }) -} - -func (r ExtensionContextPushInput) MarshalJSON() ([]byte, error) { - type alias ExtensionContextPushInput - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentBlob) MarshalJSON() ([]byte, error) { - type alias PushAttachmentBlob - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentDirectory) MarshalJSON() ([]byte, error) { - type alias PushAttachmentDirectory - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentFile) MarshalJSON() ([]byte, error) { - type alias PushAttachmentFile - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubActionsJob) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubActionsJob - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubCommit) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubCommit - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubFile) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubFile - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubFileDiff) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubFileDiff - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubReference) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubReference - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubRelease) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubRelease - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubRepository) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubRepository - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r PushAttachmentGitHubSnippet) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubSnippet - return json.Marshal(struct { - Type PushAttachmentType `json:"type"` - alias + Kind SessionOpenParamsKind `json:"kind"` }{ - Type: r.Type(), - alias: alias(r), + Kind: r.Discriminator, }) } -func (r PushAttachmentGitHubTreeComparison) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubTreeComparison +func (r SessionsOpenAttach) MarshalJSON() ([]byte, error) { + type alias SessionsOpenAttach return json.Marshal(struct { - Type PushAttachmentType `json:"type"` + Kind SessionOpenParamsKind `json:"kind"` alias }{ - Type: r.Type(), + Kind: r.Kind(), alias: alias(r), }) } -func (r PushAttachmentGitHubURL) MarshalJSON() ([]byte, error) { - type alias PushAttachmentGitHubURL +func (r SessionsOpenCloud) MarshalJSON() ([]byte, error) { + type alias SessionsOpenCloud return json.Marshal(struct { - Type PushAttachmentType `json:"type"` + Kind SessionOpenParamsKind `json:"kind"` alias }{ - Type: r.Type(), + Kind: r.Kind(), alias: alias(r), }) } -func (r PushAttachmentSelection) MarshalJSON() ([]byte, error) { - type alias PushAttachmentSelection +func (r SessionsOpenCreate) MarshalJSON() ([]byte, error) { + type alias SessionsOpenCreate return json.Marshal(struct { - Type PushAttachmentType `json:"type"` + Kind SessionOpenParamsKind `json:"kind"` alias }{ - Type: r.Type(), + Kind: r.Kind(), alias: alias(r), }) } -func (r *QueueAppendSteeringRequest) UnmarshalJSON(data []byte) error { - type rawQueueAppendSteeringRequest struct { - AgentMode SendAgentMode `json:"agentMode"` - Attachments []json.RawMessage `json:"attachments"` - DisplayPrompt string `json:"displayPrompt"` - ExpectedPrompt string `json:"expectedPrompt"` - MessageID string `json:"messageId"` - Prompt string `json:"prompt"` - } - var raw rawQueueAppendSteeringRequest - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - r.AgentMode = raw.AgentMode - if raw.Attachments != nil { - r.Attachments = make([]Attachment, 0, len(raw.Attachments)) - for _, rawItem := range raw.Attachments { - value, err := unmarshalAttachment(rawItem) - if err != nil { - return err - } - r.Attachments = append(r.Attachments, value) - } - } - r.DisplayPrompt = raw.DisplayPrompt - r.ExpectedPrompt = raw.ExpectedPrompt - r.MessageID = raw.MessageID - r.Prompt = raw.Prompt - return nil -} - -func (r *QueueInsertMessage) UnmarshalJSON(data []byte) error { - type rawQueueInsertMessage struct { - AgentMode *SendAgentMode `json:"agentMode,omitempty"` - Attachments []json.RawMessage `json:"attachments,omitzero"` - Billable *bool `json:"billable,omitempty"` - Delivery *string `json:"delivery,omitempty"` - DisplayPrompt *string `json:"displayPrompt,omitempty"` - Mode *SendMode `json:"mode,omitempty"` - Prepend *bool `json:"prepend,omitempty"` - Prompt string `json:"prompt"` - RequestHeaders map[string]string `json:"requestHeaders,omitzero"` - RequiredTool *string `json:"requiredTool,omitempty"` - Source *string `json:"source,omitempty"` - Wait *bool `json:"wait,omitempty"` - } - var raw rawQueueInsertMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - r.AgentMode = raw.AgentMode - if raw.Attachments != nil { - r.Attachments = make([]Attachment, 0, len(raw.Attachments)) - for _, rawItem := range raw.Attachments { - value, err := unmarshalAttachment(rawItem) - if err != nil { - return err - } - r.Attachments = append(r.Attachments, value) - } - } - r.Billable = raw.Billable - r.Delivery = raw.Delivery - r.DisplayPrompt = raw.DisplayPrompt - r.Mode = raw.Mode - r.Prepend = raw.Prepend - r.Prompt = raw.Prompt - r.RequestHeaders = raw.RequestHeaders - r.RequiredTool = raw.RequiredTool - r.Source = raw.Source - r.Wait = raw.Wait - return nil +func (r SessionsOpenHandoff) MarshalJSON() ([]byte, error) { + type alias SessionsOpenHandoff + return json.Marshal(struct { + Kind SessionOpenParamsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) } -func unmarshalRemoteControlStatus(data []byte) (RemoteControlStatus, error) { +func (r SessionsOpenRemote) MarshalJSON() ([]byte, error) { + type alias SessionsOpenRemote + return json.Marshal(struct { + Kind SessionOpenParamsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r SessionsOpenResume) MarshalJSON() ([]byte, error) { + type alias SessionsOpenResume + return json.Marshal(struct { + Kind SessionOpenParamsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r SessionsOpenResumeLast) MarshalJSON() ([]byte, error) { + type alias SessionsOpenResumeLast + return json.Marshal(struct { + Kind SessionOpenParamsKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func unmarshalSessionsClientMetadataEntry(data []byte) (SessionsClientMetadataEntry, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - State RemoteControlStatusState `json:"state"` + Status SessionsClientMetadataEntryStatus `json:"status"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - switch raw.State { - case RemoteControlStatusStateActive: - var d RemoteControlStatusActive + switch raw.Status { + case SessionsClientMetadataEntryStatusCorrupt: + var d SessionsClientMetadataEntryCorrupt if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case RemoteControlStatusStateConnecting: - var d RemoteControlStatusConnecting + case SessionsClientMetadataEntryStatusNotFound: + var d SessionsClientMetadataEntryNotFound if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case RemoteControlStatusStateError: - var d RemoteControlStatusError + case SessionsClientMetadataEntryStatusOk: + var d SessionsClientMetadataEntryOk if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case RemoteControlStatusStateOff: - var d RemoteControlStatusOff + case SessionsClientMetadataEntryStatusUnavailable: + var d SessionsClientMetadataEntryUnavailable + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SessionsClientMetadataEntryStatusUnsupportedVersion: + var d SessionsClientMetadataEntryUnsupportedVersion if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawRemoteControlStatusData{Discriminator: raw.State, Raw: data}, nil + return &RawSessionsClientMetadataEntryData{Discriminator: raw.Status, Raw: data}, nil } } -func (r RawRemoteControlStatusData) MarshalJSON() ([]byte, error) { +func (r RawSessionsClientMetadataEntryData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - State RemoteControlStatusState `json:"state"` + Status SessionsClientMetadataEntryStatus `json:"status"` }{ - State: r.Discriminator, + Status: r.Discriminator, }) } -func (r RemoteControlStatusActive) MarshalJSON() ([]byte, error) { - type alias RemoteControlStatusActive +func (r SessionsClientMetadataEntryCorrupt) MarshalJSON() ([]byte, error) { + type alias SessionsClientMetadataEntryCorrupt return json.Marshal(struct { - State RemoteControlStatusState `json:"state"` + Status SessionsClientMetadataEntryStatus `json:"status"` alias }{ - State: r.State(), - alias: alias(r), + Status: r.Status(), + alias: alias(r), }) } -func (r RemoteControlStatusConnecting) MarshalJSON() ([]byte, error) { - type alias RemoteControlStatusConnecting +func (r SessionsClientMetadataEntryNotFound) MarshalJSON() ([]byte, error) { + type alias SessionsClientMetadataEntryNotFound return json.Marshal(struct { - State RemoteControlStatusState `json:"state"` + Status SessionsClientMetadataEntryStatus `json:"status"` alias }{ - State: r.State(), - alias: alias(r), + Status: r.Status(), + alias: alias(r), }) } -func (r RemoteControlStatusError) MarshalJSON() ([]byte, error) { - type alias RemoteControlStatusError +func (r SessionsClientMetadataEntryOk) MarshalJSON() ([]byte, error) { + type alias SessionsClientMetadataEntryOk return json.Marshal(struct { - State RemoteControlStatusState `json:"state"` + Status SessionsClientMetadataEntryStatus `json:"status"` alias }{ - State: r.State(), - alias: alias(r), + Status: r.Status(), + alias: alias(r), }) } -func (r RemoteControlStatusOff) MarshalJSON() ([]byte, error) { - type alias RemoteControlStatusOff +func (r SessionsClientMetadataEntryUnavailable) MarshalJSON() ([]byte, error) { + type alias SessionsClientMetadataEntryUnavailable return json.Marshal(struct { - State RemoteControlStatusState `json:"state"` + Status SessionsClientMetadataEntryStatus `json:"status"` alias }{ - State: r.State(), - alias: alias(r), + Status: r.Status(), + alias: alias(r), }) } -func (r *RemoteControlStatusResult) UnmarshalJSON(data []byte) error { - type rawRemoteControlStatusResult struct { - Status json.RawMessage `json:"status"` - } - var raw rawRemoteControlStatusResult - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.Status != nil { - value, err := unmarshalRemoteControlStatus(raw.Status) - if err != nil { - return err - } - r.Status = value - } - return nil -} - -func (r *RemoteControlStopResult) UnmarshalJSON(data []byte) error { - type rawRemoteControlStopResult struct { - Status json.RawMessage `json:"status"` - Stopped bool `json:"stopped"` - } - var raw rawRemoteControlStopResult - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.Status != nil { - value, err := unmarshalRemoteControlStatus(raw.Status) - if err != nil { - return err - } - r.Status = value - } - r.Stopped = raw.Stopped - return nil -} - -func (r *RemoteControlTransferResult) UnmarshalJSON(data []byte) error { - type rawRemoteControlTransferResult struct { - Status json.RawMessage `json:"status"` - Transferred bool `json:"transferred"` - } - var raw rawRemoteControlTransferResult - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.Status != nil { - value, err := unmarshalRemoteControlStatus(raw.Status) - if err != nil { - return err - } - r.Status = value - } - r.Transferred = raw.Transferred - return nil -} - -func (r *SendAttachmentsToMessageParams) UnmarshalJSON(data []byte) error { - type rawSendAttachmentsToMessageParams struct { - Attachments []json.RawMessage `json:"attachments"` - InstanceID *string `json:"instanceId,omitempty"` - } - var raw rawSendAttachmentsToMessageParams - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.Attachments != nil { - r.Attachments = make([]PushAttachment, 0, len(raw.Attachments)) - for _, rawItem := range raw.Attachments { - value, err := unmarshalPushAttachment(rawItem) - if err != nil { - return err - } - r.Attachments = append(r.Attachments, value) - } - } - r.InstanceID = raw.InstanceID - return nil +func (r SessionsClientMetadataEntryUnsupportedVersion) MarshalJSON() ([]byte, error) { + type alias SessionsClientMetadataEntryUnsupportedVersion + return json.Marshal(struct { + Status SessionsClientMetadataEntryStatus `json:"status"` + alias + }{ + Status: r.Status(), + alias: alias(r), + }) } -func (r *SendMessageItem) UnmarshalJSON(data []byte) error { - type rawSendMessageItem struct { - Attachments []json.RawMessage `json:"attachments,omitzero"` - Billable *bool `json:"billable,omitempty"` - DisplayPrompt *string `json:"displayPrompt,omitempty"` - Prompt string `json:"prompt"` - RequiredTool *string `json:"requiredTool,omitempty"` - Source *string `json:"source,omitempty"` - } - var raw rawSendMessageItem - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.Attachments != nil { - r.Attachments = make([]Attachment, 0, len(raw.Attachments)) - for _, rawItem := range raw.Attachments { - value, err := unmarshalAttachment(rawItem) - if err != nil { - return err - } - r.Attachments = append(r.Attachments, value) - } +func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { + if string(data) == "null" { + return nil, nil } - r.Billable = raw.Billable - r.DisplayPrompt = raw.DisplayPrompt - r.Prompt = raw.Prompt - r.RequiredTool = raw.RequiredTool - r.Source = raw.Source - return nil -} - -func (r *SendRequest) UnmarshalJSON(data []byte) error { - type rawSendRequest struct { - AgentMode *SendAgentMode `json:"agentMode,omitempty"` - Attachments []json.RawMessage `json:"attachments,omitzero"` - Billable *bool `json:"billable,omitempty"` - DisplayPrompt *string `json:"displayPrompt,omitempty"` - Mode *SendMode `json:"mode,omitempty"` - Prepend *bool `json:"prepend,omitempty"` - Prompt string `json:"prompt"` - RequestHeaders map[string]string `json:"requestHeaders,omitzero"` - RequiredTool *string `json:"requiredTool,omitempty"` - ResponseFormat *ResponseFormat `json:"responseFormat,omitempty"` - Source *string `json:"source,omitempty"` - Traceparent *string `json:"traceparent,omitempty"` - Tracestate *string `json:"tracestate,omitempty"` - Wait *bool `json:"wait,omitempty"` + type rawUnion struct { + Type SettableAuthInfoType `json:"type"` } - var raw rawSendRequest + var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { - return err + return nil, err } - r.AgentMode = raw.AgentMode - if raw.Attachments != nil { - r.Attachments = make([]Attachment, 0, len(raw.Attachments)) - for _, rawItem := range raw.Attachments { - value, err := unmarshalAttachment(rawItem) - if err != nil { - return err - } - r.Attachments = append(r.Attachments, value) + + switch raw.Type { + case SettableAuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeAPIKey: + var d APIKeyAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeCopilotAPIToken: + var d CopilotAPITokenAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeEnv: + var d EnvAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeGhCLI: + var d GhCLIAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeHMAC: + var d HMACAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err } + return &d, nil + case SettableAuthInfoTypeToken: + var d SettableTokenAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeUser: + var d UserAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawSettableAuthInfoData{Discriminator: raw.Type, Raw: data}, nil } - r.Billable = raw.Billable - r.DisplayPrompt = raw.DisplayPrompt - r.Mode = raw.Mode - r.Prepend = raw.Prepend - r.Prompt = raw.Prompt - r.RequestHeaders = raw.RequestHeaders - r.RequiredTool = raw.RequiredTool - r.ResponseFormat = raw.ResponseFormat - r.Source = raw.Source - r.Traceparent = raw.Traceparent - r.Tracestate = raw.Tracestate - r.Wait = raw.Wait - return nil } -func (r *SessionAuthLogoutUserRequest) UnmarshalJSON(data []byte) error { - type rawSessionAuthLogoutUserRequest struct { - AuthInfo json.RawMessage `json:"authInfo"` - } - var raw rawSessionAuthLogoutUserRequest - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.AuthInfo != nil { - value, err := unmarshalAuthInfo(raw.AuthInfo) - if err != nil { - return err - } - r.AuthInfo = value +func (r RawSettableAuthInfoData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } - return nil + return json.Marshal(struct { + Type SettableAuthInfoType `json:"type"` + }{ + Type: r.Discriminator, + }) } -func (r *SessionAuthSwitchRequest) UnmarshalJSON(data []byte) error { - type rawSessionAuthSwitchRequest struct { - AuthInfo json.RawMessage `json:"authInfo"` - Token *string `json:"token,omitempty"` +func (r SettableTokenAuthInfo) MarshalJSON() ([]byte, error) { + type alias SettableTokenAuthInfo + return json.Marshal(struct { + Type SettableAuthInfoType `json:"type"` + alias + }{ + Type: r.settableAuthInfoType(), + alias: alias(r), + }) +} + +func (r *SessionSetCredentialsParams) UnmarshalJSON(data []byte) error { + type rawSessionSetCredentialsParams struct { + Credentials json.RawMessage `json:"credentials,omitempty"` } - var raw rawSessionAuthSwitchRequest + var raw rawSessionSetCredentialsParams if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.AuthInfo != nil { - value, err := unmarshalAuthInfo(raw.AuthInfo) + if raw.Credentials != nil { + value, err := unmarshalSettableAuthInfo(raw.Credentials) if err != nil { return err } - r.AuthInfo = value + r.Credentials = value } - r.Token = raw.Token return nil } -func (r SessionInstalledPluginSource) MarshalJSON() ([]byte, error) { - if r.SessionInstalledPluginSourceGitHub != nil { - return json.Marshal(r.SessionInstalledPluginSourceGitHub) - } - if r.SessionInstalledPluginSourceLocal != nil { - return json.Marshal(r.SessionInstalledPluginSourceLocal) - } - if r.SessionInstalledPluginSourceURL != nil { - return json.Marshal(r.SessionInstalledPluginSourceURL) - } - if r.String != nil { - return json.Marshal(r.String) - } - return []byte("null"), nil -} - -func (r *SessionInstalledPluginSource) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - *r = SessionInstalledPluginSource{} - return nil - } - { - var value SessionInstalledPluginSourceGitHub - if err := json.Unmarshal(data, &value); err == nil { - *r = SessionInstalledPluginSource{SessionInstalledPluginSourceGitHub: &value} - return nil - } - } - { - var value SessionInstalledPluginSourceLocal - if err := json.Unmarshal(data, &value); err == nil { - *r = SessionInstalledPluginSource{SessionInstalledPluginSourceLocal: &value} - return nil - } - } - { - var value SessionInstalledPluginSourceURL - if err := json.Unmarshal(data, &value); err == nil { - *r = SessionInstalledPluginSource{SessionInstalledPluginSourceURL: &value} - return nil - } - } - { - var value string - if err := json.Unmarshal(data, &value); err == nil { - *r = SessionInstalledPluginSource{String: &value} - return nil - } - } - return errors.New("data did not match any union variant for SessionInstalledPluginSource") -} - -func unmarshalSessionLimitPredictionResult(data []byte) (SessionLimitPredictionResult, error) { +func unmarshalSkillInstallationManagementOutcome(data []byte) (SkillInstallationManagementOutcome, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind SessionLimitPredictionResultKind `json:"kind"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -6243,38 +8263,80 @@ func unmarshalSessionLimitPredictionResult(data []byte) (SessionLimitPredictionR } switch raw.Kind { - case SessionLimitPredictionResultKindAvailable: - var d SessionLimitPredictionResultAvailable + case SkillInstallationManagementOutcomeKindEnabledChanged: + var d SkillInstallationManagementOutcomeEnabledChanged if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SessionLimitPredictionResultKindUnavailable: - var d SessionLimitPredictionResultUnavailable + case SkillInstallationManagementOutcomeKindInstallPlanned: + var d SkillInstallationManagementOutcomeInstallPlanned + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationManagementOutcomeKindListed: + var d SkillInstallationManagementOutcomeListed + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationManagementOutcomeKindOperation: + var d SkillInstallationManagementOutcomeOperation + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationManagementOutcomeKindRecovered: + var d SkillInstallationManagementOutcomeRecovered + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationManagementOutcomeKindRecoveryRequired: + var d SkillInstallationManagementOutcomeRecoveryRequired + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationManagementOutcomeKindRefused: + var d SkillInstallationManagementOutcomeRefused + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationManagementOutcomeKindRolledBack: + var d SkillInstallationManagementOutcomeRolledBack + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationManagementOutcomeKindUninstallPlanned: + var d SkillInstallationManagementOutcomeUninstallPlanned if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawSessionLimitPredictionResultData{Discriminator: raw.Kind, Raw: data}, nil + return &RawSkillInstallationManagementOutcomeData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawSessionLimitPredictionResultData) MarshalJSON() ([]byte, error) { +func (r RawSkillInstallationManagementOutcomeData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Kind SessionLimitPredictionResultKind `json:"kind"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` }{ Kind: r.Discriminator, }) } -func (r SessionLimitPredictionResultAvailable) MarshalJSON() ([]byte, error) { - type alias SessionLimitPredictionResultAvailable +func (r SkillInstallationManagementOutcomeEnabledChanged) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeEnabledChanged return json.Marshal(struct { - Kind SessionLimitPredictionResultKind `json:"kind"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -6282,10 +8344,34 @@ func (r SessionLimitPredictionResultAvailable) MarshalJSON() ([]byte, error) { }) } -func (r SessionLimitPredictionResultUnavailable) MarshalJSON() ([]byte, error) { - type alias SessionLimitPredictionResultUnavailable +func (r *SkillInstallPlan) UnmarshalJSON(data []byte) error { + type rawSkillInstallPlan struct { + ExpiresAt string `json:"expiresAt"` + OperationID string `json:"operationId"` + PlanHandle string `json:"planHandle"` + Review json.RawMessage `json:"review"` + } + var raw rawSkillInstallPlan + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.ExpiresAt = raw.ExpiresAt + r.OperationID = raw.OperationID + r.PlanHandle = raw.PlanHandle + if raw.Review != nil { + value, err := unmarshalSkillInstallationReview(raw.Review) + if err != nil { + return err + } + r.Review = value + } + return nil +} + +func (r SkillInstallationManagementOutcomeInstallPlanned) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeInstallPlanned return json.Marshal(struct { - Kind SessionLimitPredictionResultKind `json:"kind"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -6293,253 +8379,110 @@ func (r SessionLimitPredictionResultUnavailable) MarshalJSON() ([]byte, error) { }) } -func unmarshalSessionListEntry(data []byte) (SessionListEntry, error) { +func (r SkillInstallationManagementOutcomeListed) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeListed + return json.Marshal(struct { + Kind SkillInstallationManagementOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func unmarshalSkillInstallationOperationStatus(data []byte) (SkillInstallationOperationStatus, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - IsRemote *bool `json:"isRemote"` + Phase SkillInstallationOperationStatusPhase `json:"phase"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - if raw.IsRemote == nil { - return nil, errors.New("data did not match any union variant for SessionListEntry") - } - switch *raw.IsRemote { - case false: - var d LocalSessionMetadataValue + switch raw.Phase { + case SkillInstallationOperationStatusPhaseApplying: + var d SkillInstallationOperationStatusApplying + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationOperationStatusPhaseAwaitingConfirmation: + var d SkillInstallationOperationStatusAwaitingConfirmation + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationOperationStatusPhaseCompleted: + var d SkillInstallationOperationStatusCompleted if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case true: - var d RemoteSessionMetadataValue + case SkillInstallationOperationStatusPhasePrepared: + var d SkillInstallationOperationStatusPrepared + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationOperationStatusPhasePreparing: + var d SkillInstallationOperationStatusPreparing + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SkillInstallationOperationStatusPhaseRevalidating: + var d SkillInstallationOperationStatusRevalidating if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil + default: + return &RawSkillInstallationOperationStatusData{Discriminator: raw.Phase, Raw: data}, nil } - return nil, errors.New("data did not match any union variant for SessionListEntry") } -func (r LocalSessionMetadataValue) MarshalJSON() ([]byte, error) { - type alias LocalSessionMetadataValue +func (r RawSkillInstallationOperationStatusData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } return json.Marshal(struct { - IsRemote bool `json:"isRemote"` - alias + Phase SkillInstallationOperationStatusPhase `json:"phase"` }{ - IsRemote: r.sessionListEntryIsRemote(), - alias: alias(r), + Phase: r.Discriminator, }) } -func (r RemoteSessionMetadataValue) MarshalJSON() ([]byte, error) { - type alias RemoteSessionMetadataValue +func (r SkillInstallationOperationStatusApplying) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOperationStatusApplying return json.Marshal(struct { - IsRemote bool `json:"isRemote"` + Phase SkillInstallationOperationStatusPhase `json:"phase"` alias }{ - IsRemote: r.sessionListEntryIsRemote(), - alias: alias(r), + Phase: r.Phase(), + alias: alias(r), }) } -func (r *SessionList) UnmarshalJSON(data []byte) error { - type rawSessionList struct { - Sessions []json.RawMessage `json:"sessions"` - } - var raw rawSessionList - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - if raw.Sessions != nil { - r.Sessions = make([]SessionListEntry, 0, len(raw.Sessions)) - for _, rawItem := range raw.Sessions { - value, err := unmarshalSessionListEntry(rawItem) - if err != nil { - return err - } - r.Sessions = append(r.Sessions, value) - } - } - return nil -} - -func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { - type rawSessionOpenOptions struct { - AdditionalContentExclusionPolicies []SessionOpenOptionsAdditionalContentExclusionPolicy `json:"additionalContentExclusionPolicies,omitzero"` - AdditionalDirectories []string `json:"additionalDirectories,omitzero"` - AgentContext *string `json:"agentContext,omitempty"` - AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` - AskUserDisabled *bool `json:"askUserDisabled,omitempty"` - AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` - AuthInfo json.RawMessage `json:"authInfo,omitempty"` - AvailableTools []string `json:"availableTools,omitzero"` - Capi *CapiSessionOptions `json:"capi,omitempty"` - ClientKind *string `json:"clientKind,omitempty"` - ClientName *string `json:"clientName,omitempty"` - CoauthorEnabled *bool `json:"coauthorEnabled,omitempty"` - ConfigDir *string `json:"configDir,omitempty"` - ContinueOnAutoMode *bool `json:"continueOnAutoMode,omitempty"` - CopilotURL *string `json:"copilotUrl,omitempty"` - CustomAgentsLocalOnly *bool `json:"customAgentsLocalOnly,omitempty"` - DetachedFromSpawningParentEngagementID *string `json:"detachedFromSpawningParentEngagementId,omitempty"` - DetachedFromSpawningParentSessionID *string `json:"detachedFromSpawningParentSessionId,omitempty"` - DisabledInstructionSources []string `json:"disabledInstructionSources,omitzero"` - DisabledMCPServers []string `json:"disabledMcpServers,omitzero"` - DisabledSkills []string `json:"disabledSkills,omitzero"` - EnableCitations *bool `json:"enableCitations,omitempty"` - EnableFileChangeTracking *bool `json:"enableFileChangeTracking,omitempty"` - EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` - EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` - EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` - EnableSkills *bool `json:"enableSkills,omitempty"` - EnableStreaming *bool `json:"enableStreaming,omitempty"` - EnvValueMode *SessionOpenOptionsEnvValueMode `json:"envValueMode,omitempty"` - EventsLogDirectory *string `json:"eventsLogDirectory,omitempty"` - EventsLogIncludesSubagents *bool `json:"eventsLogIncludesSubagents,omitempty"` - ExcludedBuiltinAgents []string `json:"excludedBuiltinAgents,omitzero"` - ExcludedTools []string `json:"excludedTools,omitzero"` - ExpAssignments any `json:"expAssignments,omitempty"` - FeatureFlags map[string]bool `json:"featureFlags,omitzero"` - HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` - IgnoredSkillsLocations []string `json:"ignoredSkillsLocations,omitzero"` - IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` - IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` - InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` - IntegrationID *string `json:"integrationId,omitempty"` - IsExperimentalMode *bool `json:"isExperimentalMode,omitempty"` - LogInteractiveShells *bool `json:"logInteractiveShells,omitempty"` - LspClientName *string `json:"lspClientName,omitempty"` - ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitzero"` - ManagedSettings *SessionManagedSettings `json:"managedSettings,omitempty"` - MaxInlineBinaryBytes *int64 `json:"maxInlineBinaryBytes,omitempty"` - Memory *MemoryConfiguration `json:"memory,omitempty"` - Model *string `json:"model,omitempty"` - ModelCapabilitiesOverrides *ModelCapabilitiesOverride `json:"modelCapabilitiesOverrides,omitempty"` - Models []ProviderModelConfig `json:"models,omitzero"` - Name *string `json:"name,omitempty"` - Provider *ProviderConfig `json:"provider,omitempty"` - Providers []NamedProviderConfig `json:"providers,omitzero"` - ReasoningEffort *string `json:"reasoningEffort,omitempty"` - ReasoningSummary *SessionOpenOptionsReasoningSummary `json:"reasoningSummary,omitempty"` - RefreshCustomInstructions *bool `json:"refreshCustomInstructions,omitempty"` - RemoteDefaultedOn *bool `json:"remoteDefaultedOn,omitempty"` - RemoteExporting *bool `json:"remoteExporting,omitempty"` - RemoteSteerable *bool `json:"remoteSteerable,omitempty"` - RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` - SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` - SandboxConfigSource *SandboxConfigSource `json:"sandboxConfigSource,omitempty"` - SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` - SessionID *string `json:"sessionId,omitempty"` - SessionLimits *SessionLimitsConfig `json:"sessionLimits,omitempty"` - Shell *ShellOptions `json:"shell,omitempty"` - ShellInitProfile *string `json:"shellInitProfile,omitempty"` - ShellProcessFlags []string `json:"shellProcessFlags,omitzero"` - SkillDirectories []string `json:"skillDirectories,omitzero"` - SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` - TrajectoryFile *string `json:"trajectoryFile,omitempty"` - Verbosity *Verbosity `json:"verbosity,omitempty"` - WorkingDirectory *string `json:"workingDirectory,omitempty"` - WorkingDirectoryContext *SessionContext `json:"workingDirectoryContext,omitempty"` - } - var raw rawSessionOpenOptions - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - r.AdditionalContentExclusionPolicies = raw.AdditionalContentExclusionPolicies - r.AdditionalDirectories = raw.AdditionalDirectories - r.AgentContext = raw.AgentContext - r.AllowAllMCPServerInstructions = raw.AllowAllMCPServerInstructions - r.AskUserDisabled = raw.AskUserDisabled - r.AuthClientIDMetadataURL = raw.AuthClientIDMetadataURL - if raw.AuthInfo != nil { - value, err := unmarshalAuthInfo(raw.AuthInfo) - if err != nil { - return err - } - r.AuthInfo = value - } - r.AvailableTools = raw.AvailableTools - r.Capi = raw.Capi - r.ClientKind = raw.ClientKind - r.ClientName = raw.ClientName - r.CoauthorEnabled = raw.CoauthorEnabled - r.ConfigDir = raw.ConfigDir - r.ContinueOnAutoMode = raw.ContinueOnAutoMode - r.CopilotURL = raw.CopilotURL - r.CustomAgentsLocalOnly = raw.CustomAgentsLocalOnly - r.DetachedFromSpawningParentEngagementID = raw.DetachedFromSpawningParentEngagementID - r.DetachedFromSpawningParentSessionID = raw.DetachedFromSpawningParentSessionID - r.DisabledInstructionSources = raw.DisabledInstructionSources - r.DisabledMCPServers = raw.DisabledMCPServers - r.DisabledSkills = raw.DisabledSkills - r.EnableCitations = raw.EnableCitations - r.EnableFileChangeTracking = raw.EnableFileChangeTracking - r.EnableManagedSettings = raw.EnableManagedSettings - r.EnableOnDemandInstructionDiscovery = raw.EnableOnDemandInstructionDiscovery - r.EnableScriptSafety = raw.EnableScriptSafety - r.EnableSkills = raw.EnableSkills - r.EnableStreaming = raw.EnableStreaming - r.EnvValueMode = raw.EnvValueMode - r.EventsLogDirectory = raw.EventsLogDirectory - r.EventsLogIncludesSubagents = raw.EventsLogIncludesSubagents - r.ExcludedBuiltinAgents = raw.ExcludedBuiltinAgents - r.ExcludedTools = raw.ExcludedTools - r.ExpAssignments = raw.ExpAssignments - r.FeatureFlags = raw.FeatureFlags - r.HasSkillProvider = raw.HasSkillProvider - r.IgnoredSkillsLocations = raw.IgnoredSkillsLocations - r.IncludedBuiltinAgents = raw.IncludedBuiltinAgents - r.IncludedBuiltinSkills = raw.IncludedBuiltinSkills - r.InstalledPlugins = raw.InstalledPlugins - r.IntegrationID = raw.IntegrationID - r.IsExperimentalMode = raw.IsExperimentalMode - r.LogInteractiveShells = raw.LogInteractiveShells - r.LspClientName = raw.LspClientName - r.ManagedMCPServers = raw.ManagedMCPServers - r.ManagedSettings = raw.ManagedSettings - r.MaxInlineBinaryBytes = raw.MaxInlineBinaryBytes - r.Memory = raw.Memory - r.Model = raw.Model - r.ModelCapabilitiesOverrides = raw.ModelCapabilitiesOverrides - r.Models = raw.Models - r.Name = raw.Name - r.Provider = raw.Provider - r.Providers = raw.Providers - r.ReasoningEffort = raw.ReasoningEffort - r.ReasoningSummary = raw.ReasoningSummary - r.RefreshCustomInstructions = raw.RefreshCustomInstructions - r.RemoteDefaultedOn = raw.RemoteDefaultedOn - r.RemoteExporting = raw.RemoteExporting - r.RemoteSteerable = raw.RemoteSteerable - r.RunningInInteractiveMode = raw.RunningInInteractiveMode - r.SandboxConfig = raw.SandboxConfig - r.SandboxConfigSource = raw.SandboxConfigSource - r.SessionCapabilities = raw.SessionCapabilities - r.SessionID = raw.SessionID - r.SessionLimits = raw.SessionLimits - r.Shell = raw.Shell - r.ShellInitProfile = raw.ShellInitProfile - r.ShellProcessFlags = raw.ShellProcessFlags - r.SkillDirectories = raw.SkillDirectories - r.SkipCustomInstructions = raw.SkipCustomInstructions - r.TrajectoryFile = raw.TrajectoryFile - r.Verbosity = raw.Verbosity - r.WorkingDirectory = raw.WorkingDirectory - r.WorkingDirectoryContext = raw.WorkingDirectoryContext - return nil +func (r SkillInstallationOperationStatusAwaitingConfirmation) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOperationStatusAwaitingConfirmation + return json.Marshal(struct { + Phase SkillInstallationOperationStatusPhase `json:"phase"` + alias + }{ + Phase: r.Phase(), + alias: alias(r), + }) } -func unmarshalSessionOpenParams(data []byte) (SessionOpenParams, error) { +func unmarshalSkillInstallationOutcome(data []byte) (SkillInstallationOutcome, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Kind SessionOpenParamsKind `json:"kind"` + Kind SkillInstallationOutcomeKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -6547,68 +8490,101 @@ func unmarshalSessionOpenParams(data []byte) (SessionOpenParams, error) { } switch raw.Kind { - case SessionOpenParamsKindAttach: - var d SessionsOpenAttach + case SkillInstallationOutcomeKindCancelled: + var d SkillInstallationOutcomeCancelled if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SessionOpenParamsKindCloud: - var d SessionsOpenCloud + case SkillInstallationOutcomeKindDeclined: + var d SkillInstallationOutcomeDeclined if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SessionOpenParamsKindCreate: - var d SessionsOpenCreate + case SkillInstallationOutcomeKindInstalled: + var d SkillInstallationOutcomeInstalled if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SessionOpenParamsKindHandoff: - var d SessionsOpenHandoff + case SkillInstallationOutcomeKindRecoveryRequired: + var d SkillInstallationOutcomeRecoveryRequired if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SessionOpenParamsKindRemote: - var d SessionsOpenRemote + case SkillInstallationOutcomeKindRefused: + var d SkillInstallationOutcomeRefused if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SessionOpenParamsKindResume: - var d SessionsOpenResume + case SkillInstallationOutcomeKindRolledBack: + var d SkillInstallationOutcomeRolledBack if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SessionOpenParamsKindResumeLast: - var d SessionsOpenResumeLast + case SkillInstallationOutcomeKindUninstalled: + var d SkillInstallationOutcomeUninstalled if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawSessionOpenParamsData{Discriminator: raw.Kind, Raw: data}, nil + return &RawSkillInstallationOutcomeData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawSkillInstallationOutcomeData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil } + return json.Marshal(struct { + Kind SkillInstallationOutcomeKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r SkillInstallationOutcomeCancelled) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOutcomeCancelled + return json.Marshal(struct { + Kind SkillInstallationOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r SkillInstallationOutcomeDeclined) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOutcomeDeclined + return json.Marshal(struct { + Kind SkillInstallationOutcomeKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) } -func (r RawSessionOpenParamsData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } +func (r SkillInstallationOutcomeInstalled) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOutcomeInstalled return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Kind SkillInstallationOutcomeKind `json:"kind"` + alias }{ - Kind: r.Discriminator, + Kind: r.Kind(), + alias: alias(r), }) } -func (r SessionsOpenAttach) MarshalJSON() ([]byte, error) { - type alias SessionsOpenAttach +func (r SkillInstallationOutcomeRecoveryRequired) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOutcomeRecoveryRequired return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Kind SkillInstallationOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -6616,10 +8592,10 @@ func (r SessionsOpenAttach) MarshalJSON() ([]byte, error) { }) } -func (r SessionsOpenCloud) MarshalJSON() ([]byte, error) { - type alias SessionsOpenCloud +func (r SkillInstallationOutcomeRefused) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOutcomeRefused return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Kind SkillInstallationOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -6627,10 +8603,10 @@ func (r SessionsOpenCloud) MarshalJSON() ([]byte, error) { }) } -func (r SessionsOpenCreate) MarshalJSON() ([]byte, error) { - type alias SessionsOpenCreate +func (r SkillInstallationOutcomeRolledBack) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOutcomeRolledBack return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Kind SkillInstallationOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -6638,10 +8614,10 @@ func (r SessionsOpenCreate) MarshalJSON() ([]byte, error) { }) } -func (r SessionsOpenHandoff) MarshalJSON() ([]byte, error) { - type alias SessionsOpenHandoff +func (r SkillInstallationOutcomeUninstalled) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOutcomeUninstalled return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Kind SkillInstallationOutcomeKind `json:"kind"` alias }{ Kind: r.Kind(), @@ -6649,253 +8625,338 @@ func (r SessionsOpenHandoff) MarshalJSON() ([]byte, error) { }) } -func (r SessionsOpenRemote) MarshalJSON() ([]byte, error) { - type alias SessionsOpenRemote +func (r *SkillInstallationOperationStatusCompleted) UnmarshalJSON(data []byte) error { + type rawSkillInstallationOperationStatusCompleted struct { + CancellationRequested bool `json:"cancellationRequested"` + OperationID string `json:"operationId"` + Outcome json.RawMessage `json:"outcome"` + } + var raw rawSkillInstallationOperationStatusCompleted + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.CancellationRequested = raw.CancellationRequested + r.OperationID = raw.OperationID + if raw.Outcome != nil { + value, err := unmarshalSkillInstallationOutcome(raw.Outcome) + if err != nil { + return err + } + r.Outcome = value + } + return nil +} + +func (r SkillInstallationOperationStatusCompleted) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOperationStatusCompleted return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Phase SkillInstallationOperationStatusPhase `json:"phase"` alias }{ - Kind: r.Kind(), + Phase: r.Phase(), alias: alias(r), }) } -func (r SessionsOpenResume) MarshalJSON() ([]byte, error) { - type alias SessionsOpenResume +func (r SkillInstallationOperationStatusPrepared) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOperationStatusPrepared return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Phase SkillInstallationOperationStatusPhase `json:"phase"` alias }{ - Kind: r.Kind(), + Phase: r.Phase(), alias: alias(r), }) } -func (r SessionsOpenResumeLast) MarshalJSON() ([]byte, error) { - type alias SessionsOpenResumeLast +func (r SkillInstallationOperationStatusPreparing) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOperationStatusPreparing return json.Marshal(struct { - Kind SessionOpenParamsKind `json:"kind"` + Phase SkillInstallationOperationStatusPhase `json:"phase"` alias }{ - Kind: r.Kind(), + Phase: r.Phase(), alias: alias(r), }) } -func unmarshalSessionsClientMetadataEntry(data []byte) (SessionsClientMetadataEntry, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Status SessionsClientMetadataEntryStatus `json:"status"` +func (r SkillInstallationOperationStatusRevalidating) MarshalJSON() ([]byte, error) { + type alias SkillInstallationOperationStatusRevalidating + return json.Marshal(struct { + Phase SkillInstallationOperationStatusPhase `json:"phase"` + alias + }{ + Phase: r.Phase(), + alias: alias(r), + }) +} + +func (r *SkillInstallationManagementOutcomeOperation) UnmarshalJSON(data []byte) error { + type rawSkillInstallationManagementOutcomeOperation struct { + Operation json.RawMessage `json:"operation"` } - var raw rawUnion + var raw rawSkillInstallationManagementOutcomeOperation if err := json.Unmarshal(data, &raw); err != nil { - return nil, err + return err } - - switch raw.Status { - case SessionsClientMetadataEntryStatusCorrupt: - var d SessionsClientMetadataEntryCorrupt - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case SessionsClientMetadataEntryStatusNotFound: - var d SessionsClientMetadataEntryNotFound - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case SessionsClientMetadataEntryStatusOk: - var d SessionsClientMetadataEntryOk - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case SessionsClientMetadataEntryStatusUnavailable: - var d SessionsClientMetadataEntryUnavailable - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case SessionsClientMetadataEntryStatusUnsupportedVersion: - var d SessionsClientMetadataEntryUnsupportedVersion - if err := json.Unmarshal(data, &d); err != nil { - return nil, err + if raw.Operation != nil { + value, err := unmarshalSkillInstallationOperationStatus(raw.Operation) + if err != nil { + return err } - return &d, nil - default: - return &RawSessionsClientMetadataEntryData{Discriminator: raw.Status, Raw: data}, nil + r.Operation = value } + return nil } -func (r RawSessionsClientMetadataEntryData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } +func (r SkillInstallationManagementOutcomeOperation) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeOperation return json.Marshal(struct { - Status SessionsClientMetadataEntryStatus `json:"status"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` + alias }{ - Status: r.Discriminator, + Kind: r.Kind(), + alias: alias(r), }) } -func (r SessionsClientMetadataEntryCorrupt) MarshalJSON() ([]byte, error) { - type alias SessionsClientMetadataEntryCorrupt +func (r SkillInstallationManagementOutcomeRecovered) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeRecovered return json.Marshal(struct { - Status SessionsClientMetadataEntryStatus `json:"status"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` alias }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Kind(), + alias: alias(r), }) } -func (r SessionsClientMetadataEntryNotFound) MarshalJSON() ([]byte, error) { - type alias SessionsClientMetadataEntryNotFound +func (r SkillInstallationManagementOutcomeRecoveryRequired) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeRecoveryRequired return json.Marshal(struct { - Status SessionsClientMetadataEntryStatus `json:"status"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` alias }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Kind(), + alias: alias(r), }) } -func (r SessionsClientMetadataEntryOk) MarshalJSON() ([]byte, error) { - type alias SessionsClientMetadataEntryOk +func (r SkillInstallationManagementOutcomeRefused) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeRefused return json.Marshal(struct { - Status SessionsClientMetadataEntryStatus `json:"status"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` alias }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Kind(), + alias: alias(r), }) } -func (r SessionsClientMetadataEntryUnavailable) MarshalJSON() ([]byte, error) { - type alias SessionsClientMetadataEntryUnavailable +func (r SkillInstallationManagementOutcomeRolledBack) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeRolledBack return json.Marshal(struct { - Status SessionsClientMetadataEntryStatus `json:"status"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` alias }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Kind(), + alias: alias(r), }) } -func (r SessionsClientMetadataEntryUnsupportedVersion) MarshalJSON() ([]byte, error) { - type alias SessionsClientMetadataEntryUnsupportedVersion +func (r *SkillUninstallPlan) UnmarshalJSON(data []byte) error { + type rawSkillUninstallPlan struct { + ExpiresAt string `json:"expiresAt"` + Installation SkillInstallationSummary `json:"installation"` + OperationID string `json:"operationId"` + PlanHandle string `json:"planHandle"` + Review json.RawMessage `json:"review"` + } + var raw rawSkillUninstallPlan + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.ExpiresAt = raw.ExpiresAt + r.Installation = raw.Installation + r.OperationID = raw.OperationID + r.PlanHandle = raw.PlanHandle + if raw.Review != nil { + value, err := unmarshalSkillInstallationReview(raw.Review) + if err != nil { + return err + } + r.Review = value + } + return nil +} + +func (r SkillInstallationManagementOutcomeUninstallPlanned) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementOutcomeUninstallPlanned return json.Marshal(struct { - Status SessionsClientMetadataEntryStatus `json:"status"` + Kind SkillInstallationManagementOutcomeKind `json:"kind"` alias }{ - Status: r.Status(), - alias: alias(r), + Kind: r.Kind(), + alias: alias(r), }) } -func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { +func unmarshalSkillInstallationManagementResult(data []byte) (SkillInstallationManagementResult, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Type SettableAuthInfoType `json:"type"` + Kind SkillInstallationManagementResultKind `json:"kind"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { return nil, err } - switch raw.Type { - case SettableAuthInfoTypeAPIKey: - var d APIKeyAuthInfo + switch raw.Kind { + case SkillInstallationManagementResultKindInvalidRequest: + var d CatalogInvalidRequestError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SettableAuthInfoTypeCopilotAPIToken: - var d CopilotAPITokenAuthInfo + case SkillInstallationManagementResultKindNegotiationRefused: + var d CatalogNegotiationRefusedError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SettableAuthInfoTypeEnv: - var d EnvAuthInfo + case SkillInstallationManagementResultKindOutcome: + var d SkillInstallationManagementResultOutcome if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SettableAuthInfoTypeGhCLI: - var d GhCLIAuthInfo - if err := json.Unmarshal(data, &d); err != nil { - return nil, err + default: + return &RawSkillInstallationManagementResultData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawSkillInstallationManagementResultData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind SkillInstallationManagementResultKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r *SkillInstallationManagementResultOutcome) UnmarshalJSON(data []byte) error { + type rawSkillInstallationManagementResultOutcome struct { + Negotiated CatalogNegotiatedContract `json:"negotiated"` + Outcome json.RawMessage `json:"outcome"` + } + var raw rawSkillInstallationManagementResultOutcome + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.Negotiated = raw.Negotiated + if raw.Outcome != nil { + value, err := unmarshalSkillInstallationManagementOutcome(raw.Outcome) + if err != nil { + return err } - return &d, nil - case SettableAuthInfoTypeHMAC: - var d HMACAuthInfo + r.Outcome = value + } + return nil +} + +func (r SkillInstallationManagementResultOutcome) MarshalJSON() ([]byte, error) { + type alias SkillInstallationManagementResultOutcome + return json.Marshal(struct { + Kind SkillInstallationManagementResultKind `json:"kind"` + alias + }{ + Kind: r.skillInstallationManagementResultKind(), + alias: alias(r), + }) +} + +func unmarshalSkillInstallationResult(data []byte) (SkillInstallationResult, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind SkillInstallationResultKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case SkillInstallationResultKindInvalidRequest: + var d CatalogInvalidRequestError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SettableAuthInfoTypeToken: - var d SettableTokenAuthInfo + case SkillInstallationResultKindNegotiationRefused: + var d CatalogNegotiationRefusedError if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SettableAuthInfoTypeUser: - var d UserAuthInfo + case SkillInstallationResultKindOutcome: + var d SkillInstallationResultOutcome if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawSettableAuthInfoData{Discriminator: raw.Type, Raw: data}, nil + return &RawSkillInstallationResultData{Discriminator: raw.Kind, Raw: data}, nil } } -func (r RawSettableAuthInfoData) MarshalJSON() ([]byte, error) { +func (r RawSkillInstallationResultData) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Type SettableAuthInfoType `json:"type"` - }{ - Type: r.Discriminator, - }) -} - -func (r SettableTokenAuthInfo) MarshalJSON() ([]byte, error) { - type alias SettableTokenAuthInfo - return json.Marshal(struct { - Type SettableAuthInfoType `json:"type"` - alias + Kind SkillInstallationResultKind `json:"kind"` }{ - Type: r.settableAuthInfoType(), - alias: alias(r), + Kind: r.Discriminator, }) } -func (r *SessionSetCredentialsParams) UnmarshalJSON(data []byte) error { - type rawSessionSetCredentialsParams struct { - Credentials json.RawMessage `json:"credentials,omitempty"` +func (r *SkillInstallationResultOutcome) UnmarshalJSON(data []byte) error { + type rawSkillInstallationResultOutcome struct { + Negotiated CatalogNegotiatedContract `json:"negotiated"` + Outcome json.RawMessage `json:"outcome"` } - var raw rawSessionSetCredentialsParams + var raw rawSkillInstallationResultOutcome if err := json.Unmarshal(data, &raw); err != nil { return err } - if raw.Credentials != nil { - value, err := unmarshalSettableAuthInfo(raw.Credentials) + r.Negotiated = raw.Negotiated + if raw.Outcome != nil { + value, err := unmarshalSkillInstallationOutcome(raw.Outcome) if err != nil { return err } - r.Credentials = value + r.Outcome = value } return nil } +func (r SkillInstallationResultOutcome) MarshalJSON() ([]byte, error) { + type alias SkillInstallationResultOutcome + return json.Marshal(struct { + Kind SkillInstallationResultKind `json:"kind"` + alias + }{ + Kind: r.skillInstallationResultKind(), + alias: alias(r), + }) +} + func unmarshalSlashCommandInvocationResult(data []byte) (SlashCommandInvocationResult, error) { if string(data) == "null" { return nil, nil diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index a840934e39..fa44772dab 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -233,24 +233,6 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d - case SessionEventTypeFactoryRunSettled: - var d FactoryRunSettledData - if err := json.Unmarshal(raw.Data, &d); err != nil { - return err - } - e.Data = &d - case SessionEventTypeFactoryRunStarted: - var d FactoryRunStartedData - if err := json.Unmarshal(raw.Data, &d); err != nil { - return err - } - e.Data = &d - case SessionEventTypeFactoryRunUpdated: - var d FactoryRunUpdatedData - if err := json.Unmarshal(raw.Data, &d); err != nil { - return err - } - e.Data = &d case SessionEventTypeHookEnd: var d HookEndData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -269,6 +251,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeHumanResponseRecorded: + var d HumanResponseRecordedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeMCPAppToolCallComplete: var d MCPAppToolCallCompleteData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -323,6 +311,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeModelCallFinalResult: + var d ModelCallFinalResultData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeModelCallFinished: var d ModelCallFinishedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -899,6 +893,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeToolShellOutput: + var d ToolShellOutputData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeToolUserRequested: var d ToolUserRequestedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -929,6 +929,24 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeWorkflowRunSettled: + var d WorkflowRunSettledData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeWorkflowRunStarted: + var d WorkflowRunStartedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeWorkflowRunUpdated: + var d WorkflowRunUpdatedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d default: e.Data = &RawSessionEventData{EventType: raw.Type, Raw: raw.Data} } @@ -1584,12 +1602,6 @@ func unmarshalSystemNotification(data []byte) (SystemNotification, error) { return nil, err } return &d, nil - case SystemNotificationTypeFactoryCompleted: - var d SystemNotificationFactoryCompleted - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil case SystemNotificationTypeInstructionDiscovered: var d SystemNotificationInstructionDiscovered if err := json.Unmarshal(data, &d); err != nil { @@ -1620,6 +1632,12 @@ func unmarshalSystemNotification(data []byte) (SystemNotification, error) { return nil, err } return &d, nil + case SystemNotificationTypeWorkflowCompleted: + var d SystemNotificationWorkflowCompleted + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil default: return &RawSystemNotification{Discriminator: raw.Type, Raw: data}, nil } @@ -1658,12 +1676,67 @@ func (r SystemNotificationAgentIdle) MarshalJSON() ([]byte, error) { }) } -func unmarshalSystemNotificationFactoryPauseInfo(data []byte) (SystemNotificationFactoryPauseInfo, error) { +func (r SystemNotificationInstructionDiscovered) MarshalJSON() ([]byte, error) { + type alias SystemNotificationInstructionDiscovered + return json.Marshal(struct { + Type SystemNotificationType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r SystemNotificationNewInboxMessage) MarshalJSON() ([]byte, error) { + type alias SystemNotificationNewInboxMessage + return json.Marshal(struct { + Type SystemNotificationType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r SystemNotificationShellCompleted) MarshalJSON() ([]byte, error) { + type alias SystemNotificationShellCompleted + return json.Marshal(struct { + Type SystemNotificationType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r SystemNotificationShellDetachedCompleted) MarshalJSON() ([]byte, error) { + type alias SystemNotificationShellDetachedCompleted + return json.Marshal(struct { + Type SystemNotificationType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func (r SystemNotificationUnclassified) MarshalJSON() ([]byte, error) { + type alias SystemNotificationUnclassified + return json.Marshal(struct { + Type SystemNotificationType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + +func unmarshalSystemNotificationWorkflowPauseInfo(data []byte) (SystemNotificationWorkflowPauseInfo, error) { if string(data) == "null" { return nil, nil } type rawUnion struct { - Type SystemNotificationFactoryPauseInfoType `json:"type"` + Type SystemNotificationWorkflowPauseInfoType `json:"type"` } var raw rawUnion if err := json.Unmarshal(data, &raw); err != nil { @@ -1671,38 +1744,38 @@ func unmarshalSystemNotificationFactoryPauseInfo(data []byte) (SystemNotificatio } switch raw.Type { - case SystemNotificationFactoryPauseInfoTypeCheckpoint: - var d SystemNotificationFactoryPauseInfoCheckpoint + case SystemNotificationWorkflowPauseInfoTypeCheckpoint: + var d SystemNotificationWorkflowPauseInfoCheckpoint if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil - case SystemNotificationFactoryPauseInfoTypeUser: - var d SystemNotificationFactoryPauseInfoUser + case SystemNotificationWorkflowPauseInfoTypeUser: + var d SystemNotificationWorkflowPauseInfoUser if err := json.Unmarshal(data, &d); err != nil { return nil, err } return &d, nil default: - return &RawSystemNotificationFactoryPauseInfo{Discriminator: raw.Type, Raw: data}, nil + return &RawSystemNotificationWorkflowPauseInfo{Discriminator: raw.Type, Raw: data}, nil } } -func (r RawSystemNotificationFactoryPauseInfo) MarshalJSON() ([]byte, error) { +func (r RawSystemNotificationWorkflowPauseInfo) MarshalJSON() ([]byte, error) { if r.Raw != nil { return r.Raw, nil } return json.Marshal(struct { - Type SystemNotificationFactoryPauseInfoType `json:"type"` + Type SystemNotificationWorkflowPauseInfoType `json:"type"` }{ Type: r.Discriminator, }) } -func (r SystemNotificationFactoryPauseInfoCheckpoint) MarshalJSON() ([]byte, error) { - type alias SystemNotificationFactoryPauseInfoCheckpoint +func (r SystemNotificationWorkflowPauseInfoCheckpoint) MarshalJSON() ([]byte, error) { + type alias SystemNotificationWorkflowPauseInfoCheckpoint return json.Marshal(struct { - Type SystemNotificationFactoryPauseInfoType `json:"type"` + Type SystemNotificationWorkflowPauseInfoType `json:"type"` alias }{ Type: r.Type(), @@ -1710,10 +1783,10 @@ func (r SystemNotificationFactoryPauseInfoCheckpoint) MarshalJSON() ([]byte, err }) } -func (r SystemNotificationFactoryPauseInfoUser) MarshalJSON() ([]byte, error) { - type alias SystemNotificationFactoryPauseInfoUser +func (r SystemNotificationWorkflowPauseInfoUser) MarshalJSON() ([]byte, error) { + type alias SystemNotificationWorkflowPauseInfoUser return json.Marshal(struct { - Type SystemNotificationFactoryPauseInfoType `json:"type"` + Type SystemNotificationWorkflowPauseInfoType `json:"type"` alias }{ Type: r.Type(), @@ -1721,21 +1794,21 @@ func (r SystemNotificationFactoryPauseInfoUser) MarshalJSON() ([]byte, error) { }) } -func (r *SystemNotificationFactoryCompleted) UnmarshalJSON(data []byte) error { - type rawSystemNotificationFactoryCompleted struct { - Attempt int64 `json:"attempt"` - ConsumedNanoAiu int64 `json:"consumedNanoAiu"` - ConsumedSubagents int64 `json:"consumedSubagents"` - ElapsedMs int64 `json:"elapsedMs"` - FactoryName string `json:"factoryName"` - Failure any `json:"failure,omitempty"` - PauseInfo json.RawMessage `json:"pauseInfo,omitempty"` - ResultPreview *string `json:"resultPreview,omitempty"` - RetryGuidance *string `json:"retryGuidance,omitempty"` - RunID string `json:"runId"` - Status SystemNotificationFactoryCompletedStatus `json:"status"` +func (r *SystemNotificationWorkflowCompleted) UnmarshalJSON(data []byte) error { + type rawSystemNotificationWorkflowCompleted struct { + Attempt int64 `json:"attempt"` + ConsumedNanoAiu int64 `json:"consumedNanoAiu"` + ConsumedSubagents int64 `json:"consumedSubagents"` + ElapsedMs int64 `json:"elapsedMs"` + Failure any `json:"failure,omitempty"` + PauseInfo json.RawMessage `json:"pauseInfo,omitempty"` + ResultPreview *string `json:"resultPreview,omitempty"` + RetryGuidance *string `json:"retryGuidance,omitempty"` + RunID string `json:"runId"` + Status SystemNotificationWorkflowCompletedStatus `json:"status"` + WorkflowName string `json:"workflowName"` } - var raw rawSystemNotificationFactoryCompleted + var raw rawSystemNotificationWorkflowCompleted if err := json.Unmarshal(data, &raw); err != nil { return err } @@ -1743,10 +1816,9 @@ func (r *SystemNotificationFactoryCompleted) UnmarshalJSON(data []byte) error { r.ConsumedNanoAiu = raw.ConsumedNanoAiu r.ConsumedSubagents = raw.ConsumedSubagents r.ElapsedMs = raw.ElapsedMs - r.FactoryName = raw.FactoryName r.Failure = raw.Failure if raw.PauseInfo != nil { - value, err := unmarshalSystemNotificationFactoryPauseInfo(raw.PauseInfo) + value, err := unmarshalSystemNotificationWorkflowPauseInfo(raw.PauseInfo) if err != nil { return err } @@ -1756,66 +1828,12 @@ func (r *SystemNotificationFactoryCompleted) UnmarshalJSON(data []byte) error { r.RetryGuidance = raw.RetryGuidance r.RunID = raw.RunID r.Status = raw.Status + r.WorkflowName = raw.WorkflowName return nil } -func (r SystemNotificationFactoryCompleted) MarshalJSON() ([]byte, error) { - type alias SystemNotificationFactoryCompleted - return json.Marshal(struct { - Type SystemNotificationType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r SystemNotificationInstructionDiscovered) MarshalJSON() ([]byte, error) { - type alias SystemNotificationInstructionDiscovered - return json.Marshal(struct { - Type SystemNotificationType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r SystemNotificationNewInboxMessage) MarshalJSON() ([]byte, error) { - type alias SystemNotificationNewInboxMessage - return json.Marshal(struct { - Type SystemNotificationType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r SystemNotificationShellCompleted) MarshalJSON() ([]byte, error) { - type alias SystemNotificationShellCompleted - return json.Marshal(struct { - Type SystemNotificationType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r SystemNotificationShellDetachedCompleted) MarshalJSON() ([]byte, error) { - type alias SystemNotificationShellDetachedCompleted - return json.Marshal(struct { - Type SystemNotificationType `json:"type"` - alias - }{ - Type: r.Type(), - alias: alias(r), - }) -} - -func (r SystemNotificationUnclassified) MarshalJSON() ([]byte, error) { - type alias SystemNotificationUnclassified +func (r SystemNotificationWorkflowCompleted) MarshalJSON() ([]byte, error) { + type alias SystemNotificationWorkflowCompleted return json.Marshal(struct { Type SystemNotificationType `json:"type"` alias @@ -1884,12 +1902,6 @@ func unmarshalPermissionRequest(data []byte) (PermissionRequest, error) { return nil, err } return &d, nil - case PermissionRequestKindFactory: - var d PermissionRequestFactory - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil case PermissionRequestKindHook: var d PermissionRequestHook if err := json.Unmarshal(data, &d); err != nil { @@ -1926,6 +1938,12 @@ func unmarshalPermissionRequest(data []byte) (PermissionRequest, error) { return nil, err } return &d, nil + case PermissionRequestKindWorkflow: + var d PermissionRequestWorkflow + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case PermissionRequestKindWrite: var d PermissionRequestWrite if err := json.Unmarshal(data, &d); err != nil { @@ -1992,17 +2010,6 @@ func (r PermissionRequestExtensionPermissionAccess) MarshalJSON() ([]byte, error }) } -func (r PermissionRequestFactory) MarshalJSON() ([]byte, error) { - type alias PermissionRequestFactory - return json.Marshal(struct { - Kind PermissionRequestKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - func (r PermissionRequestHook) MarshalJSON() ([]byte, error) { type alias PermissionRequestHook return json.Marshal(struct { @@ -2069,6 +2076,17 @@ func (r PermissionRequestURL) MarshalJSON() ([]byte, error) { }) } +func (r PermissionRequestWorkflow) MarshalJSON() ([]byte, error) { + type alias PermissionRequestWorkflow + return json.Marshal(struct { + Kind PermissionRequestKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func (r PermissionRequestWrite) MarshalJSON() ([]byte, error) { type alias PermissionRequestWrite return json.Marshal(struct { @@ -2123,12 +2141,6 @@ func unmarshalPermissionPromptRequest(data []byte) (PermissionPromptRequest, err return nil, err } return &d, nil - case PermissionPromptRequestKindFactory: - var d PermissionPromptRequestFactory - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil case PermissionPromptRequestKindHook: var d PermissionPromptRequestHook if err := json.Unmarshal(data, &d); err != nil { @@ -2165,6 +2177,12 @@ func unmarshalPermissionPromptRequest(data []byte) (PermissionPromptRequest, err return nil, err } return &d, nil + case PermissionPromptRequestKindWorkflow: + var d PermissionPromptRequestWorkflow + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case PermissionPromptRequestKindWrite: var d PermissionPromptRequestWrite if err := json.Unmarshal(data, &d); err != nil { @@ -2242,17 +2260,6 @@ func (r PermissionPromptRequestExtensionPermissionAccess) MarshalJSON() ([]byte, }) } -func (r PermissionPromptRequestFactory) MarshalJSON() ([]byte, error) { - type alias PermissionPromptRequestFactory - return json.Marshal(struct { - Kind PermissionPromptRequestKind `json:"kind"` - alias - }{ - Kind: r.Kind(), - alias: alias(r), - }) -} - func (r PermissionPromptRequestHook) MarshalJSON() ([]byte, error) { type alias PermissionPromptRequestHook return json.Marshal(struct { @@ -2319,6 +2326,17 @@ func (r PermissionPromptRequestURL) MarshalJSON() ([]byte, error) { }) } +func (r PermissionPromptRequestWorkflow) MarshalJSON() ([]byte, error) { + type alias PermissionPromptRequestWorkflow + return json.Marshal(struct { + Kind PermissionPromptRequestKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func (r PermissionPromptRequestWrite) MarshalJSON() ([]byte, error) { type alias PermissionPromptRequestWrite return json.Marshal(struct { @@ -2399,6 +2417,12 @@ func unmarshalPermissionResult(data []byte) (PermissionResult, error) { return nil, err } return &d, nil + case PermissionResultKindApprovedReadOnlyForSession: + var d PermissionApprovedReadOnlyForSession + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case PermissionResultKindCancelled: var d PermissionCancelled if err := json.Unmarshal(data, &d); err != nil { @@ -2526,6 +2550,17 @@ func (r PermissionApprovedForSession) MarshalJSON() ([]byte, error) { }) } +func (r PermissionApprovedReadOnlyForSession) MarshalJSON() ([]byte, error) { + type alias PermissionApprovedReadOnlyForSession + return json.Marshal(struct { + Kind PermissionResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func (r PermissionCancelled) MarshalJSON() ([]byte, error) { type alias PermissionCancelled return json.Marshal(struct { @@ -2620,6 +2655,110 @@ func (r *PermissionCompletedData) UnmarshalJSON(data []byte) error { return nil } +func unmarshalHumanResponseRecordedResponse(data []byte) (HumanResponseRecordedResponse, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.ResponseKind { + case HumanResponseRecordedResponseResponseKindAskUser: + var d HumanResponseRecordedResponseAskUser + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case HumanResponseRecordedResponseResponseKindExitPlanMode: + var d HumanResponseRecordedResponseExitPlanMode + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case HumanResponseRecordedResponseResponseKindUserInput: + var d HumanResponseRecordedResponseUserInput + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawHumanResponseRecordedResponse{Discriminator: raw.ResponseKind, Raw: data}, nil + } +} + +func (r RawHumanResponseRecordedResponse) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + }{ + ResponseKind: r.Discriminator, + }) +} + +func (r HumanResponseRecordedResponseAskUser) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseAskUser + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r HumanResponseRecordedResponseExitPlanMode) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseExitPlanMode + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r HumanResponseRecordedResponseUserInput) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseUserInput + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r *HumanResponseRecordedData) UnmarshalJSON(data []byte) error { + type rawHumanResponseRecordedData struct { + Actor HumanResponseActor `json:"actor"` + RequestID string `json:"requestId"` + Response json.RawMessage `json:"response"` + ToolCallID *string `json:"toolCallId,omitempty"` + } + var raw rawHumanResponseRecordedData + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.Actor = raw.Actor + r.RequestID = raw.RequestID + if raw.Response != nil { + value, err := unmarshalHumanResponseRecordedResponse(raw.Response) + if err != nil { + return err + } + r.Response = value + } + r.ToolCallID = raw.ToolCallID + return nil +} + func (r *SessionExtensionsAttachmentsPushedData) UnmarshalJSON(data []byte) error { type rawSessionExtensionsAttachmentsPushedData struct { Attachments []json.RawMessage `json:"attachments"` diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 3d341cb51e..f429e809d2 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -94,30 +94,23 @@ const ( SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" SessionEventTypeExternalToolCompleted SessionEventType = "external_tool.completed" SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested" - // Experimental: SessionEventTypeFactoryRunSettled identifies an experimental event that may - // change or be removed. - SessionEventTypeFactoryRunSettled SessionEventType = "factory.run_settled" - // Experimental: SessionEventTypeFactoryRunStarted identifies an experimental event that may - // change or be removed. - SessionEventTypeFactoryRunStarted SessionEventType = "factory.run_started" - // Experimental: SessionEventTypeFactoryRunUpdated identifies an experimental event that may - // change or be removed. - SessionEventTypeFactoryRunUpdated SessionEventType = "factory.run_updated" - SessionEventTypeHookEnd SessionEventType = "hook.end" - SessionEventTypeHookProgress SessionEventType = "hook.progress" - SessionEventTypeHookStart SessionEventType = "hook.start" - SessionEventTypeMCPAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" - SessionEventTypeMCPHeadersRefreshCompleted SessionEventType = "mcp.headers_refresh_completed" - SessionEventTypeMCPHeadersRefreshRequired SessionEventType = "mcp.headers_refresh_required" - SessionEventTypeMCPOauthCompleted SessionEventType = "mcp.oauth_completed" - SessionEventTypeMCPOauthRequired SessionEventType = "mcp.oauth_required" - SessionEventTypeMCPPromptsListChanged SessionEventType = "mcp.prompts.list_changed" - SessionEventTypeMCPResourcesListChanged SessionEventType = "mcp.resources.list_changed" - SessionEventTypeMCPToolsListChanged SessionEventType = "mcp.tools.list_changed" - SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" - SessionEventTypeModelCallFinished SessionEventType = "model.call_finished" - SessionEventTypeModelCallStart SessionEventType = "model.call_start" - SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" + SessionEventTypeHookEnd SessionEventType = "hook.end" + SessionEventTypeHookProgress SessionEventType = "hook.progress" + SessionEventTypeHookStart SessionEventType = "hook.start" + SessionEventTypeHumanResponseRecorded SessionEventType = "human_response.recorded" + SessionEventTypeMCPAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" + SessionEventTypeMCPHeadersRefreshCompleted SessionEventType = "mcp.headers_refresh_completed" + SessionEventTypeMCPHeadersRefreshRequired SessionEventType = "mcp.headers_refresh_required" + SessionEventTypeMCPOauthCompleted SessionEventType = "mcp.oauth_completed" + SessionEventTypeMCPOauthRequired SessionEventType = "mcp.oauth_required" + SessionEventTypeMCPPromptsListChanged SessionEventType = "mcp.prompts.list_changed" + SessionEventTypeMCPResourcesListChanged SessionEventType = "mcp.resources.list_changed" + SessionEventTypeMCPToolsListChanged SessionEventType = "mcp.tools.list_changed" + SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" + SessionEventTypeModelCallFinalResult SessionEventType = "model.call_final_result" + SessionEventTypeModelCallFinished SessionEventType = "model.call_finished" + SessionEventTypeModelCallStart SessionEventType = "model.call_start" + SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" // Experimental: SessionEventTypePermissionAssentDetected identifies an experimental event // that may change or be removed. SessionEventTypePermissionAssentDetected SessionEventType = "permission.assentDetected" @@ -249,20 +242,22 @@ const ( SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" // Experimental: SessionEventTypeSkillInvokedRef identifies an experimental event that may // change or be removed. - SessionEventTypeSkillInvokedRef SessionEventType = "skill.invoked_ref" - SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" - SessionEventTypeSubagentConfigured SessionEventType = "subagent.configured" - SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" - SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" - SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" - SessionEventTypeSubagentStarted SessionEventType = "subagent.started" - SessionEventTypeSystemMessage SessionEventType = "system.message" - SessionEventTypeSystemNotification SessionEventType = "system.notification" - SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + SessionEventTypeSkillInvokedRef SessionEventType = "skill.invoked_ref" + SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" + SessionEventTypeSubagentConfigured SessionEventType = "subagent.configured" + SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" + SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" + SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" + SessionEventTypeSubagentStarted SessionEventType = "subagent.started" + SessionEventTypeSystemMessage SessionEventType = "system.message" + SessionEventTypeSystemNotification SessionEventType = "system.notification" + SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + // Deprecated: SessionEventTypeToolExecutionPartialResult identifies a deprecated event. SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress" SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" SessionEventTypeToolSearchActivated SessionEventType = "tool_search.activated" + SessionEventTypeToolShellOutput SessionEventType = "tool.shell_output" SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" // Experimental: SessionEventTypeUIEphemeralQuery identifies an experimental event that may // change or be removed. @@ -270,6 +265,15 @@ const ( SessionEventTypeUserInputCompleted SessionEventType = "user_input.completed" SessionEventTypeUserInputRequested SessionEventType = "user_input.requested" SessionEventTypeUserMessage SessionEventType = "user.message" + // Experimental: SessionEventTypeWorkflowRunSettled identifies an experimental event that + // may change or be removed. + SessionEventTypeWorkflowRunSettled SessionEventType = "workflow.run_settled" + // Experimental: SessionEventTypeWorkflowRunStarted identifies an experimental event that + // may change or be removed. + SessionEventTypeWorkflowRunStarted SessionEventType = "workflow.run_started" + // Experimental: SessionEventTypeWorkflowRunUpdated identifies an experimental event that + // may change or be removed. + SessionEventTypeWorkflowRunUpdated SessionEventType = "workflow.run_updated" ) // A detected loss of a previously cached prompt prefix @@ -325,6 +329,9 @@ type PromptCacheBreakData struct { // Telemetry-safe names of tools whose definition changed since the prior call // Internal: ToolsRedefined is part of the SDK's internal API surface and is not intended for external use. ToolsRedefined []string `json:"toolsRedefined,omitzero"` + // Changed definition parts of redefined tools, as `tool:part` entries; property-level parts only for telemetry-safe tools, whose other names are hashed + // Internal: ToolsRedefinedParts is part of the SDK's internal API surface and is not intended for external use. + ToolsRedefinedParts []string `json:"toolsRedefinedParts,omitzero"` // Raw names of tools redefined since the prior call, restricted because a tool name can be user-authored // Internal: ToolsRedefinedRaw is part of the SDK's internal API surface and is not intended for external use. ToolsRedefinedRaw []string `json:"toolsRedefinedRaw,omitzero"` @@ -502,6 +509,8 @@ type SessionAutoModeResolvedData struct { RouterLatencyMs *float64 `json:"routerLatencyMs,omitempty"` // The routing method the server applied, when Auto Intent ran RoutingMethod *string `json:"routingMethod,omitempty"` + // Short human-readable sentence from the routing service explaining why this model was chosen, for display alongside the model. Present only when the service supplied one: it is omitted for on-device selections, when the service did not provide an explanation, and when a replayed decision made no routing call. The text is display-only and drawn from a fixed catalogue; several distinct routing categories share identical wording, so it cannot be used to recover the category or keyed on programmatically. + SelectionReason *string `json:"selectionReason,omitempty"` // Whether a sticky model choice overrode the router result StickyOverride *bool `json:"stickyOverride,omitempty"` } @@ -639,9 +648,12 @@ func (*SessionContextClearedData) Type() SessionEventType { // Conversation compaction results including success status, metrics, and optional error details type SessionCompactionCompleteData struct { - // Authoritative active-factory reminder appended to the compacted context + // Legacy active-workflow reminder retained for replay compatibility // Internal: ActiveFactorySummary is part of the SDK's internal API surface and is not intended for external use. ActiveFactorySummary *string `json:"activeFactorySummary,omitempty"` + // Authoritative active-workflow reminder appended to the compacted context + // Internal: ActiveWorkflowSummary is part of the SDK's internal API surface and is not intended for external use. + ActiveWorkflowSummary *string `json:"activeWorkflowSummary,omitempty"` // Canonical model identifier used for model-specific behavior when replaying compaction BehaviorModelID *string `json:"behaviorModelId,omitempty"` // Checkpoint snapshot number created for recovery @@ -750,6 +762,21 @@ type SubagentSelectedData struct { func (*SubagentSelectedData) sessionEventData() {} func (*SubagentSelectedData) Type() SessionEventType { return SessionEventTypeSubagentSelected } +// Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. +// +// Deprecated: ToolExecutionPartialResultData is deprecated. +type ToolExecutionPartialResultData struct { + // Merged replacement snapshot from the running shell, not an append-only chunk + PartialOutput string `json:"partialOutput"` + // Tool call ID this partial result belongs to + ToolCallID string `json:"toolCallId"` +} + +func (*ToolExecutionPartialResultData) sessionEventData() {} +func (*ToolExecutionPartialResultData) Type() SessionEventType { + return SessionEventTypeToolExecutionPartialResult +} + // Durable record that a canvas instance is open, used to restore open canvases on cold session resume. Intentionally omits the transient url and availability. // Experimental: SessionCanvasRecordedData is part of an experimental API and may change or be removed. type SessionCanvasRecordedData struct { @@ -784,6 +811,23 @@ type SessionCanvasRemovedData struct { func (*SessionCanvasRemovedData) sessionEventData() {} func (*SessionCanvasRemovedData) Type() SessionEventType { return SessionEventTypeSessionCanvasRemoved } +// Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +type HumanResponseRecordedData struct { + // Controlled actor provenance established at response ingress. + Actor HumanResponseActor `json:"actor"` + // Request ID of the runtime-owned question or plan review. + RequestID string `json:"requestId"` + // Typed request and response payload. + Response HumanResponseRecordedResponse `json:"response"` + // Tool call ID that opened the request, when present. + ToolCallID *string `json:"toolCallId,omitempty"` +} + +func (*HumanResponseRecordedData) sessionEventData() {} +func (*HumanResponseRecordedData) Type() SessionEventType { + return SessionEventTypeHumanResponseRecorded +} + // Durable session usage checkpoint for reconstructing aggregate accounting on resume type SessionUsageCheckpointData struct { // Internal per-model prompt-cache state used to restore expiration tracking on resume @@ -821,6 +865,38 @@ func (*MCPHeadersRefreshRequiredData) Type() SessionEventType { return SessionEventTypeMCPHeadersRefreshRequired } +// 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: SessionManagedSettingsResolvedData is part of an experimental API and may change or be removed. +type SessionManagedSettingsResolvedData struct { + // Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + BypassPermissionsDisabled bool `json:"bypassPermissionsDisabled"` + // Whether a session-local permissions layer injected by the SDK host was present + ClientManaged *bool `json:"clientManaged,omitempty"` + // Whether an actual device MDM/plist/registry/file managed-settings layer was present + DeviceManaged bool `json:"deviceManaged"` + // 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. + FailClosed bool `json:"failClosed"` + // The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + ManagedKeys []string `json:"managedKeys"` + // Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. + PermissionsAllowIntersected *bool `json:"permissionsAllowIntersected,omitempty"` + // Whether the 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. + PolicyHelperManaged *bool `json:"policyHelperManaged,omitempty"` + // Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. + SandboxEnabledByUndeterminedPolicy *bool `json:"sandboxEnabledByUndeterminedPolicy,omitempty"` + // Whether the server (account/org) managed-settings layer was present + ServerManaged bool `json:"serverManaged"` + // The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + Settings any `json:"settings,omitempty"` + // 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. + Source ManagedSettingsResolvedSource `json:"source"` +} + +func (*SessionManagedSettingsResolvedData) sessionEventData() {} +func (*SessionManagedSettingsResolvedData) Type() SessionEventType { + return SessionEventTypeSessionManagedSettingsResolved +} + // Elicitation request completion with the user's response type ElicitationCompletedData struct { // The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) @@ -880,49 +956,17 @@ func (*PendingMessagesModifiedData) Type() SessionEventType { return SessionEventTypePendingMessagesModified } -// 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. -// Experimental: SessionManagedSettingsResolvedData is part of an experimental API and may change or be removed. -type SessionManagedSettingsResolvedData struct { - // Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. - BypassPermissionsDisabled bool `json:"bypassPermissionsDisabled"` - // Whether a session-local permissions layer injected by the SDK host was present - ClientManaged *bool `json:"clientManaged,omitempty"` - // Whether an actual device MDM/plist/registry/file managed-settings layer was present - DeviceManaged bool `json:"deviceManaged"` - // 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. - FailClosed bool `json:"failClosed"` - // The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. - ManagedKeys []string `json:"managedKeys"` - // Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. - PermissionsAllowIntersected *bool `json:"permissionsAllowIntersected,omitempty"` - // Whether the 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. - PolicyHelperManaged *bool `json:"policyHelperManaged,omitempty"` - // Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. - SandboxEnabledByUndeterminedPolicy *bool `json:"sandboxEnabledByUndeterminedPolicy,omitempty"` - // Whether the server (account/org) managed-settings layer was present - ServerManaged bool `json:"serverManaged"` - // The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. - Settings any `json:"settings,omitempty"` - // 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. - Source ManagedSettingsResolvedSource `json:"source"` -} - -func (*SessionManagedSettingsResolvedData) sessionEventData() {} -func (*SessionManagedSettingsResolvedData) Type() SessionEventType { - return SessionEventTypeSessionManagedSettingsResolved -} - -// Ephemeral invalidation signal for a changed factory run. -// Experimental: FactoryRunUpdatedData is part of an experimental API and may change or be removed. -type FactoryRunUpdatedData struct { +// Ephemeral invalidation signal for a changed workflow run. +// Experimental: WorkflowRunUpdatedData is part of an experimental API and may change or be removed. +type WorkflowRunUpdatedData struct { // Monotonic revision now available for the run. Revision int64 `json:"revision"` - // Factory run identifier. + // Workflow run identifier. RunID string `json:"runId"` } -func (*FactoryRunUpdatedData) sessionEventData() {} -func (*FactoryRunUpdatedData) Type() SessionEventType { return SessionEventTypeFactoryRunUpdated } +func (*WorkflowRunUpdatedData) sessionEventData() {} +func (*WorkflowRunUpdatedData) Type() SessionEventType { return SessionEventTypeWorkflowRunUpdated } // Ephemeral progress update from a running hook process type HookProgressData struct { @@ -935,39 +979,39 @@ type HookProgressData struct { func (*HookProgressData) sessionEventData() {} func (*HookProgressData) Type() SessionEventType { return SessionEventTypeHookProgress } -// Ephemeral signal that a factory run attempt began executing. -// Experimental: FactoryRunStartedData is part of an experimental API and may change or be removed. -type FactoryRunStartedData struct { +// Ephemeral signal that a workflow run attempt began executing. +// Experimental: WorkflowRunStartedData is part of an experimental API and may change or be removed. +type WorkflowRunStartedData struct { // Attempt number this start committed; a resumed run increments it. Attempt int64 `json:"attempt"` - // Name of the factory this run executes. Low cardinality by construction. - FactoryName string `json:"factoryName"` - // Identifier of the factory run that started. + // Identifier of the workflow run that started. RunID string `json:"runId"` + // Name of the workflow this run executes. Low cardinality by construction. + WorkflowName string `json:"workflowName"` } -func (*FactoryRunStartedData) sessionEventData() {} -func (*FactoryRunStartedData) Type() SessionEventType { return SessionEventTypeFactoryRunStarted } +func (*WorkflowRunStartedData) sessionEventData() {} +func (*WorkflowRunStartedData) Type() SessionEventType { return SessionEventTypeWorkflowRunStarted } -// Ephemeral signal that a factory run reached a terminal status. -// Experimental: FactoryRunSettledData is part of an experimental API and may change or be removed. -type FactoryRunSettledData struct { +// Ephemeral signal that a workflow run reached a terminal status. +// Experimental: WorkflowRunSettledData is part of an experimental API and may change or be removed. +type WorkflowRunSettledData struct { // AI credits this run consumed, in nano-AIU. ConsumedNanoAiu int64 `json:"consumedNanoAiu"` // Subagents this run consumed against its limits. ConsumedSubagents int64 `json:"consumedSubagents"` // Active milliseconds accumulated across every attempt of this run. ElapsedMs int64 `json:"elapsedMs"` - // Typed failure class recorded on the run, when it failed with one (e.g. `factory_limit_reached`). + // Typed failure class recorded on the run, when it failed with one (e.g. `workflow_limit_reached`). FailureType *string `json:"failureType,omitempty"` - // Identifier of the factory run that settled. + // Identifier of the workflow run that settled. RunID string `json:"runId"` // Terminal status the run committed. - Status FactoryRunSettledStatus `json:"status"` + Status WorkflowRunSettledStatus `json:"status"` } -func (*FactoryRunSettledData) sessionEventData() {} -func (*FactoryRunSettledData) Type() SessionEventType { return SessionEventTypeFactoryRunSettled } +func (*WorkflowRunSettledData) sessionEventData() {} +func (*WorkflowRunSettledData) Type() SessionEventType { return SessionEventTypeWorkflowRunSettled } // Error details for timeline display including message and optional diagnostic information type SessionErrorData struct { @@ -1062,6 +1106,9 @@ type AssistantFusionPhaseCompletedData struct { // Projection action for the exact internal message. // Internal: ProjectionMode is part of the SDK's internal API surface and is not intended for external use. ProjectionMode *FusionProjectionMode `json:"projectionMode,omitempty"` + // Explicit reasoning effort selected for this phase, if supplied. + // Experimental: ReasoningEffort is part of an experimental API and may change or be removed. + ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Semantic role assigned to the completed phase. Role string `json:"role"` // Terminal request held outside canonical state until selected by the final commit. @@ -1171,6 +1218,9 @@ type AssistantFusionPhaseFailedData struct { PhaseKind FusionPhaseKind `json:"phaseKind"` // Stable machine-readable reason for the phase failure. Reason string `json:"reason"` + // Explicit reasoning effort selected for this phase, if supplied. + // Experimental: ReasoningEffort is part of an experimental API and may change or be removed. + ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Semantic role assigned to the failed phase. Role string `json:"role"` // Durable outcome status of the phase. @@ -1189,6 +1239,9 @@ func (*AssistantFusionPhaseFailedData) Type() SessionEventType { type SessionFusionResolvedData struct { // Version of the validated HydraFusion event contract. ContractVersion int64 `json:"contractVersion"` + // 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. + // Experimental: Critics is part of an experimental API and may change or be removed. + Critics []FusionCritic `json:"critics,omitzero"` // Concrete model used when the planned primary model cannot execute. FallbackModel string `json:"fallbackModel"` // Router recommendation controlling reuse or rerouting on later turns. @@ -1200,6 +1253,9 @@ type SessionFusionResolvedData struct { // 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. // Experimental: Hint is part of an experimental API and may change or be removed. Hint *string `json:"hint,omitempty"` + // Concrete model selected for Cascade escalation-gate calls, when required. + // Experimental: JudgeModel is part of an experimental API and may change or be removed. + JudgeModel *string `json:"judgeModel,omitempty"` // Version of the executable model universe used for selection. ModelUniverseVersion *string `json:"modelUniverseVersion,omitempty"` // Validated orchestration pattern selected for the turn. @@ -1215,6 +1271,9 @@ type SessionFusionResolvedData struct { PolicyVersion *string `json:"policyVersion,omitempty"` // Concrete model selected for the primary solver phase. PrimaryModel string `json:"primaryModel"` + // Concrete model selected for Cascade repair, when required. + // Experimental: RepairModel is part of an experimental API and may change or be removed. + RepairModel *string `json:"repairModel,omitempty"` // Router implementation that supplied the plan. RouteSource *string `json:"routeSource,omitempty"` // Elapsed time in milliseconds required to resolve and validate the route. @@ -1227,7 +1286,7 @@ type SessionFusionResolvedData struct { RuleName *string `json:"ruleName,omitempty"` // Validated capability scores used to select the route. Scores *FusionScores `json:"scores,omitempty"` - // Concrete model selected for the review or judge phase, when required. + // Concrete model selected for Critique review, or the legacy Cascade judge/repair model when role-specific fields are absent. SecondaryModel *string `json:"secondaryModel"` // Synthetic HydraFusion model selected for the session. SyntheticModel string `json:"syntheticModel"` @@ -1255,6 +1314,9 @@ type AssistantFusionPhaseStartedData struct { PhaseID string `json:"phaseId"` // Kind of phase being executed. PhaseKind FusionPhaseKind `json:"phaseKind"` + // Explicit reasoning effort selected for this phase, if supplied. + // Experimental: ReasoningEffort is part of an experimental API and may change or be removed. + ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Semantic role assigned to the phase. Role string `json:"role"` } @@ -1328,6 +1390,8 @@ type ModelCallFailureData struct { APIEndpoint *AssistantUsageAPIEndpoint `json:"apiEndpoint,omitempty"` // For HTTP 400 failures only: whether the response carried a structured CAPI error envelope (structured_error, a deterministic validation failure) or no error body (bodyless, the transient gateway/proxy signature). Absent for non-400 failures. BadRequestKind *ModelCallFailureBadRequestKind `json:"badRequestKind,omitempty"` + // Where the bring-your-own-key model for the failed call runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + ByokKind *string `json:"byokKind,omitempty"` // Duration of the failed API call in milliseconds DurationMs *int64 `json:"durationMs,omitempty"` // For HTTP 400 failures only: the `code` from the CAPI error envelope (e.g. 'model_max_prompt_tokens_exceeded') identifying which deterministic validation failure occurred. Raw server-controlled string, emitted only through restricted telemetry. Absent for bodyless or non-400 failures. @@ -1355,6 +1419,10 @@ type ModelCallFailureData struct { MaxPromptTokens *int64 `json:"maxPromptTokens,omitempty"` // Model identifier used for the failed API call Model *string `json:"model,omitempty"` + // Fixed-set provider family serving the bring-your-own-key model for the failed call (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + ModelProvider *string `json:"modelProvider,omitempty"` + // Parent task tool call ID when this failed model call belongs to a sub-agent + ParentToolCallID *string `json:"parentToolCallId,omitempty"` // GitHub request tracing ID (x-github-request-id header) for server-side log correlation ProviderCallID *string `json:"providerCallId,omitempty"` // Per-quota usage snapshots parsed from the failed response's quota headers, keyed by quota identifier. Present when the error response carried quota headers (e.g. a 402 once the additional spend limit is reached) so the UI can refresh the quota display on failure. @@ -1398,35 +1466,7 @@ type ModelCallFinishedData struct { func (*ModelCallFinishedData) sessionEventData() {} func (*ModelCallFinishedData) Type() SessionEventType { return SessionEventTypeModelCallFinished } -// Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify. -// Experimental: PermissionContextualAuthorizationData is part of an experimental API and may change or be removed. -type PermissionContextualAuthorizationData struct { - // Whether the contextual human span granted or denied authority. - // Experimental: Polarity is part of an experimental API and may change or be removed. - Polarity PermissionMessageAuthorizationPolarity `json:"polarity"` - // Deterministic identity of the contextual message grant. - // Experimental: RecordID is part of an experimental API and may change or be removed. - RecordID string `json:"recordId"` - // Original blocked permission request selected by deterministic event ordering, never by the extraction model. - // Experimental: RequestID is part of an experimental API and may change or be removed. - RequestID string `json:"requestId"` - // End byte offset of the contextual decision span within the turn. - // Experimental: SpanEnd is part of an experimental API and may change or be removed. - SpanEnd int64 `json:"spanEnd"` - // Start byte offset of the contextual decision span within the turn. - // Experimental: SpanStart is part of an experimental API and may change or be removed. - SpanStart int64 `json:"spanStart"` - // Human turn containing the contextual decision. - // Experimental: TurnIndex is part of an experimental API and may change or be removed. - TurnIndex int64 `json:"turnIndex"` -} - -func (*PermissionContextualAuthorizationData) sessionEventData() {} -func (*PermissionContextualAuthorizationData) Type() SessionEventType { - return SessionEventTypePermissionContextualAuthorization -} - -// Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. +// Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it. // Experimental: PermissionMessageAuthorizationData is part of an experimental API and may change or be removed. type PermissionMessageAuthorizationData struct { // The kind of effect authorized, as an action-class identifier. @@ -1463,6 +1503,101 @@ func (*PermissionMessageAuthorizationData) Type() SessionEventType { return SessionEventTypePermissionMessageAuthorization } +// Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it. +// Experimental: PermissionContextualAuthorizationData is part of an experimental API and may change or be removed. +type PermissionContextualAuthorizationData struct { + // Whether the contextual human span granted or denied authority. + // Experimental: Polarity is part of an experimental API and may change or be removed. + Polarity PermissionMessageAuthorizationPolarity `json:"polarity"` + // Deterministic identity of the contextual message grant. + // Experimental: RecordID is part of an experimental API and may change or be removed. + RecordID string `json:"recordId"` + // Original blocked permission request selected by deterministic event ordering, never by the extraction model. + // Experimental: RequestID is part of an experimental API and may change or be removed. + RequestID string `json:"requestId"` + // End byte offset of the contextual decision span within the turn. + // Experimental: SpanEnd is part of an experimental API and may change or be removed. + SpanEnd int64 `json:"spanEnd"` + // Start byte offset of the contextual decision span within the turn. + // Experimental: SpanStart is part of an experimental API and may change or be removed. + SpanStart int64 `json:"spanStart"` + // Human turn containing the contextual decision. + // Experimental: TurnIndex is part of an experimental API and may change or be removed. + TurnIndex int64 `json:"turnIndex"` +} + +func (*PermissionContextualAuthorizationData) sessionEventData() {} +func (*PermissionContextualAuthorizationData) Type() SessionEventType { + return SessionEventTypePermissionContextualAuthorization +} + +// Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow. +// Experimental: PermissionAssentDetectedData is part of an experimental API and may change or be removed. +type PermissionAssentDetectedData struct { + // 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. + // Experimental: RequestID is part of an experimental API and may change or be removed. + RequestID string `json:"requestId"` + // Human turn whose text triggered the deterministic assent recognizer. + // Experimental: TurnIndex is part of an experimental API and may change or be removed. + TurnIndex int64 `json:"turnIndex"` +} + +func (*PermissionAssentDetectedData) sessionEventData() {} +func (*PermissionAssentDetectedData) Type() SessionEventType { + return SessionEventTypePermissionAssentDetected +} + +// Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions. +// Experimental: PermissionMessageAuthorizationDegradedData is part of an experimental API and may change or be removed. +type PermissionMessageAuthorizationDegradedData struct { + // The human turn that could not be represented safely. + // Experimental: TurnIndex is part of an experimental API and may change or be removed. + TurnIndex int64 `json:"turnIndex"` +} + +func (*PermissionMessageAuthorizationDegradedData) sessionEventData() {} +func (*PermissionMessageAuthorizationDegradedData) Type() SessionEventType { + return SessionEventTypePermissionMessageAuthorizationDegraded +} + +// Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it. +// Experimental: PermissionMessageAuthorizationReadData is part of an experimental API and may change or be removed. +type PermissionMessageAuthorizationReadData struct { + // 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. + // Experimental: ActivatesExtraction is part of an experimental API and may change or be removed. + ActivatesExtraction *bool `json:"activatesExtraction,omitempty"` + // The human turn that was read by the proposer. + // Experimental: TurnIndex is part of an experimental API and may change or be removed. + TurnIndex int64 `json:"turnIndex"` +} + +func (*PermissionMessageAuthorizationReadData) sessionEventData() {} +func (*PermissionMessageAuthorizationReadData) Type() SessionEventType { + return SessionEventTypePermissionMessageAuthorizationRead +} + +// Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions. +// Experimental: PermissionCarriedForwardData is part of an experimental API and may change or be removed. +type PermissionCarriedForwardData struct { + // Always `authorization_carry_forward`. Stated explicitly so a consumer reading this event cannot mistake it for a human, host-policy, or assisted-approval decision. + // Experimental: DecisionSource is part of an experimental API and may change or be removed. + DecisionSource PermissionDecisionSource `json:"decisionSource"` + // Identity of the prior authorization record that contained the proposal. + // Experimental: RecordID is part of an experimental API and may change or be removed. + RecordID string `json:"recordId"` + // Authorization edge minted for this admission. Not a prompt id: no prompt was raised, so no client should expect a request with this id. + // Experimental: RequestID is part of an experimental API and may change or be removed. + RequestID string `json:"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. + // Experimental: ToolCallID is part of an experimental API and may change or be removed. + ToolCallID string `json:"toolCallId"` +} + +func (*PermissionCarriedForwardData) sessionEventData() {} +func (*PermissionCarriedForwardData) Type() SessionEventType { + return SessionEventTypePermissionCarriedForward +} + // Hook invocation completion details including output, success status, and error information type HookEndData struct { // Error details when the hook failed @@ -1564,6 +1699,19 @@ type SkillInvokedRefData struct { func (*SkillInvokedRefData) sessionEventData() {} func (*SkillInvokedRefData) Type() SessionEventType { return SessionEventTypeSkillInvokedRef } +// Internal telemetry result for one logical model operation after all orchestrator-owned retries settle +type ModelCallFinalResultData struct { + // Whether the final attempt used a bring-your-own-key provider + IsByok *bool `json:"isByok,omitempty"` + // Model identifier used by the final attempt + Model string `json:"model"` + // Bounded result of the final attempt + Result ModelCallFinalResult `json:"result"` +} + +func (*ModelCallFinalResultData) sessionEventData() {} +func (*ModelCallFinalResultData) Type() SessionEventType { return SessionEventTypeModelCallFinalResult } + // LLM API call usage metrics including tokens, costs, quotas, and billing information type AssistantUsageData struct { // Number of accepted speculative prediction tokens @@ -1575,6 +1723,8 @@ type AssistantUsageData struct { // Number of tools available to the model for this call // Internal: AvailableToolCount is part of the SDK's internal API surface and is not intended for external use. AvailableToolCount *int64 `json:"availableToolCount,omitempty"` + // Where the bring-your-own-key model runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + ByokKind *string `json:"byokKind,omitempty"` // Whether the provider reported prompt-cache usage details for this call // Internal: CacheDetailsReported is part of the SDK's internal API surface and is not intended for external use. CacheDetailsReported *bool `json:"cacheDetailsReported,omitempty"` @@ -1622,6 +1772,8 @@ type AssistantUsageData struct { MaxPromptTokens *int64 `json:"maxPromptTokens,omitempty"` // Model identifier used for this API call Model string `json:"model"` + // Fixed-set provider family serving the bring-your-own-key model (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + ModelProvider *string `json:"modelProvider,omitempty"` // Number of tool calls returned by the model // Internal: NumToolCalls is part of the SDK's internal API surface and is not intended for external use. NumToolCalls *int64 `json:"numToolCalls,omitempty"` @@ -1685,6 +1837,21 @@ func (*AssistantServerToolProgressData) Type() SessionEventType { return SessionEventTypeAssistantServerToolProgress } +// Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +type ToolShellOutputData struct { + // Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + Sequence int64 `json:"sequence"` + // Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. + Stream *ToolShellOutputStream `json:"stream,omitempty"` + // New output to append, without synthetic shell-result markers or stream-switch separators + Text string `json:"text"` + // Tool call ID that owns this shell output + ToolCallID string `json:"toolCallId"` +} + +func (*ToolShellOutputData) sessionEventData() {} +func (*ToolShellOutputData) Type() SessionEventType { return SessionEventTypeToolShellOutput } + // Live-only Auto preference recommendation from Copilot API after a successful Auto model call. // Experimental: SessionAutoTierRecommendationData is part of an experimental API and may change or be removed. type SessionAutoTierRecommendationData struct { @@ -1799,6 +1966,8 @@ type ModelCallStartData struct { Fusion *FusionAttribution `json:"fusion,omitempty"` // Model identifier used for this API call, when known Model *string `json:"model,omitempty"` + // Parent task tool call ID when this model call belongs to a sub-agent + ParentToolCallID *string `json:"parentToolCallId,omitempty"` // Previous response or interaction identifier included in the model request, when present // Internal: PreviousResponseID is part of the SDK's internal API surface and is not intended for external use. PreviousResponseID *string `json:"previousResponseId,omitempty"` @@ -1831,6 +2000,8 @@ type SessionModelChangeData struct { PreviousVerbosity *Verbosity `json:"previousVerbosity,omitempty"` // Reasoning effort level after the model change, if applicable ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode after the model change, if applicable ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Origin of the effective model change, when known. @@ -2086,8 +2257,12 @@ func (*SessionMCPServerRemovedData) Type() SessionEventType { // Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. type SessionMCPServerStatusChangedData struct { + // Runtime configuration provenance for a connected or failed server, or unknown when unavailable. Additional string values may be introduced. + ConfigSource *string `json:"configSource,omitempty"` // Error message if the server entered a failed state Error *string `json:"error,omitempty"` + // Runtime-produced classification for the final failed connection; unclassified means no classification was supplied. Additional string values may be introduced. + ErrorClassification *string `json:"errorClassification,omitempty"` // Name of the MCP server whose status changed ServerName string `json:"serverName"` // Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured @@ -2305,28 +2480,6 @@ type CommandQueuedData struct { func (*CommandQueuedData) sessionEventData() {} func (*CommandQueuedData) Type() SessionEventType { return SessionEventTypeCommandQueued } -// Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. -// Experimental: PermissionCarriedForwardData is part of an experimental API and may change or be removed. -type PermissionCarriedForwardData struct { - // Always `authorization_carry_forward`. Stated explicitly so a consumer reading this event cannot mistake it for a human, host-policy, or assisted-approval decision. - // Experimental: DecisionSource is part of an experimental API and may change or be removed. - DecisionSource PermissionDecisionSource `json:"decisionSource"` - // Identity of the prior authorization record that contained the proposal. - // Experimental: RecordID is part of an experimental API and may change or be removed. - RecordID string `json:"recordId"` - // Authorization edge minted for this admission. Not a prompt id: no prompt was raised, so no client should expect a request with this id. - // Experimental: RequestID is part of an experimental API and may change or be removed. - RequestID string `json:"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. - // Experimental: ToolCallID is part of an experimental API and may change or be removed. - ToolCallID string `json:"toolCallId"` -} - -func (*PermissionCarriedForwardData) sessionEventData() {} -func (*PermissionCarriedForwardData) Type() SessionEventType { - return SessionEventTypePermissionCarriedForward -} - // Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. type SessionModeNoticeDeliveredData struct { // Model-visible transition notice persisted for a mid-turn delivery @@ -2340,51 +2493,6 @@ func (*SessionModeNoticeDeliveredData) Type() SessionEventType { return SessionEventTypeSessionModeNoticeDelivered } -// Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation. -// Experimental: PermissionAssentDetectedData is part of an experimental API and may change or be removed. -type PermissionAssentDetectedData struct { - // 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. - // Experimental: RequestID is part of an experimental API and may change or be removed. - RequestID string `json:"requestId"` - // Human turn whose text triggered the deterministic assent recognizer. - // Experimental: TurnIndex is part of an experimental API and may change or be removed. - TurnIndex int64 `json:"turnIndex"` -} - -func (*PermissionAssentDetectedData) sessionEventData() {} -func (*PermissionAssentDetectedData) Type() SessionEventType { - return SessionEventTypePermissionAssentDetected -} - -// Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. -// Experimental: PermissionMessageAuthorizationDegradedData is part of an experimental API and may change or be removed. -type PermissionMessageAuthorizationDegradedData struct { - // The human turn that could not be represented safely. - // Experimental: TurnIndex is part of an experimental API and may change or be removed. - TurnIndex int64 `json:"turnIndex"` -} - -func (*PermissionMessageAuthorizationDegradedData) sessionEventData() {} -func (*PermissionMessageAuthorizationDegradedData) Type() SessionEventType { - return SessionEventTypePermissionMessageAuthorizationDegraded -} - -// Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction. -// Experimental: PermissionMessageAuthorizationReadData is part of an experimental API and may change or be removed. -type PermissionMessageAuthorizationReadData struct { - // 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. - // Experimental: ActivatesExtraction is part of an experimental API and may change or be removed. - ActivatesExtraction *bool `json:"activatesExtraction,omitempty"` - // The human turn that was read by the proposer. - // Experimental: TurnIndex is part of an experimental API and may change or be removed. - TurnIndex int64 `json:"turnIndex"` -} - -func (*PermissionMessageAuthorizationReadData) sessionEventData() {} -func (*PermissionMessageAuthorizationReadData) Type() SessionEventType { - return SessionEventTypePermissionMessageAuthorizationRead -} - // Registered command dispatch request routed to the owning client type CommandExecuteData struct { // Raw argument string after the command name @@ -2569,6 +2677,8 @@ type SessionStartData struct { Producer string `json:"producer"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub @@ -2647,6 +2757,8 @@ type SessionResumeData struct { EventsFileSizeBytes *int64 `json:"eventsFileSizeBytes,omitempty"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub @@ -2822,19 +2934,6 @@ func (*AssistantStreamingDeltaData) Type() SessionEventType { return SessionEventTypeAssistantStreamingDelta } -// Streaming tool execution output for incremental result display -type ToolExecutionPartialResultData struct { - // Incremental output chunk from the running tool - PartialOutput string `json:"partialOutput"` - // Tool call ID this partial result belongs to - ToolCallID string `json:"toolCallId"` -} - -func (*ToolExecutionPartialResultData) sessionEventData() {} -func (*ToolExecutionPartialResultData) Type() SessionEventType { - return SessionEventTypeToolExecutionPartialResult -} - // Streaming tool-call input delta for incremental tool-call updates type AssistantToolCallDeltaData struct { // Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. @@ -2938,7 +3037,7 @@ type SubagentStartedData struct { AgentType *string `json:"agentType,omitempty"` // Whether the sub-agent runs synchronously or in the background. ExecutionMode *string `json:"executionMode,omitempty"` - // Root id of the factory run that spawned this sub-agent, when it was spawned by one. + // Legacy root id of the workflow run that spawned this sub-agent. New consumers should use workflowRunId. FactoryRunID *string `json:"factoryRunId,omitempty"` // Model the sub-agent will run with, when known at start. Model *string `json:"model,omitempty"` @@ -2948,10 +3047,12 @@ type SubagentStartedData struct { ParentID *string `json:"parentId,omitempty"` // Whether this sub-agent can be resumed. Currently always false. Resumable *bool `json:"resumable,omitempty"` - // Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and factory agents); absent for sub-agents created through other runtime paths. + // Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and workflow agents); absent for sub-agents created through other runtime paths. TaskModelSource *SubagentTaskModelSource `json:"taskModelSource,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` + // Root id of the workflow run that spawned this sub-agent, when it was spawned by one. + WorkflowRunID *string `json:"workflowRunId,omitempty"` } func (*SubagentStartedData) sessionEventData() {} @@ -3025,6 +3126,9 @@ func (*SessionModelDeselectedData) Type() SessionEventType { type ToolExecutionCompleteData struct { // Error details when the tool execution failed Error *ToolExecutionCompleteError `json:"error,omitempty"` + // Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + // Experimental: FileEdits is part of an experimental API and may change or be removed. + FileEdits []ToolExecutionCompleteFileEdit `json:"fileEdits,omitzero"` // Experimental HydraFusion attribution for this tool completion. // Experimental: Fusion is part of an experimental API and may change or be removed. Fusion *FusionAttribution `json:"fusion,omitempty"` @@ -3070,6 +3174,9 @@ func (*ToolExecutionCompleteData) Type() SessionEventType { type ToolExecutionProgressData struct { // Human-readable progress status message (e.g., from an MCP server) ProgressMessage string `json:"progressMessage"` + // Client-only structured progress metadata. Not model-facing tool output. + // Experimental: StructuredContent is part of an experimental API and may change or be removed. + StructuredContent any `json:"structuredContent,omitempty"` // Tool call ID this progress notification belongs to ToolCallID string `json:"toolCallId"` } @@ -3160,6 +3267,8 @@ func (*AbortData) Type() SessionEventType { return SessionEventTypeAbort } type AssistantTurnEndData struct { // Model identifier used for this turn, when known Model *string `json:"model,omitempty"` + // Parent task tool call ID when this turn belongs to a sub-agent + ParentToolCallID *string `json:"parentToolCallId,omitempty"` // Identifier of the turn that has ended, matching the corresponding assistant.turn_start event TurnID string `json:"turnId"` } @@ -3173,6 +3282,8 @@ type AssistantTurnStartData struct { InteractionID *string `json:"interactionId,omitempty"` // Model identifier used for this turn, when known Model *string `json:"model,omitempty"` + // Parent task tool call ID when this turn belongs to a sub-agent + ParentToolCallID *string `json:"parentToolCallId,omitempty"` // Identifier for this turn within the agentic loop, typically a stringified turn number TurnID string `json:"turnId"` } @@ -3283,6 +3394,8 @@ func (*SessionWorkspaceFileChangedData) Type() SessionEventType { type AssistantMessageReasoningBlocks struct { // Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. Blocks []any `json:"blocks,omitzero"` + // Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + OrderedBlocks []any `json:"orderedBlocks,omitzero"` // Model provider that produced these reasoning blocks. Provider string `json:"provider"` } @@ -3678,14 +3791,6 @@ type ExtensionsLoadedExtension struct { Status ExtensionsLoadedExtensionStatus `json:"status"` } -// A declared phase shown in a factory permission prompt. -type FactoryPermissionPhase struct { - // Optional phase detail - Detail *string `json:"detail,omitempty"` - // Phase title - Title string `json:"title"` -} - // Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. // Experimental: FusionAttribution is part of an experimental API and may change or be removed. type FusionAttribution struct { @@ -3715,6 +3820,19 @@ type FusionAttribution struct { SyntheticModel string `json:"syntheticModel"` } +// Experimental: FusionCritic is part of an experimental API and may change or be removed. +type FusionCritic struct { + // Concrete model selected for this critic. + // Experimental: Model is part of an experimental API and may change or be removed. + Model string `json:"model"` + // Unique execution phase identifier for this critic. + // Experimental: PhaseID is part of an experimental API and may change or be removed. + PhaseID string `json:"phaseId"` + // Explicit reasoning effort selected for this critic, if supplied. + // Experimental: ReasoningEffort is part of an experimental API and may change or be removed. + ReasoningEffort *string `json:"reasoningEffort,omitempty"` +} + // Durable server recommendation for subsequent HydraFusion turns. // Experimental: FusionFollowUpRecommendation is part of an experimental API and may change or be removed. type FusionFollowUpRecommendation struct { @@ -3818,6 +3936,78 @@ type HookEndError struct { Stack *string `json:"stack,omitempty"` } +// Exact runtime-owned question or reviewed plan paired with the typed response that settled it. +type HumanResponseRecordedResponse interface { + humanResponseRecordedResponse() + ResponseKind() HumanResponseRecordedResponseResponseKind +} + +type RawHumanResponseRecordedResponse struct { + Discriminator HumanResponseRecordedResponseResponseKind + Raw json.RawMessage +} + +func (RawHumanResponseRecordedResponse) humanResponseRecordedResponse() {} +func (r RawHumanResponseRecordedResponse) ResponseKind() HumanResponseRecordedResponseResponseKind { + return r.Discriminator +} + +type HumanResponseRecordedResponseAskUser struct { + // Exact answer content accepted from the user. + Content map[string]any `json:"content"` + // Exact question displayed to the user. + Message string `json:"message"` + // Exact response schema displayed to the user. + RequestedSchema ElicitationRequestedSchema `json:"requestedSchema"` +} + +func (HumanResponseRecordedResponseAskUser) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseAskUser) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindAskUser +} + +type HumanResponseRecordedResponseExitPlanMode struct { + // Actions offered by the plan review UI. + Actions []ExitPlanModeAction `json:"actions"` + // Whether the user approved the reviewed plan. + Approved bool `json:"approved"` + // Whether the selected response requested edit auto-approval. + AutoApproveEdits *bool `json:"autoApproveEdits,omitempty"` + // Exact feedback submitted with the plan decision, when present. + Feedback *string `json:"feedback,omitempty"` + // Exact full plan content available from the review UI. + PlanContent string `json:"planContent"` + // Action the plan review UI recommended. + RecommendedAction ExitPlanModeAction `json:"recommendedAction"` + // Action selected by the user, when applicable. + SelectedAction *ExitPlanModeAction `json:"selectedAction,omitempty"` + // Exact plan summary displayed to the user. + Summary string `json:"summary"` +} + +func (HumanResponseRecordedResponseExitPlanMode) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseExitPlanMode) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindExitPlanMode +} + +type HumanResponseRecordedResponseUserInput struct { + // Whether the displayed request allowed a free-form answer. + AllowFreeform *bool `json:"allowFreeform,omitempty"` + // Exact selected or free-form answer submitted by the user. + Answer string `json:"answer"` + // Exact choices displayed to the user, when the request offered choices. + Choices []string `json:"choices,omitzero"` + // Exact question displayed to the user. + Question string `json:"question"` + // Whether the answer was typed as free-form text rather than selected from the displayed choices. + WasFreeform bool `json:"wasFreeform"` +} + +func (HumanResponseRecordedResponseUserInput) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseUserInput) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindUserInput +} + // Set when the underlying tools/call threw an error before returning a CallToolResult type MCPAppToolCallCompleteError struct { // Human-readable error message @@ -3976,6 +4166,9 @@ type PermissionPromptRequestCommands struct { RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // True when the escalation is a permissive retry that keeps the sandbox and network policy attached while recording file and process accesses instead of blocking them. RequestSandboxPermissive *bool `json:"requestSandboxPermissive,omitempty"` + // Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + // Experimental: SandboxPathGrant is part of an experimental API and may change or be removed. + SandboxPathGrant *PermissionSandboxPathGrant `json:"sandboxPathGrant,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` // Optional warning message about risks of running this command @@ -4061,50 +4254,6 @@ func (PermissionPromptRequestExtensionPermissionAccess) Kind() PermissionPromptR return PermissionPromptRequestKindExtensionPermissionAccess } -// Factory run or authoring permission prompt -type PermissionPromptRequestFactory struct { - // Canonical key used for scoped factory approvals - ApprovalKey string `json:"approvalKey"` - // Assisted-approval judge information for this request; present only in assisted mode. - // Experimental: AssistedApproval is part of an experimental API and may change or be removed. - AssistedApproval *PermissionAssistedApproval `json:"assistedApproval,omitempty"` - // Whether this factory is eligible for persistent approval - CanPersistApproval bool `json:"canPersistApproval"` - // Factory-declared AI-credit limit before any run/resume caller override is applied. - DeclaredMaxAiCredits *float64 `json:"declaredMaxAiCredits,omitempty"` - // Factory-declared concurrent-subagent limit before any run/resume caller override is applied. - DeclaredMaxConcurrentSubagents *int64 `json:"declaredMaxConcurrentSubagents,omitempty"` - // Factory-declared total-subagent limit before any run/resume caller override is applied. - DeclaredMaxTotalSubagents *int64 `json:"declaredMaxTotalSubagents,omitempty"` - // Factory-declared active-time limit in seconds before any run/resume caller override is applied. - DeclaredTimeoutSeconds *float64 `json:"declaredTimeoutSeconds,omitempty"` - // Factory description - Description string `json:"description"` - // Whether managed policy requires a human response and forbids host auto-approval - ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` - // Effective AI-credit limit; omitted means unlimited - MaxAiCredits *float64 `json:"maxAiCredits,omitempty"` - // Effective concurrent-subagent limit; omitted means unlimited - MaxConcurrentSubagents *int64 `json:"maxConcurrentSubagents,omitempty"` - // Effective total-subagent limit; omitted means unlimited - MaxTotalSubagents *int64 `json:"maxTotalSubagents,omitempty"` - // Factory name - Name string `json:"name"` - // Factory operation, either run or author - Operation FactoryPermissionOperation `json:"operation"` - // Declared factory phases - Phases []FactoryPermissionPhase `json:"phases"` - // Effective active-time limit in seconds; omitted means unlimited - TimeoutSeconds *float64 `json:"timeoutSeconds,omitempty"` - // Tool call ID that triggered this permission request - ToolCallID *string `json:"toolCallId,omitempty"` -} - -func (PermissionPromptRequestFactory) permissionPromptRequest() {} -func (PermissionPromptRequestFactory) Kind() PermissionPromptRequestKind { - return PermissionPromptRequestKindFactory -} - // Hook confirmation permission prompt type PermissionPromptRequestHook struct { // Assisted-approval judge information for this request; present only in assisted mode. @@ -4187,6 +4336,9 @@ type PermissionPromptRequestPath struct { AssistedApproval *PermissionAssistedApproval `json:"assistedApproval,omitempty"` // File paths that require explicit approval Paths []string `json:"paths"` + // Canonical directory candidates that can be granted for file-tool read access in this logical session. Present only for read path prompts. + // Experimental: ReadOnlyDirectories is part of an experimental API and may change or be removed. + ReadOnlyDirectories []string `json:"readOnlyDirectories,omitzero"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` } @@ -4245,6 +4397,50 @@ func (PermissionPromptRequestURL) Kind() PermissionPromptRequestKind { return PermissionPromptRequestKindURL } +// Workflow run or authoring permission prompt +type PermissionPromptRequestWorkflow struct { + // Canonical key used for scoped workflow approvals + ApprovalKey string `json:"approvalKey"` + // Assisted-approval judge information for this request; present only in assisted mode. + // Experimental: AssistedApproval is part of an experimental API and may change or be removed. + AssistedApproval *PermissionAssistedApproval `json:"assistedApproval,omitempty"` + // Whether this workflow is eligible for persistent approval + CanPersistApproval bool `json:"canPersistApproval"` + // Workflow-declared AI-credit limit before any run/resume caller override is applied. + DeclaredMaxAiCredits *float64 `json:"declaredMaxAiCredits,omitempty"` + // Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. + DeclaredMaxConcurrentSubagents *int64 `json:"declaredMaxConcurrentSubagents,omitempty"` + // Workflow-declared total-subagent limit before any run/resume caller override is applied. + DeclaredMaxTotalSubagents *int64 `json:"declaredMaxTotalSubagents,omitempty"` + // Workflow-declared active-time limit in seconds before any run/resume caller override is applied. + DeclaredTimeoutSeconds *float64 `json:"declaredTimeoutSeconds,omitempty"` + // Workflow description + Description string `json:"description"` + // Whether managed policy requires a human response and forbids host auto-approval + ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` + // Effective AI-credit limit; omitted means unlimited + MaxAiCredits *float64 `json:"maxAiCredits,omitempty"` + // Effective concurrent-subagent limit; omitted means unlimited + MaxConcurrentSubagents *int64 `json:"maxConcurrentSubagents,omitempty"` + // Effective total-subagent limit; omitted means unlimited + MaxTotalSubagents *int64 `json:"maxTotalSubagents,omitempty"` + // Workflow name + Name string `json:"name"` + // Workflow operation, either run or author + Operation WorkflowPermissionOperation `json:"operation"` + // Declared workflow phases + Phases []WorkflowPermissionPhase `json:"phases"` + // Effective active-time limit in seconds; omitted means unlimited + TimeoutSeconds *float64 `json:"timeoutSeconds,omitempty"` + // Tool call ID that triggered this permission request + ToolCallID *string `json:"toolCallId,omitempty"` +} + +func (PermissionPromptRequestWorkflow) permissionPromptRequest() {} +func (PermissionPromptRequestWorkflow) Kind() PermissionPromptRequestKind { + return PermissionPromptRequestKindWorkflow +} + // File write permission prompt type PermissionPromptRequestWrite struct { // Assisted-approval judge information for this request; present only in assisted mode. @@ -4363,47 +4559,6 @@ func (PermissionRequestExtensionPermissionAccess) Kind() PermissionRequestKind { return PermissionRequestKindExtensionPermissionAccess } -// Factory run or authoring permission request -type PermissionRequestFactory struct { - // Canonical key used for scoped factory approvals - ApprovalKey string `json:"approvalKey"` - // Whether this factory is eligible for persistent approval - CanPersistApproval bool `json:"canPersistApproval"` - // Factory-declared AI-credit limit before any run/resume caller override is applied. - DeclaredMaxAiCredits *float64 `json:"declaredMaxAiCredits,omitempty"` - // Factory-declared concurrent-subagent limit before any run/resume caller override is applied. - DeclaredMaxConcurrentSubagents *int64 `json:"declaredMaxConcurrentSubagents,omitempty"` - // Factory-declared total-subagent limit before any run/resume caller override is applied. - DeclaredMaxTotalSubagents *int64 `json:"declaredMaxTotalSubagents,omitempty"` - // Factory-declared active-time limit in seconds before any run/resume caller override is applied. - DeclaredTimeoutSeconds *float64 `json:"declaredTimeoutSeconds,omitempty"` - // Factory description - Description string `json:"description"` - // When true, managed policy requires an explicit user decision and automatic approval must be bypassed. - ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` - // Effective AI-credit limit; omitted means unlimited - MaxAiCredits *float64 `json:"maxAiCredits,omitempty"` - // Effective concurrent-subagent limit; omitted means unlimited - MaxConcurrentSubagents *int64 `json:"maxConcurrentSubagents,omitempty"` - // Effective total-subagent limit; omitted means unlimited - MaxTotalSubagents *int64 `json:"maxTotalSubagents,omitempty"` - // Factory name - Name string `json:"name"` - // Factory operation, either run or author - Operation FactoryPermissionOperation `json:"operation"` - // Declared factory phases - Phases []FactoryPermissionPhase `json:"phases"` - // Effective active-time limit in seconds; omitted means unlimited - TimeoutSeconds *float64 `json:"timeoutSeconds,omitempty"` - // Tool call ID that triggered this permission request - ToolCallID *string `json:"toolCallId,omitempty"` -} - -func (PermissionRequestFactory) permissionRequest() {} -func (PermissionRequestFactory) Kind() PermissionRequestKind { - return PermissionRequestKindFactory -} - // Hook confirmation permission request type PermissionRequestHook struct { // Optional message from the hook explaining why confirmation is needed @@ -4496,6 +4651,9 @@ type PermissionRequestRead struct { // Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display path. // Experimental: ResolvedPath is part of an experimental API and may change or be removed. ResolvedPath *string `json:"resolvedPath,omitempty"` + // Sandbox policy edit that would let the read run inside the sandbox. Only present when requestSandboxBypass is true. + // Experimental: SandboxPathGrant is part of an experimental API and may change or be removed. + SandboxPathGrant *PermissionSandboxPathGrant `json:"sandboxPathGrant,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` } @@ -4537,6 +4695,9 @@ type PermissionRequestShell struct { // Runtime-resolved canonical working directory the command runs in, used for authorization identity checks. Internal and experimental; clients should not display it. // Experimental: ResolvedWorkingDirectory is part of an experimental API and may change or be removed. ResolvedWorkingDirectory *string `json:"resolvedWorkingDirectory,omitempty"` + // Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + // Experimental: SandboxPathGrant is part of an experimental API and may change or be removed. + SandboxPathGrant *PermissionSandboxPathGrant `json:"sandboxPathGrant,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` // Optional warning message about risks of running this command @@ -4571,6 +4732,47 @@ func (PermissionRequestURL) Kind() PermissionRequestKind { return PermissionRequestKindURL } +// Workflow run or authoring permission request +type PermissionRequestWorkflow struct { + // Canonical key used for scoped workflow approvals + ApprovalKey string `json:"approvalKey"` + // Whether this workflow is eligible for persistent approval + CanPersistApproval bool `json:"canPersistApproval"` + // Workflow-declared AI-credit limit before any run/resume caller override is applied. + DeclaredMaxAiCredits *float64 `json:"declaredMaxAiCredits,omitempty"` + // Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. + DeclaredMaxConcurrentSubagents *int64 `json:"declaredMaxConcurrentSubagents,omitempty"` + // Workflow-declared total-subagent limit before any run/resume caller override is applied. + DeclaredMaxTotalSubagents *int64 `json:"declaredMaxTotalSubagents,omitempty"` + // Workflow-declared active-time limit in seconds before any run/resume caller override is applied. + DeclaredTimeoutSeconds *float64 `json:"declaredTimeoutSeconds,omitempty"` + // Workflow description + Description string `json:"description"` + // Whether managed policy requires a human response and forbids host auto-approval + ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` + // Effective AI-credit limit; omitted means unlimited + MaxAiCredits *float64 `json:"maxAiCredits,omitempty"` + // Effective concurrent-subagent limit; omitted means unlimited + MaxConcurrentSubagents *int64 `json:"maxConcurrentSubagents,omitempty"` + // Effective total-subagent limit; omitted means unlimited + MaxTotalSubagents *int64 `json:"maxTotalSubagents,omitempty"` + // Workflow name + Name string `json:"name"` + // Workflow operation, either run or author + Operation WorkflowPermissionOperation `json:"operation"` + // Declared workflow phases + Phases []WorkflowPermissionPhase `json:"phases"` + // Effective active-time limit in seconds; omitted means unlimited + TimeoutSeconds *float64 `json:"timeoutSeconds,omitempty"` + // Tool call ID that triggered this permission request + ToolCallID *string `json:"toolCallId,omitempty"` +} + +func (PermissionRequestWorkflow) permissionRequest() {} +func (PermissionRequestWorkflow) Kind() PermissionRequestKind { + return PermissionRequestKindWorkflow +} + // File write permission request type PermissionRequestWrite struct { // Whether the UI can offer session-wide approval for file write operations @@ -4592,6 +4794,9 @@ type PermissionRequestWrite struct { // Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display fileName. // Experimental: ResolvedPath is part of an experimental API and may change or be removed. ResolvedPath *string `json:"resolvedPath,omitempty"` + // Sandbox policy edit that would let the write run inside the sandbox. Only present when requestSandboxBypass is true. + // Experimental: SandboxPathGrant is part of an experimental API and may change or be removed. + SandboxPathGrant *PermissionSandboxPathGrant `json:"sandboxPathGrant,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` } @@ -4678,6 +4883,17 @@ func (PermissionApprovedForSession) Kind() PermissionResultKind { return PermissionResultKindApprovedForSession } +// Permission response variant that approves a request and records file-tool read authority for specific directories in this logical session. +type PermissionApprovedReadOnlyForSession struct { + // Canonical directories covered by the session read-only grant + Directories []string `json:"directories"` +} + +func (PermissionApprovedReadOnlyForSession) permissionResult() {} +func (PermissionApprovedReadOnlyForSession) Kind() PermissionResultKind { + return PermissionResultKindApprovedReadOnlyForSession +} + // Permission response variant indicating the request was cancelled before use, with an optional reason. type PermissionCancelled struct { // Optional explanation of why the request was cancelled @@ -4748,6 +4964,19 @@ func (PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser) Kind() Permissio return PermissionResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser } +// A sandbox filesystem policy edit that would let a blocked operation run inside the sandbox instead of outside it. Offered only on a sandbox escalation request whose denial adding this path lifts, and only when managed policy permits the grant. A host accepts it with session.sandbox.grantPathForRequest, which adds the path to the session's sandbox policy and re-runs the operation sandboxed; a host that persists sandbox settings may also save the path there. +// Experimental: PermissionSandboxPathGrant is part of an experimental API and may change or be removed. +type PermissionSandboxPathGrant struct { + // Which access the grant confers, and so which policy list the path is added to + Access PermissionSandboxPathGrantAccess `json:"access"` + // The path the sandbox refused, present only when it differs from path. That happens when a write under a read-only folder moves the folder to the read-write paths, when a path that does not exist yet is granted through its nearest existing folder, because the OS sandbox cannot grant a path before it exists, and when either is spelled through a symlink, because a grant covers its path as written, so path is then the resolved location. Hosts should then name path in the offer, since the denial names this one. + DeniedPath *string `json:"deniedPath,omitempty"` + // Absolute path to add to the sandbox filesystem policy + Path string `json:"path"` + // readonlyPaths entries the grant removes, exactly as written in the policy, because a read-only entry for the same location would otherwise keep the path read-only. A host that persists the path must remove these entries from its stored readonlyPaths too. + RemovedReadonlyPaths []string `json:"removedReadonlyPaths,omitzero"` +} + // A model-facing binary result as persisted: full inline data, a size-omitted marker, or a deduplicated asset reference // Experimental: PersistedBinaryResult is part of an experimental API and may change or be removed. type PersistedBinaryResult interface { @@ -5017,37 +5246,6 @@ func (SystemNotificationAgentIdle) Type() SystemNotificationType { return SystemNotificationTypeAgentIdle } -// System notification metadata for a factory execution attempt that reached a terminal state. -type SystemNotificationFactoryCompleted struct { - // Execution attempt that reached this terminal state. - Attempt int64 `json:"attempt"` - // Consumed AI usage in nano-AIU. - ConsumedNanoAiu int64 `json:"consumedNanoAiu"` - // Subagents consumed by the run across all attempts. - ConsumedSubagents int64 `json:"consumedSubagents"` - // Accumulated active execution time in milliseconds. - ElapsedMs int64 `json:"elapsedMs"` - // Persisted factory name. - FactoryName string `json:"factoryName"` - // Machine-readable terminal failure details, when present. - Failure any `json:"failure,omitempty"` - // Pause initiator metadata when this attempt settled as paused. - PauseInfo SystemNotificationFactoryPauseInfo `json:"pauseInfo,omitempty"` - // Bounded prompt-safe preview of the completed result. - ResultPreview *string `json:"resultPreview,omitempty"` - // Actionable run_factory resume guidance for a resource-limit failure. - RetryGuidance *string `json:"retryGuidance,omitempty"` - // Factory run identifier. - RunID string `json:"runId"` - // Terminal status reached by this execution attempt. - Status SystemNotificationFactoryCompletedStatus `json:"status"` -} - -func (SystemNotificationFactoryCompleted) systemNotification() {} -func (SystemNotificationFactoryCompleted) Type() SystemNotificationType { - return SystemNotificationTypeFactoryCompleted -} - // System notification metadata for an instruction file discovered during tool access, including source, trigger file, and tool. type SystemNotificationInstructionDiscovered struct { // Human-readable label for the timeline (e.g., 'AGENTS.md from packages/billing/') @@ -5121,38 +5319,69 @@ func (SystemNotificationUnclassified) Type() SystemNotificationType { return SystemNotificationTypeUnclassified } -// Durable metadata describing who initiated a factory pause. -type SystemNotificationFactoryPauseInfo interface { - systemNotificationFactoryPauseInfo() - Type() SystemNotificationFactoryPauseInfoType +// System notification metadata for a workflow execution attempt that reached a terminal state. +type SystemNotificationWorkflowCompleted struct { + // Execution attempt that reached this terminal state. + Attempt int64 `json:"attempt"` + // Consumed AI usage in nano-AIU. + ConsumedNanoAiu int64 `json:"consumedNanoAiu"` + // Subagents consumed by the run across all attempts. + ConsumedSubagents int64 `json:"consumedSubagents"` + // Accumulated active execution time in milliseconds. + ElapsedMs int64 `json:"elapsedMs"` + // Machine-readable terminal failure details, when present. + Failure any `json:"failure,omitempty"` + // Pause initiator metadata when this attempt settled as paused. + PauseInfo SystemNotificationWorkflowPauseInfo `json:"pauseInfo,omitempty"` + // Bounded prompt-safe preview of the completed result. + ResultPreview *string `json:"resultPreview,omitempty"` + // Actionable run_dynamic_workflow resume guidance for a resource-limit failure. + RetryGuidance *string `json:"retryGuidance,omitempty"` + // Workflow run identifier. + RunID string `json:"runId"` + // Terminal status reached by this execution attempt. + Status SystemNotificationWorkflowCompletedStatus `json:"status"` + // Persisted workflow name. + WorkflowName string `json:"workflowName"` +} + +func (SystemNotificationWorkflowCompleted) systemNotification() {} +func (SystemNotificationWorkflowCompleted) Type() SystemNotificationType { + return SystemNotificationTypeWorkflowCompleted +} + +// Durable metadata describing who initiated a workflow pause. +type SystemNotificationWorkflowPauseInfo interface { + systemNotificationWorkflowPauseInfo() + Type() SystemNotificationWorkflowPauseInfoType } -type RawSystemNotificationFactoryPauseInfo struct { - Discriminator SystemNotificationFactoryPauseInfoType +type RawSystemNotificationWorkflowPauseInfo struct { + Discriminator SystemNotificationWorkflowPauseInfoType Raw json.RawMessage } -func (RawSystemNotificationFactoryPauseInfo) systemNotificationFactoryPauseInfo() {} -func (r RawSystemNotificationFactoryPauseInfo) Type() SystemNotificationFactoryPauseInfoType { +func (RawSystemNotificationWorkflowPauseInfo) systemNotificationWorkflowPauseInfo() {} +func (r RawSystemNotificationWorkflowPauseInfo) Type() SystemNotificationWorkflowPauseInfoType { return r.Discriminator } -type SystemNotificationFactoryPauseInfoCheckpoint struct { +type SystemNotificationWorkflowPauseInfoCheckpoint struct { // Stable author-defined checkpoint key that initiated the pause. Key string `json:"key"` } -func (SystemNotificationFactoryPauseInfoCheckpoint) systemNotificationFactoryPauseInfo() {} -func (SystemNotificationFactoryPauseInfoCheckpoint) Type() SystemNotificationFactoryPauseInfoType { - return SystemNotificationFactoryPauseInfoTypeCheckpoint +func (SystemNotificationWorkflowPauseInfoCheckpoint) systemNotificationWorkflowPauseInfo() {} +func (SystemNotificationWorkflowPauseInfoCheckpoint) Type() SystemNotificationWorkflowPauseInfoType { + return SystemNotificationWorkflowPauseInfoTypeCheckpoint } -type SystemNotificationFactoryPauseInfoUser struct { +type SystemNotificationWorkflowPauseInfoUser struct { } -func (SystemNotificationFactoryPauseInfoUser) systemNotificationFactoryPauseInfo() {} -func (SystemNotificationFactoryPauseInfoUser) Type() SystemNotificationFactoryPauseInfoType { - return SystemNotificationFactoryPauseInfoTypeUser +func (SystemNotificationWorkflowPauseInfoUser) systemNotificationWorkflowPauseInfo() {} +func (SystemNotificationWorkflowPauseInfoUser) Type() SystemNotificationWorkflowPauseInfoType { + return SystemNotificationWorkflowPauseInfoTypeUser } // A content block within a tool result, which may be text, terminal output, image, audio, or a resource @@ -5306,6 +5535,15 @@ type ToolExecutionCompleteError struct { Remediation *RemediationAction `json:"remediation,omitempty"` } +// A file mutation that was actually committed by a built-in file editing tool. +// Experimental: ToolExecutionCompleteFileEdit is part of an experimental API and may change or be removed. +type ToolExecutionCompleteFileEdit struct { + // Kind of mutation committed at this path. + Kind ToolExecutionCompleteFileEditKind `json:"kind"` + // Absolute path in the session filesystem namespace. + Path string `json:"path"` +} + // Tool execution result on success type ToolExecutionCompleteResult struct { // Model-facing binary results (base64 inline or size-omitted markers) sent to the LLM for this tool call @@ -5479,6 +5717,14 @@ type UsageCheckpointModelCacheState struct { ModelID string `json:"modelId"` } +// A declared phase shown in a workflow permission prompt. +type WorkflowPermissionPhase struct { + // Optional phase detail + Detail *string `json:"detail,omitempty"` + // Phase title + Title string `json:"title"` +} + // Working directory and git context at session start type WorkingDirectoryContext struct { // Base commit of current git branch at session start time @@ -5820,32 +6066,6 @@ const ( ExtensionsLoadedExtensionStatusStarting ExtensionsLoadedExtensionStatus = "starting" ) -// Operation gated by a factory permission request. -type FactoryPermissionOperation string - -const ( - // Authoring a factory, which writes JavaScript into a session-scoped extension and loads it. - FactoryPermissionOperationAuthor FactoryPermissionOperation = "author" - // Running a registered factory, which spends subagents, active time, and AI credits under the approved limits. - FactoryPermissionOperationRun FactoryPermissionOperation = "run" -) - -// Terminal status a factory run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. -type FactoryRunSettledStatus string - -const ( - // The run was cancelled by its caller or by session disposal. - FactoryRunSettledStatusCancelled FactoryRunSettledStatus = "cancelled" - // The factory body resolved and its result was committed. - FactoryRunSettledStatusCompleted FactoryRunSettledStatus = "completed" - // The run failed, with `failureType` carrying the class when it has one. - FactoryRunSettledStatusError FactoryRunSettledStatus = "error" - // The run was stopped by a limit, an approval refusal or another policy decision. - FactoryRunSettledStatusHalted FactoryRunSettledStatus = "halted" - // The attempt paused intentionally while preserving resumable run state. - FactoryRunSettledStatusPaused FactoryRunSettledStatus = "paused" -) - // Conversation scope in which a HydraFusion phase executes. // Experimental: FusionConversationScope is part of an experimental API and may change or be removed. type FusionConversationScope string @@ -5962,6 +6182,27 @@ const ( HandoffSourceTypeRemote HandoffSourceType = "remote" ) +// Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. +type HumanResponseActor string + +const ( + // A host or SDK automation submitted the response without direct human interaction. + HumanResponseActorHostAutomation HumanResponseActor = "host_automation" + // A built-in trusted client submitted the response after direct human interaction. + HumanResponseActorHumanResponse HumanResponseActor = "human_response" + // The response came through a legacy or otherwise unattributed ingress. + HumanResponseActorUnknown HumanResponseActor = "unknown" +) + +// ResponseKind discriminator for HumanResponseRecordedResponse. +type HumanResponseRecordedResponseResponseKind string + +const ( + HumanResponseRecordedResponseResponseKindAskUser HumanResponseRecordedResponseResponseKind = "ask_user" + HumanResponseRecordedResponseResponseKindExitPlanMode HumanResponseRecordedResponseResponseKind = "exit_plan_mode" + HumanResponseRecordedResponseResponseKindUserInput HumanResponseRecordedResponseResponseKind = "user_input" +) + // The category of runtime action that enterprise managed settings governed (blocked or capped) type ManagedSettingsEnforcedAction string @@ -6101,6 +6342,28 @@ const ( ModelCallFailureTransportWebsocket ModelCallFailureTransport = "websocket" ) +// Final bounded result of one logical model operation after its internal retry loop settles +type ModelCallFinalResult string + +const ( + // The final attempt failed with HTTP 400. + ModelCallFinalResultHTTP400 ModelCallFinalResult = "http_400" + // The final attempt failed with HTTP 413. + ModelCallFinalResultHTTP413 ModelCallFinalResult = "http_413" + // The final attempt failed with HTTP 429. + ModelCallFinalResultHTTP429 ModelCallFinalResult = "http_429" + // The final attempt failed with another HTTP 4xx status. + ModelCallFinalResultHTTP4xx ModelCallFinalResult = "http_4xx" + // The final attempt failed with an HTTP 5xx status. + ModelCallFinalResultHTTP5xx ModelCallFinalResult = "http_5xx" + // The final attempt failed without another bounded classification. + ModelCallFinalResultOtherError ModelCallFinalResult = "other_error" + // The final attempt succeeded. + ModelCallFinalResultSuccess ModelCallFinalResult = "success" + // The final attempt failed in the request transport. + ModelCallFinalResultTransportError ModelCallFinalResult = "transport_error" +) + // Final outcome of one logical model dispatch after response acceptance processing type ModelCallFinishedOutcome string @@ -6177,6 +6440,14 @@ const ( PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable PermissionApprovalEvaluationReasonCode = "argument-binding-unreviewable" // The judge was skipped because authorization extraction could not safely establish a complete recent history. PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete PermissionApprovalEvaluationReasonCode = "authorization-history-incomplete" + // A code source was excluded from review by content exclusion policy. + PermissionApprovalEvaluationReasonCodeContentExcluded PermissionApprovalEvaluationReasonCode = "content-excluded" + // The shell command used a code source computed at run time. + PermissionApprovalEvaluationReasonCodeDynamicSource PermissionApprovalEvaluationReasonCode = "dynamic-source" + // A code-bearing executable exceeded the binding size limit. + PermissionApprovalEvaluationReasonCodeExecutableTooLarge PermissionApprovalEvaluationReasonCode = "executable-too-large" + // A code-bearing executable could not be inspected. + PermissionApprovalEvaluationReasonCodeExecutableUnavailable PermissionApprovalEvaluationReasonCode = "executable-unavailable" // Assisted approval was inactive for this request. PermissionApprovalEvaluationReasonCodeInactive PermissionApprovalEvaluationReasonCode = "inactive" // The request inherited an outcome from another decision. @@ -6213,6 +6484,8 @@ const ( PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable PermissionApprovalEvaluationReasonCode = "shell-environment-unreviewable" // The script snapshot exceeded the size limit. PermissionApprovalEvaluationReasonCodeTooLarge PermissionApprovalEvaluationReasonCode = "too-large" + // The shell command referenced more code sources than can be reviewed. + PermissionApprovalEvaluationReasonCodeTooManySources PermissionApprovalEvaluationReasonCode = "too-many-sources" // Script review was unavailable. PermissionApprovalEvaluationReasonCodeUnavailable PermissionApprovalEvaluationReasonCode = "unavailable" // Attribution is missing or outside the supported vocabulary. @@ -6223,16 +6496,20 @@ const ( PermissionApprovalEvaluationReasonCodeUnrepresentablePath PermissionApprovalEvaluationReasonCode = "unrepresentable-path" // The script invocation could not be reviewed. PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation PermissionApprovalEvaluationReasonCode = "unreviewable-script-invocation" + // The shell command could not be analyzed for execution evidence. + PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape PermissionApprovalEvaluationReasonCode = "unsupported-command-shape" + // The shell command used a code source that cannot be bound for review. + PermissionApprovalEvaluationReasonCodeUnsupportedSource PermissionApprovalEvaluationReasonCode = "unsupported-source" ) -// Which direction a message-backed authorization claim moves authority in. +// Direction stored in a historical extractor claim. Current runtimes do not apply it. // Experimental: PermissionMessageAuthorizationPolarity is part of an experimental API and may change or be removed. type PermissionMessageAuthorizationPolarity string const ( - // The human's words refused an effect. + // Historical claim recorded as a denial. PermissionMessageAuthorizationPolarityDenial PermissionMessageAuthorizationPolarity = "denial" - // The human's words authorized an effect. + // Historical claim recorded as a grant. PermissionMessageAuthorizationPolarityGrant PermissionMessageAuthorizationPolarity = "grant" ) @@ -6245,13 +6522,13 @@ const ( PermissionPromptRequestKindExtensionEnvAccess PermissionPromptRequestKind = "extension-env-access" PermissionPromptRequestKindExtensionManagement PermissionPromptRequestKind = "extension-management" PermissionPromptRequestKindExtensionPermissionAccess PermissionPromptRequestKind = "extension-permission-access" - PermissionPromptRequestKindFactory PermissionPromptRequestKind = "factory" PermissionPromptRequestKindHook PermissionPromptRequestKind = "hook" PermissionPromptRequestKindMCP PermissionPromptRequestKind = "mcp" PermissionPromptRequestKindMemory PermissionPromptRequestKind = "memory" PermissionPromptRequestKindPath PermissionPromptRequestKind = "path" PermissionPromptRequestKindRead PermissionPromptRequestKind = "read" PermissionPromptRequestKindURL PermissionPromptRequestKind = "url" + PermissionPromptRequestKindWorkflow PermissionPromptRequestKind = "workflow" PermissionPromptRequestKindWrite PermissionPromptRequestKind = "write" ) @@ -6284,13 +6561,13 @@ const ( PermissionRequestKindExtensionEnvAccess PermissionRequestKind = "extension-env-access" PermissionRequestKindExtensionManagement PermissionRequestKind = "extension-management" PermissionRequestKindExtensionPermissionAccess PermissionRequestKind = "extension-permission-access" - PermissionRequestKindFactory PermissionRequestKind = "factory" PermissionRequestKindHook PermissionRequestKind = "hook" PermissionRequestKindMCP PermissionRequestKind = "mcp" PermissionRequestKindMemory PermissionRequestKind = "memory" PermissionRequestKindRead PermissionRequestKind = "read" PermissionRequestKindShell PermissionRequestKind = "shell" PermissionRequestKindURL PermissionRequestKind = "url" + PermissionRequestKindWorkflow PermissionRequestKind = "workflow" PermissionRequestKindWrite PermissionRequestKind = "write" ) @@ -6331,6 +6608,7 @@ const ( PermissionResultKindApproved PermissionResultKind = "approved" PermissionResultKindApprovedForLocation PermissionResultKind = "approved-for-location" PermissionResultKindApprovedForSession PermissionResultKind = "approved-for-session" + PermissionResultKindApprovedReadOnlyForSession PermissionResultKind = "approved-read-only-for-session" PermissionResultKindCancelled PermissionResultKind = "cancelled" PermissionResultKindDeniedByContentExclusionPolicy PermissionResultKind = "denied-by-content-exclusion-policy" PermissionResultKindDeniedByPermissionRequestHook PermissionResultKind = "denied-by-permission-request-hook" @@ -6339,6 +6617,16 @@ const ( PermissionResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser PermissionResultKind = "denied-no-approval-rule-and-could-not-request-from-user" ) +// Access a sandbox path grant confers +type PermissionSandboxPathGrantAccess string + +const ( + // Read access: the path is added to readonlyPaths. + PermissionSandboxPathGrantAccessRead PermissionSandboxPathGrantAccess = "read" + // Read and write access: the path is added to readwritePaths. + PermissionSandboxPathGrantAccessReadWrite PermissionSandboxPathGrantAccess = "readWrite" +) + // Binary result type discriminator. Use "image" for images and "resource" for other binary data. type PersistedBinaryImageType string @@ -6472,42 +6760,42 @@ const ( SystemNotificationAgentCompletedStatusFailed SystemNotificationAgentCompletedStatus = "failed" ) -// Terminal status reached by a factory execution attempt. -type SystemNotificationFactoryCompletedStatus string - -const ( - // The factory was cancelled. - SystemNotificationFactoryCompletedStatusCancelled SystemNotificationFactoryCompletedStatus = "cancelled" - // The factory completed successfully. - SystemNotificationFactoryCompletedStatusCompleted SystemNotificationFactoryCompletedStatus = "completed" - // The factory failed. - SystemNotificationFactoryCompletedStatusError SystemNotificationFactoryCompletedStatus = "error" - // The factory was halted. - SystemNotificationFactoryCompletedStatusHalted SystemNotificationFactoryCompletedStatus = "halted" - // The factory attempt paused intentionally. - SystemNotificationFactoryCompletedStatusPaused SystemNotificationFactoryCompletedStatus = "paused" -) - -// Type discriminator for SystemNotificationFactoryPauseInfo. -type SystemNotificationFactoryPauseInfoType string - -const ( - SystemNotificationFactoryPauseInfoTypeCheckpoint SystemNotificationFactoryPauseInfoType = "checkpoint" - SystemNotificationFactoryPauseInfoTypeUser SystemNotificationFactoryPauseInfoType = "user" -) - // Type discriminator for SystemNotification. type SystemNotificationType string const ( SystemNotificationTypeAgentCompleted SystemNotificationType = "agent_completed" SystemNotificationTypeAgentIdle SystemNotificationType = "agent_idle" - SystemNotificationTypeFactoryCompleted SystemNotificationType = "factory_completed" SystemNotificationTypeInstructionDiscovered SystemNotificationType = "instruction_discovered" SystemNotificationTypeNewInboxMessage SystemNotificationType = "new_inbox_message" SystemNotificationTypeShellCompleted SystemNotificationType = "shell_completed" SystemNotificationTypeShellDetachedCompleted SystemNotificationType = "shell_detached_completed" SystemNotificationTypeUnclassified SystemNotificationType = "unclassified" + SystemNotificationTypeWorkflowCompleted SystemNotificationType = "workflow_completed" +) + +// Terminal status reached by a workflow execution attempt. +type SystemNotificationWorkflowCompletedStatus string + +const ( + // The workflow was cancelled. + SystemNotificationWorkflowCompletedStatusCancelled SystemNotificationWorkflowCompletedStatus = "cancelled" + // The workflow completed successfully. + SystemNotificationWorkflowCompletedStatusCompleted SystemNotificationWorkflowCompletedStatus = "completed" + // The workflow failed. + SystemNotificationWorkflowCompletedStatusError SystemNotificationWorkflowCompletedStatus = "error" + // The workflow was halted. + SystemNotificationWorkflowCompletedStatusHalted SystemNotificationWorkflowCompletedStatus = "halted" + // The workflow attempt paused intentionally. + SystemNotificationWorkflowCompletedStatusPaused SystemNotificationWorkflowCompletedStatus = "paused" +) + +// Type discriminator for SystemNotificationWorkflowPauseInfo. +type SystemNotificationWorkflowPauseInfoType string + +const ( + SystemNotificationWorkflowPauseInfoTypeCheckpoint SystemNotificationWorkflowPauseInfoType = "checkpoint" + SystemNotificationWorkflowPauseInfoTypeUser SystemNotificationWorkflowPauseInfoType = "user" ) // Theme variant this icon is intended for @@ -6533,6 +6821,19 @@ const ( ToolExecutionCompleteContentTypeText ToolExecutionCompleteContentType = "text" ) +// Kind of file mutation committed by a built-in editing tool. +// Experimental: ToolExecutionCompleteFileEditKind is part of an experimental API and may change or be removed. +type ToolExecutionCompleteFileEditKind string + +const ( + // A file was created. + ToolExecutionCompleteFileEditKindCreate ToolExecutionCompleteFileEditKind = "create" + // A file was deleted. + ToolExecutionCompleteFileEditKindDelete ToolExecutionCompleteFileEditKind = "delete" + // A file was written by an edit operation. + ToolExecutionCompleteFileEditKindEdit ToolExecutionCompleteFileEditKind = "edit" +) + // Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration. type ToolExecutionCompleteToolDescriptionMetaUIVisibility string @@ -6553,6 +6854,18 @@ const ( ToolExecutionStartToolDescriptionMetaUIVisibilityModel ToolExecutionStartToolDescriptionMetaUIVisibility = "model" ) +// Shell output source. Terminal output has no separate stdout/stderr attribution. +type ToolShellOutputStream string + +const ( + // Output from the shell command's standard error stream. + ToolShellOutputStreamStderr ToolShellOutputStream = "stderr" + // Output from the shell command's standard output stream. This is the default when stream is omitted. + ToolShellOutputStreamStdout ToolShellOutputStream = "stdout" + // Inherently merged output that cannot be attributed separately to stdout or stderr. + ToolShellOutputStreamTerminal ToolShellOutputStream = "terminal" +) + // Lifecycle phase for a Rust-owned ephemeral query stream. // Experimental: UIEphemeralQueryPhase is part of an experimental API and may change or be removed. type UIEphemeralQueryPhase string @@ -6596,6 +6909,32 @@ const ( UserMessageDeliverySteering UserMessageDelivery = "steering" ) +// Operation gated by a workflow permission request. +type WorkflowPermissionOperation string + +const ( + // Authoring a workflow, which writes JavaScript into a session-scoped extension and loads it. + WorkflowPermissionOperationAuthor WorkflowPermissionOperation = "author" + // Running a registered workflow, which spends subagents, active time, and AI credits under the approved limits. + WorkflowPermissionOperationRun WorkflowPermissionOperation = "run" +) + +// 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. +type WorkflowRunSettledStatus string + +const ( + // The run was cancelled by its caller or by session disposal. + WorkflowRunSettledStatusCancelled WorkflowRunSettledStatus = "cancelled" + // The workflow body resolved and its result was committed. + WorkflowRunSettledStatusCompleted WorkflowRunSettledStatus = "completed" + // The run failed, with `failureType` carrying the class when it has one. + WorkflowRunSettledStatusError WorkflowRunSettledStatus = "error" + // The run was stopped by a limit, an approval refusal or another policy decision. + WorkflowRunSettledStatusHalted WorkflowRunSettledStatus = "halted" + // The attempt paused intentionally while preserving resumable run state. + WorkflowRunSettledStatusPaused WorkflowRunSettledStatus = "paused" +) + // Hosting platform type of the repository (github or ado) type WorkingDirectoryContextHostType string diff --git a/go/session.go b/go/session.go index f4e7696518..bfc618f422 100644 --- a/go/session.go +++ b/go/session.go @@ -59,6 +59,7 @@ type Session struct { // SessionID is the unique identifier for this session. SessionID string workspacePath string + transcriptRecovery *TranscriptRecoveryReport client *jsonrpc2.Client clientSessionAPIs *rpc.ClientSessionAPIHandlers handlers []sessionHandler @@ -66,6 +67,8 @@ type Session struct { handlerMutex sync.RWMutex toolHandlers map[string]ToolHandler toolHandlersM sync.RWMutex + setToolsOnce sync.Once + setToolsGate chan struct{} pendingExternalTools map[string]*pendingExternalTool pendingExternalToolsM sync.Mutex externalToolsClosed bool @@ -90,6 +93,8 @@ type Session struct { elicitationMu sync.RWMutex canvasHandler CanvasHandler canvasMu sync.RWMutex + skillProvider SkillProvider + skillProviderMu sync.RWMutex bearerTokenProviders map[string]BearerTokenProvider bearerTokenMu sync.RWMutex releaseGitHubTokenProvider func() @@ -123,6 +128,17 @@ func (s *Session) WorkspacePath() string { return s.workspacePath } +// TranscriptRecovery returns the report from session.resume, or nil if no +// transcript repair was reported. The returned value is independent of session state. +func (s *Session) TranscriptRecovery() *TranscriptRecoveryReport { + if s.transcriptRecovery == nil { + return nil + } + report := *s.transcriptRecovery + report.InvalidLineNumbers = append([]int(nil), report.InvalidLineNumbers...) + return &report +} + // OpenCanvases returns the open-canvas snapshot last reported by the runtime. // The snapshot is populated from session.resume and live session.canvas.opened // and session.canvas.closed events. The returned slice is a copy and is safe to @@ -207,6 +223,24 @@ func (s *Session) getCanvasHandler() CanvasHandler { return s.canvasHandler } +func (s *Session) registerSkillProvider(provider SkillProvider) { + s.skillProviderMu.Lock() + defer s.skillProviderMu.Unlock() + s.skillProvider = provider +} + +func (s *Session) getSkillProvider() SkillProvider { + s.skillProviderMu.RLock() + defer s.skillProviderMu.RUnlock() + return s.skillProvider +} + +func (s *Session) clearSkillProvider() { + s.skillProviderMu.Lock() + s.skillProvider = nil + s.skillProviderMu.Unlock() +} + // registerBearerTokenProviders installs per-provider [BearerTokenProvider] callbacks // for BYOK providers configured with managed-identity / on-demand bearer-token // auth, keyed by provider name. @@ -624,16 +658,24 @@ func (s *Session) On(handler SessionEventHandler) func() { // // This method is internal and typically called when creating a session with tools. func (s *Session) registerTools(tools []Tool) { - s.toolHandlersM.Lock() - defer s.toolHandlersM.Unlock() + s.replaceToolHandlers(buildToolHandlerMap(tools)) +} - s.toolHandlers = make(map[string]ToolHandler) +func buildToolHandlerMap(tools []Tool) map[string]ToolHandler { + handlers := make(map[string]ToolHandler) for _, tool := range tools { if tool.Name == "" || tool.Handler == nil { continue } - s.toolHandlers[tool.Name] = tool.Handler + handlers[tool.Name] = tool.Handler } + return handlers +} + +func (s *Session) replaceToolHandlers(handlers map[string]ToolHandler) { + s.toolHandlersM.Lock() + defer s.toolHandlersM.Unlock() + s.toolHandlers = handlers } // getToolHandler retrieves a registered tool handler by name. @@ -874,6 +916,26 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) ( } return hooks.OnAgentStop(input, invocation) + case "subagentStart": + if hooks.OnSubagentStart == nil { + return nil, nil + } + var input SubagentStartHookInput + if err := json.Unmarshal(rawInput, &input); err != nil { + return nil, fmt.Errorf("invalid hook input: %w", err) + } + return hooks.OnSubagentStart(input, invocation) + + case "subagentStop": + if hooks.OnSubagentStop == nil { + return nil, nil + } + var input SubagentStopHookInput + if err := json.Unmarshal(rawInput, &input); err != nil { + return nil, fmt.Errorf("invalid hook input: %w", err) + } + return hooks.OnSubagentStop(input, invocation) + default: return nil, nil } @@ -1478,6 +1540,7 @@ func (s *Session) processEvents() { // CreateSession/ResumeSession use this when a locally registered session fails // before it can be returned to the caller. func (s *Session) stopEventProcessing() { + s.clearSkillProvider() s.closeOnce.Do(func() { close(s.eventDone) }) } @@ -1853,6 +1916,7 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) { // log.Printf("Failed to disconnect session: %v", err) // } func (s *Session) Disconnect() error { + s.clearSkillProvider() s.cancelPendingExternalTools() result, err := s.client.Request(context.Background(), "session.detach", sessionDetachRequest{SessionID: s.SessionID}) if err == nil { @@ -2029,6 +2093,108 @@ func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOpti return nil } +func (s *Session) setToolsSemaphore() chan struct{} { + s.setToolsOnce.Do(func() { + s.setToolsGate = make(chan struct{}, 1) + s.setToolsGate <- struct{}{} + }) + return s.setToolsGate +} + +func toolDefinitionsForSetTools(tools []Tool) []rpc.ProtocolExternalToolDefinition { + definitions := make([]rpc.ProtocolExternalToolDefinition, 0, len(tools)) + for _, tool := range tools { + definition := rpc.ProtocolExternalToolDefinition{ + Name: tool.Name, + Description: tool.Description, + Parameters: tool.Parameters, + Metadata: tool.Metadata, + } + if tool.OverridesBuiltInTool { + definition.OverridesBuiltInTool = &tool.OverridesBuiltInTool + } + if tool.SkipPermission { + definition.SkipPermission = &tool.SkipPermission + } + if tool.IsTerminal { + definition.IsTerminal = &tool.IsTerminal + } + if tool.Defer != "" { + deferPolicy := rpc.ProtocolExternalToolDefer(tool.Defer) + definition.Defer = &deferPolicy + } + definitions = append(definitions, definition) + } + return definitions +} + +// SetTools atomically replaces the externally implemented tools supplied by +// this client connection for the live session. Built-in tools, MCP/plugin tools, +// extension-discovered tools, subagent tools, and tools supplied by other +// connections are unchanged. +// +// The tools use the same [Tool] definitions and handlers as +// [Client.CreateSession] and [Client.ResumeSession]. The slice is a complete +// replacement for this connection: pass an empty slice to remove every tool this +// connection previously supplied. Tools with nil handlers are declaration-only. +// +// Handlers switch after the runtime accepts the replacement. Calls already +// running finish on the handlers that started them. If the runtime rejects the +// replacement, the previous handlers remain installed. Concurrent SetTools calls +// on the same session are sent and applied in call order; an earlier failure does +// not block later calls. If ctx is done before the request is sent, for example +// while an earlier SetTools call is still in flight, nothing is sent. Once the +// request is sent, ctx only bounds the wait: an accepted replacement still +// installs its handlers. +// +// The agent sees the new tools from its next model request, which can fall +// within a turn in progress. A model request already in flight was made with the +// previous tools, so the agent can still call a tool you removed. This session +// doesn't answer that call, and it can stay pending until the turn is aborted. If +// a running turn might still call a tool you remove, replace tools while the +// session is idle. +// +// Experimental: SetTools wraps the experimental session.tools.set RPC and may +// change or be removed. +func (s *Session) SetTools(ctx context.Context, tools []Tool) error { + definitions := toolDefinitionsForSetTools(tools) + handlers := buildToolHandlerMap(tools) + + gate := s.setToolsSemaphore() + select { + case <-gate: + case <-ctx.Done(): + return ctx.Err() + } + + if err := ctx.Err(); err != nil { + gate <- struct{}{} + return err + } + + done := make(chan error, 1) + go func() { + defer func() { gate <- struct{}{} }() + + _, err := s.RPC.Tools.Set(context.WithoutCancel(ctx), &rpc.ToolsSetRequest{ + Tools: definitions, + }) + if err != nil { + done <- fmt.Errorf("failed to set tools: %w", err) + return + } + s.replaceToolHandlers(handlers) + done <- nil + }() + + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() + } +} + // SetAutoTier changes the Auto routing preference without changing the selected model. // // The runtime does not apply the preference immediately. It records the request and diff --git a/go/session_fs_provider.go b/go/session_fs_provider.go index 0f653f1a0f..1f00ecc83f 100644 --- a/go/session_fs_provider.go +++ b/go/session_fs_provider.go @@ -5,6 +5,7 @@ package copilot import ( + "encoding/base64" "errors" "os" "time" @@ -12,6 +13,9 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) +const maxSessionFSBinaryBytes = (64*1024*1024 - 1024) / 4 * 3 +const maxSessionFSBinaryContentLength = (maxSessionFSBinaryBytes + 2) / 3 * 4 + // SessionFSProvider is the interface that SDK users implement to provide // a session filesystem. Methods use idiomatic Go error handling: return an // error for failures (the adapter maps os.ErrNotExist → ENOENT automatically). @@ -23,6 +27,7 @@ type SessionFSProvider interface { ReadFile(path string) (string, error) // WriteFile writes content to a file, creating it and parent directories if needed. // mode is an optional POSIX-style permission mode. Pass nil to use the OS default. + // Return a *SessionFSWriteFailure if the failed write changed the target. WriteFile(path string, content string, mode *int) error // AppendFile appends content to a file, creating it and parent directories if needed. // mode is an optional POSIX-style permission mode. Pass nil to use the OS default. @@ -48,6 +53,32 @@ type SessionFSProvider interface { Rename(src string, dest string) error } +// SessionFSWriteFailure reports that a failed WriteFile changed its target. +// Wrap the original error so its cause remains available to errors.Is and errors.As. +type SessionFSWriteFailure struct { + Err error +} + +func (e *SessionFSWriteFailure) Error() string { + if e == nil || e.Err == nil { + return "session filesystem write failed after changing the target" + } + return e.Err.Error() +} +func (e *SessionFSWriteFailure) Unwrap() error { + if e == nil { + return nil + } + return e.Err +} + +// SessionFSBinaryProvider is an optional interface for exact file bytes. +// Declare capabilities.binary only when the provider implements both methods. +type SessionFSBinaryProvider interface { + ReadFileBytes(path string) ([]byte, error) + WriteFileBytes(path string, content []byte, mode *int) error +} + // SessionFSSqliteProvider is an optional interface that a [SessionFSProvider] // may also implement to support per-session SQLite databases. The adapter // checks for this interface at runtime using a type assertion. If the @@ -129,6 +160,23 @@ func (a *sessionFSAdapter) ReadFile(request *rpc.SessionFSReadFileRequest) (*rpc return &rpc.SessionFSReadFileResult{Content: content}, nil } +func (a *sessionFSAdapter) ReadFileBytes(request *rpc.SessionFSReadFileBytesRequest) (*rpc.SessionFSReadFileBytesResult, error) { + provider, ok := a.provider.(SessionFSBinaryProvider) + if !ok { + return &rpc.SessionFSReadFileBytesResult{Error: toSessionFSError(errors.New("binary reads are not supported"))}, nil + } + bytes, err := provider.ReadFileBytes(request.Path) + if err != nil { + return &rpc.SessionFSReadFileBytesResult{Error: toSessionFSError(err)}, nil + } + if len(bytes) > maxSessionFSBinaryBytes { + return &rpc.SessionFSReadFileBytesResult{ + Error: toSessionFSError(errors.New("sessionFs.readFileBytes content exceeds the binary read limit")), + }, nil + } + return &rpc.SessionFSReadFileBytesResult{Content: base64.StdEncoding.EncodeToString(bytes)}, nil +} + func (a *sessionFSAdapter) WriteFile(request *rpc.SessionFSWriteFileRequest) (*rpc.SessionFSError, error) { var mode *int if request.Mode != nil { @@ -136,6 +184,41 @@ func (a *sessionFSAdapter) WriteFile(request *rpc.SessionFSWriteFileRequest) (*r mode = &m } if err := a.provider.WriteFile(request.Path, request.Content, mode); err != nil { + wire := toSessionFSError(err) + var changed *SessionFSWriteFailure + if errors.As(err, &changed) && changed != nil { + value := true + wire.WriteChanged = &value + } + return wire, nil + } + return nil, nil +} + +func (a *sessionFSAdapter) WriteFileBytes(request *rpc.SessionFSWriteFileBytesRequest) (*rpc.SessionFSError, error) { + provider, ok := a.provider.(SessionFSBinaryProvider) + if !ok { + return toSessionFSError(errors.New("binary writes are not supported")), nil + } + if len(request.Content) > maxSessionFSBinaryContentLength { + return toSessionFSError(errors.New("sessionFs.writeFileBytes content exceeds the binary write limit")), nil + } + content, err := base64.StdEncoding.Strict().DecodeString(request.Content) + if err != nil { + return toSessionFSError(err), nil + } + if base64.StdEncoding.EncodeToString(content) != request.Content { + return toSessionFSError(errors.New("invalid sessionFs.writeFileBytes base64 content")), nil + } + if len(content) > maxSessionFSBinaryBytes { + return toSessionFSError(errors.New("sessionFs.writeFileBytes content exceeds the binary write limit")), nil + } + var mode *int + if request.Mode != nil { + m := int(*request.Mode) + mode = &m + } + if err := provider.WriteFileBytes(request.Path, content, mode); err != nil { return toSessionFSError(err), nil } return nil, nil diff --git a/go/session_fs_provider_test.go b/go/session_fs_provider_test.go new file mode 100644 index 0000000000..e0b3d3aba0 --- /dev/null +++ b/go/session_fs_provider_test.go @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/github/copilot-sdk/go/rpc" +) + +type failingWriteProvider struct { + SessionFSProvider + err error +} + +func (p failingWriteProvider) WriteFile(string, string, *int) error { return p.err } +func (p failingWriteProvider) AppendFile(string, string, *int) error { return p.err } + +func TestSessionFSWriteFailure(t *testing.T) { + for _, tc := range []struct { + name string + err error + wantChanged bool + wantCode rpc.SessionFSErrorCode + wantMessage string + }{ + {"rejected", errors.New("rejected"), false, rpc.SessionFSErrorCodeUNKNOWN, "rejected"}, + {"partial", &SessionFSWriteFailure{Err: errors.New("disk full")}, true, rpc.SessionFSErrorCodeUNKNOWN, "disk full"}, + {"wrapped", fmt.Errorf("write failed: %w", &SessionFSWriteFailure{Err: os.ErrNotExist}), true, rpc.SessionFSErrorCodeENOENT, "write failed: file does not exist"}, + {"nil cause", &SessionFSWriteFailure{}, true, rpc.SessionFSErrorCodeUNKNOWN, "session filesystem write failed after changing the target"}, + {"nil marker", (*SessionFSWriteFailure)(nil), false, rpc.SessionFSErrorCodeUNKNOWN, "session filesystem write failed after changing the target"}, + } { + t.Run(tc.name, func(t *testing.T) { + adapter := newSessionFSAdapter(failingWriteProvider{err: tc.err}) + wire, err := adapter.WriteFile(&rpc.SessionFSWriteFileRequest{Path: "/file", Content: "data"}) + if err != nil || wire == nil { + t.Fatalf("WriteFile = %v, %v; want wire error", wire, err) + } + if wire.Code != tc.wantCode { + t.Errorf("code = %v; want %v", wire.Code, tc.wantCode) + } + if wire.Message == nil || *wire.Message != tc.wantMessage { + t.Errorf("message = %v; want %q", wire.Message, tc.wantMessage) + } + gotChanged := wire.WriteChanged != nil && *wire.WriteChanged + if gotChanged != tc.wantChanged || !tc.wantChanged && wire.WriteChanged != nil { + t.Errorf("writeChanged = %v; want %v", wire.WriteChanged, tc.wantChanged) + } + }) + } +} + +func TestSessionFSWriteFailureDoesNotMarkAppend(t *testing.T) { + adapter := newSessionFSAdapter(failingWriteProvider{ + err: &SessionFSWriteFailure{Err: errors.New("append failed")}, + }) + wire, err := adapter.AppendFile(&rpc.SessionFSAppendFileRequest{Path: "/file", Content: "data"}) + if err != nil || wire == nil || wire.WriteChanged != nil { + t.Fatalf("AppendFile = %v, %v; want unmarked wire error", wire, err) + } +} diff --git a/go/session_test.go b/go/session_test.go index 60e51308e6..b66643544b 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" "io" "strconv" @@ -184,6 +185,321 @@ func TestSession_SetAutoTierSendsExplicitNull(t *testing.T) { } } +func TestSession_SetToolsSendsPayloadAndInstallsHandlers(t *testing.T) { + session, server := newSetToolsTestSession(t) + handler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "handled"}, nil + } + tools := []Tool{ + { + Name: "lookup_fruit", + Parameters: map[string]any{"type": "object"}, + OverridesBuiltInTool: true, + SkipPermission: true, + IsTerminal: true, + Defer: ToolDeferNever, + Metadata: map[string]any{"x-test": "metadata"}, + Handler: handler, + }, + { + Name: "declaration_only", + Description: "No handler", + }, + } + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), tools) + }() + + request := server.expectRequest(t, "session.tools.set") + if request.Params["sessionId"] != "session-1" { + t.Fatalf("sessionId = %v, want session-1", request.Params["sessionId"]) + } + wireTools, ok := request.Params["tools"].([]any) + if !ok || len(wireTools) != 2 { + t.Fatalf("tools = %#v, want two tools", request.Params["tools"]) + } + first, ok := wireTools[0].(map[string]any) + if !ok { + t.Fatalf("first tool = %#v, want object", wireTools[0]) + } + if first["name"] != "lookup_fruit" { + t.Fatalf("name = %v, want lookup_fruit", first["name"]) + } + if description, ok := first["description"].(string); !ok || description != "" { + t.Fatalf("description = %#v, want empty string", first["description"]) + } + for _, key := range []string{"overridesBuiltInTool", "skipPermission", "isTerminal"} { + if first[key] != true { + t.Fatalf("%s = %v, want true", key, first[key]) + } + } + if first["defer"] != "never" { + t.Fatalf("defer = %v, want never", first["defer"]) + } + if _, ok := first["parameters"].(map[string]any); !ok { + t.Fatalf("parameters = %#v, want object", first["parameters"]) + } + if _, ok := first["metadata"].(map[string]any); !ok { + t.Fatalf("metadata = %#v, want object", first["metadata"]) + } + + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + if got, ok := session.getToolHandler("lookup_fruit"); !ok || got == nil { + t.Fatal("lookup_fruit handler was not installed") + } + if _, ok := session.getToolHandler("declaration_only"); ok { + t.Fatal("declaration-only tool installed a handler") + } +} + +func TestSession_SetToolsSwitchesHandlersAfterAccepted(t *testing.T) { + session, server := newSetToolsTestSession(t) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + + handler, ok := session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing while replacement RPC is pending") + } + result, err := handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "old" { + t.Fatalf("pending replacement used %q, %v; want old handler", result.TextResultForLLM, err) + } + + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + handler, ok = session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing after accepted replacement") + } + result, err = handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "new" { + t.Fatalf("accepted replacement used %q, %v; want new handler", result.TextResultForLLM, err) + } +} + +func TestSession_SetToolsRunningCallsFinishOnOriginalHandler(t *testing.T) { + session, server := newSetToolsTestSession(t) + started := make(chan struct{}) + release := make(chan struct{}) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + close(started) + <-release + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + session.startExternalTool(&ExternalToolRequestedData{ + RequestID: "request-1", + SessionID: "session-1", + ToolCallID: "tool-call-1", + ToolName: "lookup", + }) + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("tool handler did not start") + } + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + close(release) + + completion := server.expectRequest(t, "session.tools.handlePendingToolCall") + result, ok := completion.Params["result"].(map[string]any) + if !ok { + t.Fatalf("completion result = %#v, want object", completion.Params["result"]) + } + if result["textResultForLlm"] != "old" { + t.Fatalf("completion result = %#v, want old handler result", result) + } + server.respond(t, completion.ID, map[string]any{}) +} + +func TestSession_SetToolsRejectionLeavesHandlersUnchanged(t *testing.T) { + session, server := newSetToolsTestSession(t) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + server.respondError(t, request.ID, -32602, "invalid tool name") + if err := awaitSetTools(t, errCh); err == nil { + t.Fatal("SetTools succeeded; want rejection error") + } + + handler, ok := session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing after rejected replacement") + } + result, err := handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "old" { + t.Fatalf("rejected replacement used %q, %v; want old handler", result.TextResultForLLM, err) + } +} + +func TestSession_SetToolsEmptySetRemovesHandlers(t *testing.T) { + session, server := newSetToolsTestSession(t) + session.registerTools([]Tool{{Name: "lookup", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + }}}) + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{}) + }() + request := server.expectRequest(t, "session.tools.set") + wireTools, ok := request.Params["tools"].([]any) + if !ok || len(wireTools) != 0 { + t.Fatalf("tools = %#v, want empty list", request.Params["tools"]) + } + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + if _, ok := session.getToolHandler("lookup"); ok { + t.Fatal("handler remained after empty replacement") + } +} + +func TestSession_SetToolsConcurrentCallsAreSerialized(t *testing.T) { + session, server := newSetToolsTestSession(t) + + firstErr := make(chan error, 1) + go func() { + firstErr <- session.SetTools(context.Background(), []Tool{{Name: "first", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + first := server.expectRequest(t, "session.tools.set") + + secondErr := make(chan error, 1) + go func() { + secondErr <- session.SetTools(context.Background(), []Tool{{Name: "second", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + + server.expectNoRequest(t) + server.respondError(t, first.ID, -32602, "first rejected") + if err := awaitSetTools(t, firstErr); err == nil { + t.Fatal("first SetTools succeeded; want rejection error") + } + + second := server.expectRequest(t, "session.tools.set") + server.respond(t, second.ID, map[string]any{}) + if err := awaitSetTools(t, secondErr); err != nil { + t.Fatalf("second SetTools failed: %v", err) + } + if _, ok := session.getToolHandler("first"); ok { + t.Fatal("first handler installed after rejected replacement") + } + if _, ok := session.getToolHandler("second"); !ok { + t.Fatal("second handler was not installed after earlier rejection") + } +} + +func TestSession_SetToolsCancelledAfterSendStillInstallsAcceptedHandlers(t *testing.T) { + session, server := newSetToolsTestSession(t) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(ctx, []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + cancel() + if err := awaitSetTools(t, errCh); !errors.Is(err, context.Canceled) { + t.Fatalf("SetTools error = %v, want context.Canceled", err) + } + handler, ok := session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing before accepted replacement resolves") + } + result, err := handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "old" { + t.Fatalf("cancelled pending replacement used %q, %v; want old handler", result.TextResultForLLM, err) + } + + server.respond(t, request.ID, map[string]any{}) + waitForToolHandlerResult(t, session, "lookup", "new") +} + +func TestSession_SetToolsAlreadyCancelledWhileBlockedDoesNotSend(t *testing.T) { + session, server := newSetToolsTestSession(t) + firstErr := make(chan error, 1) + go func() { + firstErr <- session.SetTools(context.Background(), []Tool{{Name: "first", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + first := server.expectRequest(t, "session.tools.set") + + ctx, cancel := context.WithCancel(context.Background()) + secondErr := make(chan error, 1) + go func() { + secondErr <- session.SetTools(ctx, []Tool{{Name: "second", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + cancel() + if err := awaitSetTools(t, secondErr); !errors.Is(err, context.Canceled) { + t.Fatalf("blocked SetTools error = %v, want context.Canceled", err) + } + server.expectNoRequest(t) + + server.respond(t, first.ID, map[string]any{}) + if err := awaitSetTools(t, firstErr); err != nil { + t.Fatalf("first SetTools failed: %v", err) + } + server.expectNoRequest(t) + if _, ok := session.getToolHandler("second"); ok { + t.Fatal("cancelled blocked replacement installed a handler") + } +} + func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() @@ -521,6 +837,169 @@ func readTestJSONRPCFrame(r io.Reader) ([]byte, error) { return data, err } +type setToolsJSONRPCRequest struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` +} + +type setToolsTestServer struct { + stdinR *io.PipeReader + stdinW *io.PipeWriter + stdoutR *io.PipeReader + stdoutW *io.PipeWriter + client *jsonrpc2.Client + mu sync.Mutex + request chan setToolsJSONRPCRequest + errs chan error +} + +func newSetToolsTestSession(t *testing.T) (*Session, *setToolsTestServer) { + t.Helper() + + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + client := jsonrpc2.NewClient(stdinW, stdoutR) + client.Start() + + server := &setToolsTestServer{ + stdinR: stdinR, + stdinW: stdinW, + stdoutR: stdoutR, + stdoutW: stdoutW, + client: client, + request: make(chan setToolsJSONRPCRequest, 16), + errs: make(chan error, 1), + } + go server.readRequests() + t.Cleanup(func() { + client.Stop() + stdinR.Close() + stdinW.Close() + stdoutR.Close() + stdoutW.Close() + }) + + session := &Session{ + SessionID: "session-1", + client: client, + toolHandlers: make(map[string]ToolHandler), + RPC: rpc.NewSessionRPC(client, "session-1"), + } + return session, server +} + +func (s *setToolsTestServer) readRequests() { + for { + frame, err := readTestJSONRPCFrame(s.stdinR) + if err != nil { + return + } + + var request setToolsJSONRPCRequest + if err := json.Unmarshal(frame, &request); err != nil { + select { + case s.errs <- err: + default: + } + return + } + s.request <- request + } +} + +func (s *setToolsTestServer) expectRequest(t *testing.T, method string) setToolsJSONRPCRequest { + t.Helper() + select { + case request := <-s.request: + if request.Method != method { + t.Fatalf("method = %s, want %s", request.Method, method) + } + return request + case err := <-s.errs: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatalf("timed out waiting for %s request", method) + } + return setToolsJSONRPCRequest{} +} + +func (s *setToolsTestServer) expectNoRequest(t *testing.T) { + t.Helper() + select { + case request := <-s.request: + t.Fatalf("unexpected request while replacement should be blocked: %s", request.Method) + case err := <-s.errs: + t.Fatal(err) + case <-time.After(100 * time.Millisecond): + } +} + +func (s *setToolsTestServer) respond(t *testing.T, id json.RawMessage, result any) { + t.Helper() + s.writeResponse(t, map[string]any{ + "jsonrpc": "2.0", + "id": id, + "result": result, + }) +} + +func (s *setToolsTestServer) respondError(t *testing.T, id json.RawMessage, code int, message string) { + t.Helper() + s.writeResponse(t, map[string]any{ + "jsonrpc": "2.0", + "id": id, + "error": map[string]any{ + "code": code, + "message": message, + }, + }) +} + +func (s *setToolsTestServer) writeResponse(t *testing.T, response map[string]any) { + t.Helper() + data, err := json.Marshal(response) + if err != nil { + t.Fatal(err) + } + s.mu.Lock() + defer s.mu.Unlock() + if _, err := fmt.Fprintf(s.stdoutW, "Content-Length: %d\r\n\r\n%s", len(data), data); err != nil { + t.Fatal(err) + } +} + +func awaitSetTools(t *testing.T, errCh <-chan error) error { + t.Helper() + select { + case err := <-errCh: + return err + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for SetTools") + } + return nil +} + +func waitForToolHandlerResult(t *testing.T, session *Session, name, want string) { + t.Helper() + deadline := time.After(2 * time.Second) + for { + handler, ok := session.getToolHandler(name) + if ok { + result, err := handler(ToolInvocation{}) + if err == nil && result.TextResultForLLM == want { + return + } + } + select { + case <-deadline: + t.Fatalf("timed out waiting for %s handler to return %q", name, want) + default: + time.Sleep(time.Millisecond) + } + } +} + func TestSession_SendAndWaitSkipsSubagentAndAutopilotContinuationIdle(t *testing.T) { t.Run("with root reply", func(t *testing.T) { checkSendAndWaitSkipsSubagentAndAutopilotContinuationIdle(t, true) diff --git a/go/skill_provider_test.go b/go/skill_provider_test.go new file mode 100644 index 0000000000..957b99aa8f --- /dev/null +++ b/go/skill_provider_test.go @@ -0,0 +1,700 @@ +package copilot + +import ( + "context" + "encoding/json" + "errors" + "log" + "strings" + "sync" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +type testSkillProvider struct { + mu sync.Mutex + skills []rpc.SkillProviderDescriptor + markdown map[string]string + listErr error + readErr error + calls []string +} + +func (p *testSkillProvider) ListSkills(context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "list") + if p.listErr != nil { + return nil, p.listErr + } + return p.skills, nil +} + +func (p *testSkillProvider) ReadSkill(_ context.Context, name string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "read:"+name) + if p.readErr != nil { + return "", p.readErr + } + markdown, ok := p.markdown[name] + if !ok { + return "", ErrSkillNotFound + } + return markdown, nil +} + +func (p *testSkillProvider) snapshotCalls() []string { + p.mu.Lock() + defer p.mu.Unlock() + return append([]string(nil), p.calls...) +} + +var reviewSkillDescriptor = rpc.SkillProviderDescriptor{ + Name: "review", + Description: "Reviews code", +} + +func newTestSkillProvider() *testSkillProvider { + return &testSkillProvider{ + skills: []rpc.SkillProviderDescriptor{reviewSkillDescriptor}, + markdown: map[string]string{"review": "Review carefully."}, + } +} + +func TestSkillProviderSessionPayloads(t *testing.T) { + t.Run("omits flag without provider and sends flag with provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + createParams := make(chan json.RawMessage, 2) + resumeParams := make(chan json.RawMessage, 2) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{}); err != nil { + t.Fatalf("CreateSession without provider failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "resume-without-provider", &ResumeSessionConfig{}); err != nil { + t.Fatalf("ResumeSession without provider failed: %v", err) + } + provider := newTestSkillProvider() + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("CreateSession with provider failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "resume-with-provider", &ResumeSessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("ResumeSession with provider failed: %v", err) + } + + assertSkillProviderFlag(t, <-createParams, false) + assertSkillProviderFlag(t, <-resumeParams, false) + assertSkillProviderFlag(t, <-createParams, true) + assertSkillProviderFlag(t, <-resumeParams, true) + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called during open: %v", got) + } + }) + + t.Run("keeps empty mode enableSkills default with provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + options: ClientOptions{Mode: ModeEmpty}, + } + + createParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + server.SetRequestHandler("session.options.update", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{}`), nil + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{ + AvailableTools: []string{}, + SkillProvider: newTestSkillProvider(), + }); err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(<-createParams, &payload); err != nil { + t.Fatal(err) + } + if payload["enableSkills"] != false { + t.Fatalf("enableSkills = %v, want false", payload["enableSkills"]) + } + if payload["hasSkillProvider"] != true { + t.Fatalf("hasSkillProvider = %v, want true", payload["hasSkillProvider"]) + } + }) +} + +func TestSkillProviderServesEarlyCallbacksDuringOpen(t *testing.T) { + for _, method := range []string{"session.create", "session.resume"} { + t.Run(method, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + + earlyResult := make(chan rpc.SkillProviderListResult, 1) + server.SetRequestHandler(method, func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + raw, err := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": sessionID}) + if err != nil { + if rpcErr, ok := err.(*jsonrpc2.Error); ok { + return nil, rpcErr + } + return nil, &jsonrpc2.Error{Code: -32603, Message: err.Error()} + } + var result rpc.SkillProviderListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: err.Error()} + } + earlyResult <- result + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + + provider := newTestSkillProvider() + if method == "session.create" { + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + } else if _, err := client.ResumeSession(t.Context(), "early-resume", &ResumeSessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + + got := <-earlyResult + if len(got.Skills) != 1 || got.Skills[0].Name != "review" { + t.Fatalf("early list result = %+v", got) + } + }) + } +} + +func TestSkillProviderRejectsCloudBeforeConnect(t *testing.T) { + provider := newTestSkillProvider() + client := &Client{} + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Cloud: &CloudSessionOptions{}, + SkillProvider: provider, + }) + + if err == nil || err.Error() != "Skill providers are not supported for cloud sessions." { + t.Fatalf("CreateSession error = %v", err) + } + if client.client != nil { + t.Fatal("client connected before rejecting cloud skill provider") + } + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called: %v", got) + } +} + +func TestSkillProviderDispatch(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + + provider := &testSkillProvider{ + skills: []rpc.SkillProviderDescriptor{ + reviewSkillDescriptor, + { + Name: "deploy", + Description: "Deploys the service", + UserInvocable: Bool(false), + DisableModelInvocation: Bool(true), + ArgumentHint: String("[environment]"), + }, + }, + markdown: map[string]string{"deploy": "# deploy"}, + } + session := newSession("dispatch-session", rpcClient, "", false) + session.registerSkillProvider(provider) + client.sessions[session.SessionID] = session + + listRaw, rpcErr := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + if rpcErr != nil { + t.Fatalf("skillProvider.list failed: %v", rpcErr) + } + var listResult rpc.SkillProviderListResult + if err := json.Unmarshal(listRaw, &listResult); err != nil { + t.Fatal(err) + } + if len(listResult.Skills) != 2 || listResult.Skills[1].Name != "deploy" { + t.Fatalf("list result = %+v", listResult) + } + + readRaw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "deploy", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + var readResult rpc.SkillProviderReadResult + if err := json.Unmarshal(readRaw, &readResult); err != nil { + t.Fatal(err) + } + if readResult.Markdown == nil { + t.Fatal("markdown = nil, want # deploy") + } + if *readResult.Markdown != "# deploy" { + t.Fatalf("markdown = %q, want # deploy", *readResult.Markdown) + } + if got := provider.snapshotCalls(); strings.Join(got, ",") != "list,read:deploy" { + t.Fatalf("provider calls = %v", got) + } +} + +func TestSkillProviderOmitsUnsetOptionalDescriptorFields(t *testing.T) { + data, err := json.Marshal(rpc.SkillProviderListResult{ + Skills: []rpc.SkillProviderDescriptor{{Name: "review", Description: "Reviews code"}}, + }) + if err != nil { + t.Fatal(err) + } + var decoded map[string][]map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + skill := decoded["skills"][0] + for _, key := range []string{"argumentHint", "userInvocable", "disableModelInvocation"} { + if _, ok := skill[key]; ok { + t.Fatalf("%s should be omitted when unset: %s", key, data) + } + } +} + +func TestSkillProviderEmptyListBecomesEmptyArray(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + session := newSession("empty-list-session", rpcClient, "", false) + session.registerSkillProvider(&testSkillProvider{}) + client.sessions[session.SessionID] = session + + raw, rpcErr := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + if rpcErr != nil { + t.Fatalf("skillProvider.list failed: %v", rpcErr) + } + if string(raw) != `{"skills":[]}` { + t.Fatalf("list response = %s, want empty skills array", raw) + } +} + +func TestSkillProviderErrorEnvelopes(t *testing.T) { + t.Run("read not found returns null markdown", func(t *testing.T) { + _, server, sessionID := skillProviderErrorHarness(t, newTestSkillProvider()) + raw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": sessionID, + "name": "missing", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + if string(raw) != `{"markdown":null}` { + t.Fatalf("read response = %s, want null markdown", raw) + } + var result rpc.SkillProviderReadResult + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatal(err) + } + if result.Markdown != nil { + t.Fatalf("markdown = %q, want nil", *result.Markdown) + } + }) + + for _, method := range []string{"skillProvider.list", "skillProvider.read"} { + t.Run(method+" provider failure", func(t *testing.T) { + output := captureSkillProviderLog(t) + secret := errors.New("db-password-in-error") + provider := newTestSkillProvider() + operation := "listSkills" + if method == "skillProvider.list" { + provider.listErr = secret + } else { + provider.readErr = secret + operation = "readSkill" + } + _, server, sessionID := skillProviderErrorHarness(t, provider) + params := map[string]any{"sessionId": sessionID} + wantMessage := "Skill provider " + operation + " failed" + if method == "skillProvider.read" { + params["name"] = "review" + } + + rpcErr := requestSkillProviderError(t, server, method, params) + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage { + t.Fatalf("error = %+v, want generic message %q", rpcErr, wantMessage) + } + if rpcErr.Data != nil { + t.Fatalf("generic provider error data = %s, want omitted", rpcErr.Data) + } + if strings.Contains(rpcErr.Message, secret.Error()) { + t.Fatalf("provider error leaked secret: %q", rpcErr.Message) + } + wantLog := "skill provider " + operation + " failed: session_id=" + sessionID + " error=" + secret.Error() + if got := output.text(); !strings.Contains(got, wantLog) { + t.Fatalf("log = %q, want it to contain %q", got, wantLog) + } + }) + } + + t.Run("not-found classification panic", func(t *testing.T) { + captureSkillProviderLog(t) + provider := newTestSkillProvider() + provider.readErr = panickyIsError{} + _, server, sessionID := skillProviderErrorHarness(t, provider) + + rpcErr := requestSkillProviderError(t, server, "skillProvider.read", map[string]any{ + "sessionId": sessionID, + "name": "review", + }) + if rpcErr.Code != -32603 || rpcErr.Message != "Skill provider readSkill failed" || rpcErr.Data != nil { + t.Fatalf("error = %+v, want generic readSkill failure", rpcErr) + } + }) + + t.Run("unknown session", func(t *testing.T) { + _, server, _ := skillProviderErrorHarness(t, newTestSkillProvider()) + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": "missing"}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: missing") + }) + + for _, method := range []string{"skillProvider.list", "skillProvider.read"} { + t.Run(method+" provider panic", func(t *testing.T) { + output := captureSkillProviderLog(t) + _, server, sessionID := skillProviderErrorHarness(t, panickingSkillProvider{}) + params := map[string]any{"sessionId": sessionID} + wantMessage := "Skill provider listSkills failed" + if method == "skillProvider.read" { + params["name"] = "review" + wantMessage = "Skill provider readSkill failed" + } + + rpcErr := requestSkillProviderError(t, server, method, params) + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage || rpcErr.Data != nil { + t.Fatalf("error = %+v, want generic message %q without data", rpcErr, wantMessage) + } + if got := output.text(); !strings.Contains(got, "panic=db-password-in-panic") { + t.Fatalf("log = %q, want the recovered panic value", got) + } + }) + } + + t.Run("no provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + session := newSession("no-provider-session", rpcClient, "", false) + client.sessions[session.SessionID] = session + + rpcErr := requestSkillProviderError(t, server, "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "review", + }) + assertSkillProviderError(t, rpcErr, "No skill provider for session: no-provider-session") + }) +} + +func TestSkillProviderTeardown(t *testing.T) { + t.Run("disconnect clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "disconnect-session") + + if err := session.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: disconnect-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after disconnect: %v", got) + } + _ = client + }) + + t.Run("delete clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "delete-session") + server.SetRequestHandler("session.delete", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + if err := client.DeleteSession(t.Context(), session.SessionID); err != nil { + t.Fatalf("DeleteSession failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: delete-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after delete: %v", got) + } + }) + + t.Run("connection loss clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "closed-session") + + client.handleConnectionClose() + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: closed-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after connection loss: %v", got) + } + }) + + t.Run("failed create cleanup", func(t *testing.T) { + provider := newTestSkillProvider() + var sessionID string + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID = sessionIDFromParams(t, params) + return nil, &jsonrpc2.Error{Code: -32000, Message: "create failed"} + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err == nil { + t.Fatal("CreateSession succeeded unexpectedly") + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": sessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: "+sessionID) + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after failed create: %v", got) + } + }) + + t.Run("failed resume cleanup", func(t *testing.T) { + provider := newTestSkillProvider() + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.resume", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "resume failed"} + }) + + if _, err := client.ResumeSession(t.Context(), "failed-resume", &ResumeSessionConfig{SkillProvider: provider}); err == nil { + t.Fatal("ResumeSession succeeded unexpectedly") + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": "failed-resume"}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: failed-resume") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after failed resume: %v", got) + } + }) +} + +func TestSkillProviderResumeRebinding(t *testing.T) { + t.Run("serves replacement provider", func(t *testing.T) { + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + original := newTestSkillProvider() + replacement := newTestSkillProvider() + replacement.markdown = map[string]string{"review": "replacement"} + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: "rebind-session", + SkillProvider: original, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{SkillProvider: replacement}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + raw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "review", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + var result rpc.SkillProviderReadResult + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatal(err) + } + if result.Markdown == nil { + t.Fatal("markdown = nil, want replacement") + } + if *result.Markdown != "replacement" { + t.Fatalf("markdown = %q, want replacement", *result.Markdown) + } + if got := original.snapshotCalls(); len(got) != 0 { + t.Fatalf("original provider was called after resume: %v", got) + } + if got := replacement.snapshotCalls(); strings.Join(got, ",") != "read:review" { + t.Fatalf("replacement provider calls = %v", got) + } + }) + + t.Run("resume without provider unbinds previous provider", func(t *testing.T) { + client, server, original, session := newSkillProviderOpenSession(t, "unbind-session") + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + + if _, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: unbind-session") + if got := original.snapshotCalls(); len(got) != 0 { + t.Fatalf("original provider was called after unbound resume: %v", got) + } + }) +} + +type panickingSkillProvider struct{} + +func (panickingSkillProvider) ListSkills(context.Context) ([]rpc.SkillProviderDescriptor, error) { + panic("db-password-in-panic") +} + +func (panickingSkillProvider) ReadSkill(context.Context, string) (string, error) { + panic("db-password-in-panic") +} + +// panickyIsError panics when errors.Is asks whether it matches a target. +type panickyIsError struct{} + +func (panickyIsError) Error() string { return "panicky" } + +func (panickyIsError) Is(error) bool { panic("classification panicked") } + +func captureSkillProviderLog(t *testing.T) *ahpTestLog { + t.Helper() + output := new(ahpTestLog) + previous := log.Writer() + log.SetOutput(output) + t.Cleanup(func() { log.SetOutput(previous) }) + return output +} + +func newSkillProviderClient(t *testing.T) (*Client, *jsonrpc2.Client) { + t.Helper() + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + return client, server +} + +func newSkillProviderOpenSession(t *testing.T, sessionID string) (*Client, *jsonrpc2.Client, *testSkillProvider, *Session) { + t.Helper() + client, server := newSkillProviderClient(t) + provider := newTestSkillProvider() + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + gotSessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + gotSessionID + `"}`), nil + }) + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: sessionID, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + return client, server, provider, session +} + +func skillProviderErrorHarness(t *testing.T, provider SkillProvider) (*Client, *jsonrpc2.Client, string) { + t.Helper() + client, server := newSkillProviderClient(t) + session := newSession("error-session", client.client, "", false) + session.registerSkillProvider(provider) + client.sessions[session.SessionID] = session + return client, server, session.SessionID +} + +func requestSkillProviderError(t *testing.T, server *jsonrpc2.Client, method string, params map[string]any) *jsonrpc2.Error { + t.Helper() + _, err := server.Request(t.Context(), method, params) + if err == nil { + t.Fatalf("%s succeeded unexpectedly", method) + } + rpcErr, ok := err.(*jsonrpc2.Error) + if !ok { + t.Fatalf("%s error = %T %v, want *jsonrpc2.Error", method, err, err) + } + return rpcErr +} + +func assertSkillProviderError(t *testing.T, rpcErr *jsonrpc2.Error, wantMessage string) { + t.Helper() + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage { + t.Fatalf("error = %+v, want code -32603 message %q", rpcErr, wantMessage) + } + if rpcErr.Data != nil { + t.Fatalf("error data = %s, want omitted", rpcErr.Data) + } +} + +func assertSkillProviderFlag(t *testing.T, params json.RawMessage, wantPresent bool) { + t.Helper() + var payload map[string]any + if err := json.Unmarshal(params, &payload); err != nil { + t.Fatalf("failed to decode request params: %v", err) + } + got, present := payload["hasSkillProvider"] + if !wantPresent { + if present { + t.Fatalf("hasSkillProvider = %v, want omitted", got) + } + return + } + if got != true { + t.Fatalf("hasSkillProvider = %v, want true", got) + } + if _, present := payload["skillProvider"]; present { + t.Fatalf("skillProvider callback was serialized: %v", payload["skillProvider"]) + } +} diff --git a/go/subagent_hooks_test.go b/go/subagent_hooks_test.go new file mode 100644 index 0000000000..8f68cc49d6 --- /dev/null +++ b/go/subagent_hooks_test.go @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package copilot + +import ( + "encoding/json" + "reflect" + "testing" + "time" +) + +func TestSession_SubagentStartHook(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + session.SessionID = "parent-session" + + var captured SubagentStartHookInput + session.registerHooks(&SessionHooks{ + OnSubagentStart: func(input SubagentStartHookInput, invocation HookInvocation) (*SubagentStartHookOutput, error) { + captured = input + if invocation.SessionID != "parent-session" { + t.Errorf("invocation session ID = %q, want parent-session", invocation.SessionID) + } + return &SubagentStartHookOutput{AdditionalContext: "read only the requested file"}, nil + }, + }) + + raw := json.RawMessage(`{"sessionId":"parent-session","timestamp":1700000000123,"cwd":"C:\\work","transcriptPath":"C:\\transcript.jsonl","agentName":"explore","agentDisplayName":"Explore Agent","agentDescription":"Find files"}`) + output, err := session.handleHooksInvoke("subagentStart", raw) + if err != nil { + t.Fatalf("subagentStart dispatch failed: %v", err) + } + if captured.SessionID != "parent-session" || !captured.Timestamp.Equal(time.UnixMilli(1700000000123)) || + captured.WorkingDirectory != `C:\work` || captured.TranscriptPath != `C:\transcript.jsonl` || + captured.AgentName != "explore" || captured.AgentDisplayName != "Explore Agent" || + captured.AgentDescription != "Find files" { + t.Errorf("subagentStart input = %+v", captured) + } + assertHookJSON(t, captured, raw) + assertHookJSON(t, output, json.RawMessage(`{"additionalContext":"read only the requested file"}`)) + + session.registerHooks(&SessionHooks{}) + output, err = session.handleHooksInvoke("subagentStart", raw) + if err != nil || output != nil { + t.Errorf("unregistered subagentStart = (%v, %v), want (nil, nil)", output, err) + } +} + +func TestSession_SubagentStopHook(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + session.SessionID = "parent-session" + + raw := json.RawMessage(`{"sessionId":"parent-session","timestamp":1700000000456,"cwd":"C:\\work","transcriptPath":"C:\\transcript.jsonl","agentId":"read-file","agentType":"explore","agentName":"explore","agentDisplayName":"Explore Agent","agentDescription":"Find files","stopReason":"end_turn","response":"Hello from subagent test!"}`) + tests := []struct { + name string + output *SubagentStopHookOutput + want json.RawMessage + }{ + { + name: "block with a reason", + output: &SubagentStopHookOutput{Decision: "block", Reason: "Read the file again"}, + want: json.RawMessage(`{"decision":"block","reason":"Read the file again"}`), + }, + { + name: "replace the response", + output: &SubagentStopHookOutput{ModifiedResponse: String("Changed result")}, + want: json.RawMessage(`{"modifiedResponse":"Changed result"}`), + }, + { + name: "replace with an empty response", + output: &SubagentStopHookOutput{ModifiedResponse: String("")}, + want: json.RawMessage(`{"modifiedResponse":""}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var captured SubagentStopHookInput + session.registerHooks(&SessionHooks{ + OnSubagentStop: func(input SubagentStopHookInput, invocation HookInvocation) (*SubagentStopHookOutput, error) { + captured = input + if invocation.SessionID != "parent-session" { + t.Errorf("invocation session ID = %q, want parent-session", invocation.SessionID) + } + return tt.output, nil + }, + }) + + output, err := session.handleHooksInvoke("subagentStop", raw) + if err != nil { + t.Fatalf("subagentStop dispatch failed: %v", err) + } + if captured.SessionID != "parent-session" || !captured.Timestamp.Equal(time.UnixMilli(1700000000456)) || + captured.WorkingDirectory != `C:\work` || captured.TranscriptPath != `C:\transcript.jsonl` || + captured.AgentID != "read-file" || captured.AgentType != "explore" || + captured.AgentName != "explore" || captured.AgentDisplayName != "Explore Agent" || + captured.AgentDescription != "Find files" || captured.StopReason != "end_turn" || + captured.Response != "Hello from subagent test!" { + t.Errorf("subagentStop input = %+v", captured) + } + assertHookJSON(t, captured, raw) + assertHookJSON(t, output, tt.want) + }) + } + + session.registerHooks(&SessionHooks{}) + output, err := session.handleHooksInvoke("subagentStop", raw) + if err != nil || output != nil { + t.Errorf("unregistered subagentStop = (%v, %v), want (nil, nil)", output, err) + } +} + +func assertHookJSON(t *testing.T, value any, expected json.RawMessage) { + t.Helper() + gotJSON, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal %T: %v", value, err) + } + var got, want any + if err := json.Unmarshal(gotJSON, &got); err != nil { + t.Fatalf("decode marshaled %T: %v", value, err) + } + if err := json.Unmarshal(expected, &want); err != nil { + t.Fatalf("decode expected JSON: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("JSON for %T = %s, want %s", value, gotJSON, expected) + } +} diff --git a/go/types.go b/go/types.go index 3ee067a3f1..dc478bbdbd 100644 --- a/go/types.go +++ b/go/types.go @@ -3,6 +3,7 @@ package copilot import ( "context" "encoding/json" + "errors" "time" "github.com/github/copilot-sdk/go/rpc" @@ -138,6 +139,10 @@ type ClientOptions struct { // discovered by the runtime. When non-nil, Start registers the provider // before any sessions can be created. ExtensionLaunchProvider ExtensionLaunchProvider + // InstallationConfirmationHandler receives connection-global human reviews + // for experimental `installations.confirm` callbacks. It does not enable + // installation capabilities or perform any runtime registration RPC. + InstallationConfirmationHandler InstallationConfirmationHandler // LogLevel for the runtime. When empty (the default), the runtime // uses its own default level; the SDK does not pass --log-level. // Recognized values: "none", "error", "warning", "info", "debug", "all". @@ -369,12 +374,14 @@ const ( SectionToolInstructions = "tool_instructions" // SectionCustomInstructions covers repository and organization custom instructions. SectionCustomInstructions = "custom_instructions" - // SectionRuntimeInstructions targets runtime-provided context and instructions - // (e.g. system notifications, memories, workspace context, mode-specific instructions, - // content-exclusion policy). + // SectionRuntimeInstructions targets runtime-provided system-prompt context and + // instructions, such as system notifications, memories, workspace context, and + // content-exclusion policy. Mode-specific instructions can travel in transition + // messages instead. SectionRuntimeInstructions = "runtime_instructions" // SectionLastInstructions covers end-of-prompt instructions: parallel tool calling, - // persistence, and task completion. + // persistence, task completion, and configured subagent-model guidance when the + // task tool is available. SectionLastInstructions = "last_instructions" ) @@ -1023,6 +1030,98 @@ type AgentStopHookOutput struct { // AgentStopHandler handles agent-stop hook invocations. type AgentStopHandler func(input AgentStopHookInput, invocation HookInvocation) (*AgentStopHookOutput, error) +// SubagentStartHookInput is the input before a subagent's first turn. +type SubagentStartHookInput struct { + SessionID string `json:"sessionId"` + Timestamp time.Time `json:"-"` + WorkingDirectory string `json:"cwd"` + TranscriptPath string `json:"transcriptPath"` + AgentName string `json:"agentName"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AgentDescription string `json:"agentDescription,omitempty"` +} + +// MarshalJSON implements json.Marshaler, emitting Timestamp as Unix milliseconds. +func (h SubagentStartHookInput) MarshalJSON() ([]byte, error) { + type alias SubagentStartHookInput + return json.Marshal(&struct { + Timestamp int64 `json:"timestamp"` + alias + }{Timestamp: h.Timestamp.UnixMilli(), alias: alias(h)}) +} + +// UnmarshalJSON implements json.Unmarshaler, parsing Timestamp from Unix milliseconds. +func (h *SubagentStartHookInput) UnmarshalJSON(data []byte) error { + type alias SubagentStartHookInput + aux := &struct { + Timestamp int64 `json:"timestamp"` + *alias + }{alias: (*alias)(h)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + h.Timestamp = time.UnixMilli(aux.Timestamp) + return nil +} + +// SubagentStartHookOutput provides context prepended to the subagent's initial prompt. +type SubagentStartHookOutput struct { + AdditionalContext string `json:"additionalContext,omitempty"` +} + +// SubagentStartHandler handles subagent-start hook invocations. +type SubagentStartHandler func(input SubagentStartHookInput, invocation HookInvocation) (*SubagentStartHookOutput, error) + +// SubagentStopHookInput is the input after a subagent completes a turn. +type SubagentStopHookInput struct { + SessionID string `json:"sessionId"` + Timestamp time.Time `json:"-"` + WorkingDirectory string `json:"cwd"` + TranscriptPath string `json:"transcriptPath"` + AgentID string `json:"agentId,omitempty"` + AgentType string `json:"agentType"` + AgentName string `json:"agentName"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AgentDescription string `json:"agentDescription,omitempty"` + StopReason string `json:"stopReason"` + Response string `json:"response"` +} + +// MarshalJSON implements json.Marshaler, emitting Timestamp as Unix milliseconds. +func (h SubagentStopHookInput) MarshalJSON() ([]byte, error) { + type alias SubagentStopHookInput + return json.Marshal(&struct { + Timestamp int64 `json:"timestamp"` + alias + }{Timestamp: h.Timestamp.UnixMilli(), alias: alias(h)}) +} + +// UnmarshalJSON implements json.Unmarshaler, parsing Timestamp from Unix milliseconds. +func (h *SubagentStopHookInput) UnmarshalJSON(data []byte) error { + type alias SubagentStopHookInput + aux := &struct { + Timestamp int64 `json:"timestamp"` + *alias + }{alias: (*alias)(h)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + h.Timestamp = time.UnixMilli(aux.Timestamp) + return nil +} + +// SubagentStopHookOutput can block a subagent's completion or replace its response. +// Decision must be "block" with a nonempty Reason, "allow", or empty to allow. +// Reason without a block decision and unsupported decisions fail the subagent. +type SubagentStopHookOutput struct { + Decision string `json:"decision,omitempty"` + Reason string `json:"reason,omitempty"` + ModifiedResponse *string `json:"modifiedResponse,omitempty"` +} + +// SubagentStopHandler handles subagent-stop hook invocations. +type SubagentStopHandler func(input SubagentStopHookInput, invocation HookInvocation) (*SubagentStopHookOutput, error) + // PreMCPToolCallHookInput is the input for a pre-mcp-tool-call hook type PreMCPToolCallHookInput struct { SessionID string `json:"sessionId"` @@ -1082,6 +1181,8 @@ type SessionHooks struct { OnSessionEnd SessionEndHandler OnErrorOccurred ErrorOccurredHandler OnAgentStop AgentStopHandler + OnSubagentStart SubagentStartHandler + OnSubagentStop SubagentStopHandler OnPreMCPToolCall PreMCPToolCallHandler } @@ -1238,6 +1339,8 @@ type ToolSearchConfig struct { type SessionFSCapabilities struct { // Sqlite indicates whether the provider supports SQLite query/exists operations. Sqlite bool + // Binary indicates whether the provider supports exact binary file reads and writes. + Binary bool } // SessionFSConfig configures a custom session filesystem provider. @@ -1342,6 +1445,24 @@ const ( AskUserVariantElicitation AskUserVariant = "elicitation" ) +// ErrSkillNotFound is returned by [SkillProvider.ReadSkill] when a listed skill +// is no longer available. It can be wrapped; the SDK checks it with [errors.Is]. +var ErrSkillNotFound = errors.New("skill not found") + +// SkillProvider supplies session-scoped skills from the SDK host. +// +// Experimental: this API may change or be removed in future SDK or CLI +// releases. Implementations must be safe for concurrent calls. +type SkillProvider interface { + // ListSkills returns catalog metadata for skills provided by this session. + // Return nil, nil to publish an empty catalog. + ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) + // ReadSkill returns the SKILL.md markdown for name. Return an error wrapping + // [ErrSkillNotFound] when the skill no longer exists. The markdown is + // ignored whenever a non-nil error is returned. + ReadSkill(ctx context.Context, name string) (string, error) +} + // SessionConfig configures a new session type SessionConfig struct { // SessionID is an optional custom session ID @@ -1541,6 +1662,12 @@ type SessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // SkillProvider supplies ephemeral SDK-hosted skills for this session. The + // provider is never serialized or persisted; re-supply it when resuming. + // + // Experimental: this API may change or be removed in future SDK or CLI + // releases. Implementations must be safe for concurrent calls. + SkillProvider SkillProvider `json:"-"` // PluginDirectories is a list of local filesystem paths to Open Plugins-format // directories (https://open-plugins.com/) to load for this session. // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). @@ -1726,6 +1853,11 @@ type ManagedSettingsPermissions struct { // Allow lists operations permitted without prompting. Every declared allow // list across managed layers must admit an operation for it to be allowed. Allow []string `json:"allow,omitzero"` + // LimitTo is a closed-world host boundary expressed as Domain(hostname), + // Domain(IP), or Domain(*.example.com) rules. Schemes, ports, paths, + // queries, and fragments are rejected. Multiple managed layers intersect + // their lists. A present empty list denies all hosts. + LimitTo []string `json:"limitTo,omitzero"` } // ToolDefer controls whether a tool may be deferred (loaded lazily via tool @@ -2106,6 +2238,13 @@ type ResumeSessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // SkillProvider supplies ephemeral SDK-hosted skills for this resumed + // session. Re-supply it on every resume; omitting it unbinds any previous + // provider. + // + // Experimental: this API may change or be removed in future SDK or CLI + // releases. Implementations must be safe for concurrent calls. + SkillProvider SkillProvider `json:"-"` // PluginDirectories is a list of local filesystem paths to Open Plugins-format // directories (https://open-plugins.com/) to load for this session. // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). @@ -2146,13 +2285,18 @@ type ResumeSessionConfig struct { // ContinuePendingWork, when non-nil, controls whether the runtime continues any // tool calls or permission prompts that were still pending when the session was // last suspended. Nil leaves the runtime default unchanged; use Bool(false) to - // explicitly treat pending work as interrupted on resume. + // explicitly treat pending work as interrupted on resume. Completed tool results + // already durably recorded by the runtime are preserved. // // For permission requests, the runtime re-emits permission.requested so the // registered OnPermissionRequest handler can re-prompt; for external tool calls, // the consumer is expected to supply the result via the corresponding low-level // RPC method. ContinuePendingWork *bool + // AllowTranscriptRecovery controls whether resume repairs a damaged transcript. + // Nil uses the runtime default (true) in all modes. Set false to reject recovery. + // Recovery may discard a torn tail; inspect Session.TranscriptRecovery(). + AllowTranscriptRecovery *bool // OnEvent is an optional event handler registered before the session.resume RPC // is issued, ensuring early events are delivered. See SessionConfig.OnEvent. OnEvent SessionEventHandler @@ -2268,6 +2412,10 @@ type ProviderConfig struct { // Set "websockets" to deliver Responses API requests over a persistent WebSocket // connection instead of HTTP. Applies to OpenAI-compatible providers using WireAPI "responses". Transport string `json:"transport,omitempty"` + // ModelProvider is the product serving the model, reported in telemetry as + // model_provider. Allowed values are "openai", "anthropic", "azure_openai", + // "ollama", "lm_studio", "foundry_local", and "llama_cpp"; only affects telemetry. + ModelProvider string `json:"modelProvider,omitempty"` // BaseURL is the API endpoint URL BaseURL string `json:"baseUrl"` // APIKey is the API key. Optional for local providers like Ollama. @@ -2396,6 +2544,11 @@ type NamedProviderConfig struct { Type string `json:"type,omitempty"` // WireAPI is the API format (openai/azure only): "completions" or "responses". Defaults to "completions". WireAPI string `json:"wireApi,omitempty"` + // ModelProvider is the product serving this provider's models, reported in + // telemetry as model_provider. Allowed values are "openai", "anthropic", + // "azure_openai", "ollama", "lm_studio", "foundry_local", and "llama_cpp"; + // only affects telemetry. + ModelProvider string `json:"modelProvider,omitempty"` // BaseURL is the API endpoint URL. BaseURL string `json:"baseUrl"` // APIKey is the API key. Optional for local providers like Ollama. @@ -2688,7 +2841,7 @@ type createSessionRequest struct { RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` WorkingDirectory string `json:"workingDirectory,omitempty"` - AdditionalDirectories []string `json:"additionalDirectories,omitempty"` + AdditionalDirectories []string `json:"additionalDirectories,omitzero"` Streaming *bool `json:"streaming,omitempty"` IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` @@ -2710,6 +2863,7 @@ type createSessionRequest struct { EnableHostGitOperations *bool `json:"enableHostGitOperations,omitempty"` EnableSessionStore *bool `json:"enableSessionStore,omitempty"` EnableSkills *bool `json:"enableSkills,omitempty"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` SkillDirectories []string `json:"skillDirectories,omitempty"` PluginDirectories []string `json:"pluginDirectories,omitempty"` InstructionDirectories []string `json:"instructionDirectories,omitempty"` @@ -2789,7 +2943,7 @@ type resumeSessionRequest struct { RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` WorkingDirectory string `json:"workingDirectory,omitempty"` - AdditionalDirectories []string `json:"additionalDirectories,omitempty"` + AdditionalDirectories []string `json:"additionalDirectories,omitzero"` ConfigDir string `json:"configDir,omitempty"` EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` SkipEmbeddingRetrieval *bool `json:"skipEmbeddingRetrieval,omitempty"` @@ -2800,8 +2954,10 @@ type resumeSessionRequest struct { EnableHostGitOperations *bool `json:"enableHostGitOperations,omitempty"` EnableSessionStore *bool `json:"enableSessionStore,omitempty"` EnableSkills *bool `json:"enableSkills,omitempty"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` DisableResume *bool `json:"disableResume,omitempty"` ContinuePendingWork *bool `json:"continuePendingWork,omitempty"` + AllowTranscriptRecovery *bool `json:"allowTranscriptRecovery,omitempty"` Streaming *bool `json:"streaming,omitempty"` IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` @@ -2846,10 +3002,18 @@ type resumeSessionRequest struct { // resumeSessionResponse is the response from session.resume type resumeSessionResponse struct { - SessionID string `json:"sessionId"` - WorkspacePath string `json:"workspacePath"` - Capabilities *SessionCapabilities `json:"capabilities,omitempty"` - OpenCanvases []rpc.OpenCanvasInstance `json:"openCanvases,omitempty"` + SessionID string `json:"sessionId"` + WorkspacePath string `json:"workspacePath"` + Capabilities *SessionCapabilities `json:"capabilities,omitempty"` + OpenCanvases []rpc.OpenCanvasInstance `json:"openCanvases,omitempty"` + TranscriptRecovery *TranscriptRecoveryReport `json:"transcriptRecovery,omitempty"` +} + +// TranscriptRecoveryReport describes the repair performed while resuming a session. +type TranscriptRecoveryReport struct { + PlannedBackupPath string `json:"plannedBackupPath"` + InvalidLineNumbers []int `json:"invalidLineNumbers"` + SessionStartMoved bool `json:"sessionStartMoved"` } type hooksInvokeRequest struct { diff --git a/go/types_test.go b/go/types_test.go index ad7972fe15..7c9afb6274 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -124,6 +124,7 @@ func TestProviderConfig_JSONIncludesAllFields(t *testing.T) { Headers: map[string]string{"Authorization": "Bearer provider-token"}, ModelID: "gpt-4o", WireModel: "my-finetune-v3", + ModelProvider: "lm_studio", MaxPromptTokens: 100000, MaxOutputTokens: 4096, } @@ -147,6 +148,9 @@ func TestProviderConfig_JSONIncludesAllFields(t *testing.T) { if decoded["wireModel"] != "my-finetune-v3" { t.Errorf("expected wireModel 'my-finetune-v3', got %v", decoded["wireModel"]) } + if decoded["modelProvider"] != "lm_studio" { + t.Errorf("expected modelProvider 'lm_studio', got %v", decoded["modelProvider"]) + } if decoded["maxPromptTokens"] != float64(100000) { t.Errorf("expected maxPromptTokens 100000, got %v", decoded["maxPromptTokens"]) } @@ -175,13 +179,36 @@ func TestProviderConfig_JSONOmitsUnsetTokenFields(t *testing.T) { t.Fatalf("failed to unmarshal ProviderConfig: %v", err) } - for _, field := range []string{"modelId", "wireModel", "maxPromptTokens", "maxOutputTokens", "headers"} { + for _, field := range []string{"modelId", "wireModel", "modelProvider", "maxPromptTokens", "maxOutputTokens", "headers"} { if _, present := decoded[field]; present { t.Errorf("expected %q to be omitted when unset, got %v", field, decoded[field]) } } } +func TestNamedProviderConfig_JSONIncludesModelProvider(t *testing.T) { + cfg := NamedProviderConfig{ + Name: "local", + Type: "openai", + BaseURL: "http://localhost:11434/v1", + ModelProvider: "ollama", + } + + data, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("failed to marshal NamedProviderConfig: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to unmarshal NamedProviderConfig: %v", err) + } + + if decoded["modelProvider"] != "ollama" { + t.Errorf("expected modelProvider 'ollama', got %v", decoded["modelProvider"]) + } +} + func TestCustomAgentConfig_JSONIncludesModel(t *testing.T) { cfg := CustomAgentConfig{ Name: "model-agent", diff --git a/go/zsession_events.go b/go/zsession_events.go index a1aab5837f..9887c2166b 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -122,14 +122,9 @@ type ( ExtensionsLoadedExtensionStatus = rpc.ExtensionsLoadedExtensionStatus ExternalToolCompletedData = rpc.ExternalToolCompletedData ExternalToolRequestedData = rpc.ExternalToolRequestedData - FactoryPermissionOperation = rpc.FactoryPermissionOperation - FactoryPermissionPhase = rpc.FactoryPermissionPhase - FactoryRunSettledData = rpc.FactoryRunSettledData - FactoryRunSettledStatus = rpc.FactoryRunSettledStatus - FactoryRunStartedData = rpc.FactoryRunStartedData - FactoryRunUpdatedData = rpc.FactoryRunUpdatedData FusionAttribution = rpc.FusionAttribution FusionConversationScope = rpc.FusionConversationScope + FusionCritic = rpc.FusionCritic FusionFollowUpAction = rpc.FusionFollowUpAction FusionFollowUpRecommendation = rpc.FusionFollowUpRecommendation FusionPattern = rpc.FusionPattern @@ -148,6 +143,13 @@ type ( HookEndError = rpc.HookEndError HookProgressData = rpc.HookProgressData HookStartData = rpc.HookStartData + HumanResponseActor = rpc.HumanResponseActor + HumanResponseRecordedData = rpc.HumanResponseRecordedData + HumanResponseRecordedResponse = rpc.HumanResponseRecordedResponse + HumanResponseRecordedResponseAskUser = rpc.HumanResponseRecordedResponseAskUser + HumanResponseRecordedResponseExitPlanMode = rpc.HumanResponseRecordedResponseExitPlanMode + HumanResponseRecordedResponseResponseKind = rpc.HumanResponseRecordedResponseResponseKind + HumanResponseRecordedResponseUserInput = rpc.HumanResponseRecordedResponseUserInput IndexedSearchState = rpc.IndexedSearchState ManagedSettingsEnforcedAction = rpc.ManagedSettingsEnforcedAction ManagedSettingsEnforcedEscalation = rpc.ManagedSettingsEnforcedEscalation @@ -183,6 +185,8 @@ type ( ModelCallFailureRequestFingerprint = rpc.ModelCallFailureRequestFingerprint ModelCallFailureSource = rpc.ModelCallFailureSource ModelCallFailureTransport = rpc.ModelCallFailureTransport + ModelCallFinalResult = rpc.ModelCallFinalResult + ModelCallFinalResultData = rpc.ModelCallFinalResultData ModelCallFinishedData = rpc.ModelCallFinishedData ModelCallFinishedOutcome = rpc.ModelCallFinishedOutcome ModelCallStartData = rpc.ModelCallStartData @@ -199,6 +203,7 @@ type ( PermissionApproved = rpc.PermissionApproved PermissionApprovedForLocation = rpc.PermissionApprovedForLocation PermissionApprovedForSession = rpc.PermissionApprovedForSession + PermissionApprovedReadOnlyForSession = rpc.PermissionApprovedReadOnlyForSession PermissionAssentDetectedData = rpc.PermissionAssentDetectedData PermissionAssistedApproval = rpc.PermissionAssistedApproval PermissionCancelled = rpc.PermissionCancelled @@ -221,7 +226,6 @@ type ( PermissionPromptRequestExtensionEnvAccess = rpc.PermissionPromptRequestExtensionEnvAccess PermissionPromptRequestExtensionManagement = rpc.PermissionPromptRequestExtensionManagement PermissionPromptRequestExtensionPermissionAccess = rpc.PermissionPromptRequestExtensionPermissionAccess - PermissionPromptRequestFactory = rpc.PermissionPromptRequestFactory PermissionPromptRequestHook = rpc.PermissionPromptRequestHook PermissionPromptRequestKind = rpc.PermissionPromptRequestKind PermissionPromptRequestMCP = rpc.PermissionPromptRequestMCP @@ -230,6 +234,7 @@ type ( PermissionPromptRequestPathAccessKind = rpc.PermissionPromptRequestPathAccessKind PermissionPromptRequestRead = rpc.PermissionPromptRequestRead PermissionPromptRequestURL = rpc.PermissionPromptRequestURL + PermissionPromptRequestWorkflow = rpc.PermissionPromptRequestWorkflow PermissionPromptRequestWrite = rpc.PermissionPromptRequestWrite PermissionRecommendation = rpc.PermissionRecommendation PermissionRecoveryAttempt = rpc.PermissionRecoveryAttempt @@ -247,7 +252,6 @@ type ( PermissionRequestExtensionEnvAccess = rpc.PermissionRequestExtensionEnvAccess PermissionRequestExtensionManagement = rpc.PermissionRequestExtensionManagement PermissionRequestExtensionPermissionAccess = rpc.PermissionRequestExtensionPermissionAccess - PermissionRequestFactory = rpc.PermissionRequestFactory PermissionRequestHook = rpc.PermissionRequestHook PermissionRequestKind = rpc.PermissionRequestKind PermissionRequestMCP = rpc.PermissionRequestMCP @@ -261,10 +265,13 @@ type ( PermissionRequestShellCommandSegment = rpc.PermissionRequestShellCommandSegment PermissionRequestShellPossibleURL = rpc.PermissionRequestShellPossibleURL PermissionRequestURL = rpc.PermissionRequestURL + PermissionRequestWorkflow = rpc.PermissionRequestWorkflow PermissionRequestWrite = rpc.PermissionRequestWrite PermissionResult = rpc.PermissionResult PermissionResultKind = rpc.PermissionResultKind PermissionRule = rpc.PermissionRule + PermissionSandboxPathGrant = rpc.PermissionSandboxPathGrant + PermissionSandboxPathGrantAccess = rpc.PermissionSandboxPathGrantAccess PersistedBinaryImage = rpc.PersistedBinaryImage PersistedBinaryImageType = rpc.PersistedBinaryImageType PersistedBinaryResult = rpc.PersistedBinaryResult @@ -273,13 +280,14 @@ type ( PossibleURL = rpc.PossibleURL PromptCacheBreakData = rpc.PromptCacheBreakData RawCitationLocation = rpc.RawCitationLocation + RawHumanResponseRecordedResponse = rpc.RawHumanResponseRecordedResponse RawPermissionPromptRequest = rpc.RawPermissionPromptRequest RawPermissionRequest = rpc.RawPermissionRequest RawPermissionResult = rpc.RawPermissionResult RawPersistedBinaryResult = rpc.RawPersistedBinaryResult RawSessionEventData = rpc.RawSessionEventData RawSystemNotification = rpc.RawSystemNotification - RawSystemNotificationFactoryPauseInfo = rpc.RawSystemNotificationFactoryPauseInfo + RawSystemNotificationWorkflowPauseInfo = rpc.RawSystemNotificationWorkflowPauseInfo RawToolExecutionCompleteContent = rpc.RawToolExecutionCompleteContent ReasoningSummary = rpc.ReasoningSummary RecommendedAutoTier = rpc.RecommendedAutoTier @@ -392,18 +400,18 @@ type ( SystemNotificationAgentCompletedStatus = rpc.SystemNotificationAgentCompletedStatus SystemNotificationAgentIdle = rpc.SystemNotificationAgentIdle SystemNotificationData = rpc.SystemNotificationData - SystemNotificationFactoryCompleted = rpc.SystemNotificationFactoryCompleted - SystemNotificationFactoryCompletedStatus = rpc.SystemNotificationFactoryCompletedStatus - SystemNotificationFactoryPauseInfo = rpc.SystemNotificationFactoryPauseInfo - SystemNotificationFactoryPauseInfoCheckpoint = rpc.SystemNotificationFactoryPauseInfoCheckpoint - SystemNotificationFactoryPauseInfoType = rpc.SystemNotificationFactoryPauseInfoType - SystemNotificationFactoryPauseInfoUser = rpc.SystemNotificationFactoryPauseInfoUser SystemNotificationInstructionDiscovered = rpc.SystemNotificationInstructionDiscovered SystemNotificationNewInboxMessage = rpc.SystemNotificationNewInboxMessage SystemNotificationShellCompleted = rpc.SystemNotificationShellCompleted SystemNotificationShellDetachedCompleted = rpc.SystemNotificationShellDetachedCompleted SystemNotificationType = rpc.SystemNotificationType SystemNotificationUnclassified = rpc.SystemNotificationUnclassified + SystemNotificationWorkflowCompleted = rpc.SystemNotificationWorkflowCompleted + SystemNotificationWorkflowCompletedStatus = rpc.SystemNotificationWorkflowCompletedStatus + SystemNotificationWorkflowPauseInfo = rpc.SystemNotificationWorkflowPauseInfo + SystemNotificationWorkflowPauseInfoCheckpoint = rpc.SystemNotificationWorkflowPauseInfoCheckpoint + SystemNotificationWorkflowPauseInfoType = rpc.SystemNotificationWorkflowPauseInfoType + SystemNotificationWorkflowPauseInfoUser = rpc.SystemNotificationWorkflowPauseInfoUser TaskBlocker = rpc.TaskBlocker TaskBlockerKind = rpc.TaskBlockerKind TaskCompleteData = rpc.TaskCompleteData @@ -422,6 +430,8 @@ type ( ToolExecutionCompleteContentType = rpc.ToolExecutionCompleteContentType ToolExecutionCompleteData = rpc.ToolExecutionCompleteData ToolExecutionCompleteError = rpc.ToolExecutionCompleteError + ToolExecutionCompleteFileEdit = rpc.ToolExecutionCompleteFileEdit + ToolExecutionCompleteFileEditKind = rpc.ToolExecutionCompleteFileEditKind ToolExecutionCompleteResult = rpc.ToolExecutionCompleteResult ToolExecutionCompleteShellExecution = rpc.ToolExecutionCompleteShellExecution ToolExecutionCompleteToolDescription = rpc.ToolExecutionCompleteToolDescription @@ -437,39 +447,48 @@ type ( ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone - ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData - ToolExecutionProgressData = rpc.ToolExecutionProgressData - ToolExecutionStartData = rpc.ToolExecutionStartData - ToolExecutionStartShellToolInfo = rpc.ToolExecutionStartShellToolInfo - ToolExecutionStartToolDescription = rpc.ToolExecutionStartToolDescription - ToolExecutionStartToolDescriptionMeta = rpc.ToolExecutionStartToolDescriptionMeta - ToolExecutionStartToolDescriptionMetaUI = rpc.ToolExecutionStartToolDescriptionMetaUI - ToolExecutionStartToolDescriptionMetaUIVisibility = rpc.ToolExecutionStartToolDescriptionMetaUIVisibility - ToolSearchActivatedData = rpc.ToolSearchActivatedData - ToolUserRequestedData = rpc.ToolUserRequestedData - UIEphemeralQueryData = rpc.UIEphemeralQueryData - UIEphemeralQueryPhase = rpc.UIEphemeralQueryPhase - UserInputCompletedData = rpc.UserInputCompletedData - UserInputRequestedData = rpc.UserInputRequestedData - UserMessageAgentMode = rpc.UserMessageAgentMode - UserMessageData = rpc.UserMessageData - UserMessageDelivery = rpc.UserMessageDelivery - UserToolSessionApproval = rpc.UserToolSessionApproval - UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands - UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool - UserToolSessionApprovalExtensionEnvAccess = rpc.UserToolSessionApprovalExtensionEnvAccess - UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement - UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess - UserToolSessionApprovalFactory = rpc.UserToolSessionApprovalFactory - UserToolSessionApprovalKind = rpc.UserToolSessionApprovalKind - UserToolSessionApprovalMCP = rpc.UserToolSessionApprovalMCP - UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory - UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead - UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite - Verbosity = rpc.Verbosity - WorkingDirectoryContext = rpc.WorkingDirectoryContext - WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType - WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation + // Deprecated: ToolExecutionPartialResultData is deprecated. + ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData + ToolExecutionProgressData = rpc.ToolExecutionProgressData + ToolExecutionStartData = rpc.ToolExecutionStartData + ToolExecutionStartShellToolInfo = rpc.ToolExecutionStartShellToolInfo + ToolExecutionStartToolDescription = rpc.ToolExecutionStartToolDescription + ToolExecutionStartToolDescriptionMeta = rpc.ToolExecutionStartToolDescriptionMeta + ToolExecutionStartToolDescriptionMetaUI = rpc.ToolExecutionStartToolDescriptionMetaUI + ToolExecutionStartToolDescriptionMetaUIVisibility = rpc.ToolExecutionStartToolDescriptionMetaUIVisibility + ToolSearchActivatedData = rpc.ToolSearchActivatedData + ToolShellOutputData = rpc.ToolShellOutputData + ToolShellOutputStream = rpc.ToolShellOutputStream + ToolUserRequestedData = rpc.ToolUserRequestedData + UIEphemeralQueryData = rpc.UIEphemeralQueryData + UIEphemeralQueryPhase = rpc.UIEphemeralQueryPhase + UserInputCompletedData = rpc.UserInputCompletedData + UserInputRequestedData = rpc.UserInputRequestedData + UserMessageAgentMode = rpc.UserMessageAgentMode + UserMessageData = rpc.UserMessageData + UserMessageDelivery = rpc.UserMessageDelivery + UserToolSessionApproval = rpc.UserToolSessionApproval + UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands + UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool + UserToolSessionApprovalExtensionEnvAccess = rpc.UserToolSessionApprovalExtensionEnvAccess + UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement + UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess + UserToolSessionApprovalKind = rpc.UserToolSessionApprovalKind + UserToolSessionApprovalMCP = rpc.UserToolSessionApprovalMCP + UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory + UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead + UserToolSessionApprovalWorkflow = rpc.UserToolSessionApprovalWorkflow + UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite + Verbosity = rpc.Verbosity + WorkflowPermissionOperation = rpc.WorkflowPermissionOperation + WorkflowPermissionPhase = rpc.WorkflowPermissionPhase + WorkflowRunSettledData = rpc.WorkflowRunSettledData + WorkflowRunSettledStatus = rpc.WorkflowRunSettledStatus + WorkflowRunStartedData = rpc.WorkflowRunStartedData + WorkflowRunUpdatedData = rpc.WorkflowRunUpdatedData + WorkingDirectoryContext = rpc.WorkingDirectoryContext + WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType + WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation ) // Session-event constants are generated in the rpc package and re-exported here for source compatibility. @@ -583,13 +602,6 @@ const ( ExtensionsLoadedExtensionStatusFailed = rpc.ExtensionsLoadedExtensionStatusFailed ExtensionsLoadedExtensionStatusRunning = rpc.ExtensionsLoadedExtensionStatusRunning ExtensionsLoadedExtensionStatusStarting = rpc.ExtensionsLoadedExtensionStatusStarting - FactoryPermissionOperationAuthor = rpc.FactoryPermissionOperationAuthor - FactoryPermissionOperationRun = rpc.FactoryPermissionOperationRun - FactoryRunSettledStatusCancelled = rpc.FactoryRunSettledStatusCancelled - FactoryRunSettledStatusCompleted = rpc.FactoryRunSettledStatusCompleted - FactoryRunSettledStatusError = rpc.FactoryRunSettledStatusError - FactoryRunSettledStatusHalted = rpc.FactoryRunSettledStatusHalted - FactoryRunSettledStatusPaused = rpc.FactoryRunSettledStatusPaused FusionConversationScopeReview = rpc.FusionConversationScopeReview FusionConversationScopeRoot = rpc.FusionConversationScopeRoot FusionFollowUpActionReroute = rpc.FusionFollowUpActionReroute @@ -617,6 +629,12 @@ const ( FusionTurnKindUser = rpc.FusionTurnKindUser HandoffSourceTypeLocal = rpc.HandoffSourceTypeLocal HandoffSourceTypeRemote = rpc.HandoffSourceTypeRemote + HumanResponseActorHostAutomation = rpc.HumanResponseActorHostAutomation + HumanResponseActorHumanResponse = rpc.HumanResponseActorHumanResponse + HumanResponseActorUnknown = rpc.HumanResponseActorUnknown + HumanResponseRecordedResponseResponseKindAskUser = rpc.HumanResponseRecordedResponseResponseKindAskUser + HumanResponseRecordedResponseResponseKindExitPlanMode = rpc.HumanResponseRecordedResponseResponseKindExitPlanMode + HumanResponseRecordedResponseResponseKindUserInput = rpc.HumanResponseRecordedResponseResponseKindUserInput IndexedSearchStateDisabled = rpc.IndexedSearchStateDisabled IndexedSearchStateEnabled = rpc.IndexedSearchStateEnabled IndexedSearchStateFailed = rpc.IndexedSearchStateFailed @@ -649,6 +667,7 @@ const ( MCPOauthRequestReasonRefresh = rpc.MCPOauthRequestReasonRefresh MCPOauthRequestReasonUpscope = rpc.MCPOauthRequestReasonUpscope MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials = rpc.MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials + MCPServerSourceAccount = rpc.MCPServerSourceAccount MCPServerSourceBuiltin = rpc.MCPServerSourceBuiltin MCPServerSourceManaged = rpc.MCPServerSourceManaged MCPServerSourcePlugin = rpc.MCPServerSourcePlugin @@ -674,12 +693,21 @@ const ( ModelCallFailureSourceTopLevel = rpc.ModelCallFailureSourceTopLevel ModelCallFailureTransportHTTP = rpc.ModelCallFailureTransportHTTP ModelCallFailureTransportWebsocket = rpc.ModelCallFailureTransportWebsocket + ModelCallFinalResultHTTP400 = rpc.ModelCallFinalResultHTTP400 + ModelCallFinalResultHTTP413 = rpc.ModelCallFinalResultHTTP413 + ModelCallFinalResultHTTP429 = rpc.ModelCallFinalResultHTTP429 + ModelCallFinalResultHTTP4xx = rpc.ModelCallFinalResultHTTP4xx + ModelCallFinalResultHTTP5xx = rpc.ModelCallFinalResultHTTP5xx + ModelCallFinalResultOtherError = rpc.ModelCallFinalResultOtherError + ModelCallFinalResultSuccess = rpc.ModelCallFinalResultSuccess + ModelCallFinalResultTransportError = rpc.ModelCallFinalResultTransportError ModelCallFinishedOutcomeCancelled = rpc.ModelCallFinishedOutcomeCancelled ModelCallFinishedOutcomeError = rpc.ModelCallFinishedOutcomeError ModelCallFinishedOutcomeRejected = rpc.ModelCallFinishedOutcomeRejected ModelCallFinishedOutcomeSuccess = rpc.ModelCallFinishedOutcomeSuccess ModelChangeSourceAgent = rpc.ModelChangeSourceAgent ModelChangeSourceAutomatic = rpc.ModelChangeSourceAutomatic + ModelChangeSourceAutoTierRecommendation = rpc.ModelChangeSourceAutoTierRecommendation ModelChangeSourceChangeboardingShortcut = rpc.ModelChangeSourceChangeboardingShortcut ModelChangeSourceConfigCommand = rpc.ModelChangeSourceConfigCommand ModelChangeSourceManagedSettings = rpc.ModelChangeSourceManagedSettings @@ -709,6 +737,10 @@ const ( PermissionApprovalEvaluationReasonCodeActionTooLong = rpc.PermissionApprovalEvaluationReasonCodeActionTooLong PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable = rpc.PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete = rpc.PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete + PermissionApprovalEvaluationReasonCodeContentExcluded = rpc.PermissionApprovalEvaluationReasonCodeContentExcluded + PermissionApprovalEvaluationReasonCodeDynamicSource = rpc.PermissionApprovalEvaluationReasonCodeDynamicSource + PermissionApprovalEvaluationReasonCodeExecutableTooLarge = rpc.PermissionApprovalEvaluationReasonCodeExecutableTooLarge + PermissionApprovalEvaluationReasonCodeExecutableUnavailable = rpc.PermissionApprovalEvaluationReasonCodeExecutableUnavailable PermissionApprovalEvaluationReasonCodeInactive = rpc.PermissionApprovalEvaluationReasonCodeInactive PermissionApprovalEvaluationReasonCodeInherited = rpc.PermissionApprovalEvaluationReasonCodeInherited PermissionApprovalEvaluationReasonCodeInterpreterTooLarge = rpc.PermissionApprovalEvaluationReasonCodeInterpreterTooLarge @@ -727,11 +759,14 @@ const ( PermissionApprovalEvaluationReasonCodeSandboxBypass = rpc.PermissionApprovalEvaluationReasonCodeSandboxBypass PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable = rpc.PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable PermissionApprovalEvaluationReasonCodeTooLarge = rpc.PermissionApprovalEvaluationReasonCodeTooLarge + PermissionApprovalEvaluationReasonCodeTooManySources = rpc.PermissionApprovalEvaluationReasonCodeTooManySources PermissionApprovalEvaluationReasonCodeUnavailable = rpc.PermissionApprovalEvaluationReasonCodeUnavailable PermissionApprovalEvaluationReasonCodeUnknown = rpc.PermissionApprovalEvaluationReasonCodeUnknown PermissionApprovalEvaluationReasonCodeUnreadable = rpc.PermissionApprovalEvaluationReasonCodeUnreadable PermissionApprovalEvaluationReasonCodeUnrepresentablePath = rpc.PermissionApprovalEvaluationReasonCodeUnrepresentablePath PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation = rpc.PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation + PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape = rpc.PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape + PermissionApprovalEvaluationReasonCodeUnsupportedSource = rpc.PermissionApprovalEvaluationReasonCodeUnsupportedSource PermissionDecisionSourceAuthorizationCarryForward = rpc.PermissionDecisionSourceAuthorizationCarryForward PermissionMessageAuthorizationPolarityDenial = rpc.PermissionMessageAuthorizationPolarityDenial PermissionMessageAuthorizationPolarityGrant = rpc.PermissionMessageAuthorizationPolarityGrant @@ -743,13 +778,13 @@ const ( PermissionPromptRequestKindExtensionEnvAccess = rpc.PermissionPromptRequestKindExtensionEnvAccess PermissionPromptRequestKindExtensionManagement = rpc.PermissionPromptRequestKindExtensionManagement PermissionPromptRequestKindExtensionPermissionAccess = rpc.PermissionPromptRequestKindExtensionPermissionAccess - PermissionPromptRequestKindFactory = rpc.PermissionPromptRequestKindFactory PermissionPromptRequestKindHook = rpc.PermissionPromptRequestKindHook PermissionPromptRequestKindMCP = rpc.PermissionPromptRequestKindMCP PermissionPromptRequestKindMemory = rpc.PermissionPromptRequestKindMemory PermissionPromptRequestKindPath = rpc.PermissionPromptRequestKindPath PermissionPromptRequestKindRead = rpc.PermissionPromptRequestKindRead PermissionPromptRequestKindURL = rpc.PermissionPromptRequestKindURL + PermissionPromptRequestKindWorkflow = rpc.PermissionPromptRequestKindWorkflow PermissionPromptRequestKindWrite = rpc.PermissionPromptRequestKindWrite PermissionPromptRequestPathAccessKindRead = rpc.PermissionPromptRequestPathAccessKindRead PermissionPromptRequestPathAccessKindShell = rpc.PermissionPromptRequestPathAccessKindShell @@ -788,13 +823,13 @@ const ( PermissionRequestKindExtensionEnvAccess = rpc.PermissionRequestKindExtensionEnvAccess PermissionRequestKindExtensionManagement = rpc.PermissionRequestKindExtensionManagement PermissionRequestKindExtensionPermissionAccess = rpc.PermissionRequestKindExtensionPermissionAccess - PermissionRequestKindFactory = rpc.PermissionRequestKindFactory PermissionRequestKindHook = rpc.PermissionRequestKindHook PermissionRequestKindMCP = rpc.PermissionRequestKindMCP PermissionRequestKindMemory = rpc.PermissionRequestKindMemory PermissionRequestKindRead = rpc.PermissionRequestKindRead PermissionRequestKindShell = rpc.PermissionRequestKindShell PermissionRequestKindURL = rpc.PermissionRequestKindURL + PermissionRequestKindWorkflow = rpc.PermissionRequestKindWorkflow PermissionRequestKindWrite = rpc.PermissionRequestKindWrite PermissionRequestMemoryActionStore = rpc.PermissionRequestMemoryActionStore PermissionRequestMemoryActionVote = rpc.PermissionRequestMemoryActionVote @@ -805,12 +840,15 @@ const ( PermissionResultKindApproved = rpc.PermissionResultKindApproved PermissionResultKindApprovedForLocation = rpc.PermissionResultKindApprovedForLocation PermissionResultKindApprovedForSession = rpc.PermissionResultKindApprovedForSession + PermissionResultKindApprovedReadOnlyForSession = rpc.PermissionResultKindApprovedReadOnlyForSession PermissionResultKindCancelled = rpc.PermissionResultKindCancelled PermissionResultKindDeniedByContentExclusionPolicy = rpc.PermissionResultKindDeniedByContentExclusionPolicy PermissionResultKindDeniedByPermissionRequestHook = rpc.PermissionResultKindDeniedByPermissionRequestHook PermissionResultKindDeniedByRules = rpc.PermissionResultKindDeniedByRules PermissionResultKindDeniedInteractivelyByUser = rpc.PermissionResultKindDeniedInteractivelyByUser PermissionResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser = rpc.PermissionResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser + PermissionSandboxPathGrantAccessRead = rpc.PermissionSandboxPathGrantAccessRead + PermissionSandboxPathGrantAccessReadWrite = rpc.PermissionSandboxPathGrantAccessReadWrite PersistedBinaryImageTypeImage = rpc.PersistedBinaryImageTypeImage PersistedBinaryImageTypeResource = rpc.PersistedBinaryImageTypeResource PersistedBinaryResultTypeImage = rpc.PersistedBinaryResultTypeImage @@ -864,12 +902,10 @@ const ( SessionEventTypeExitPlanModeRequested = rpc.SessionEventTypeExitPlanModeRequested SessionEventTypeExternalToolCompleted = rpc.SessionEventTypeExternalToolCompleted SessionEventTypeExternalToolRequested = rpc.SessionEventTypeExternalToolRequested - SessionEventTypeFactoryRunSettled = rpc.SessionEventTypeFactoryRunSettled - SessionEventTypeFactoryRunStarted = rpc.SessionEventTypeFactoryRunStarted - SessionEventTypeFactoryRunUpdated = rpc.SessionEventTypeFactoryRunUpdated SessionEventTypeHookEnd = rpc.SessionEventTypeHookEnd SessionEventTypeHookProgress = rpc.SessionEventTypeHookProgress SessionEventTypeHookStart = rpc.SessionEventTypeHookStart + SessionEventTypeHumanResponseRecorded = rpc.SessionEventTypeHumanResponseRecorded SessionEventTypeMCPAppToolCallComplete = rpc.SessionEventTypeMCPAppToolCallComplete SessionEventTypeMCPHeadersRefreshCompleted = rpc.SessionEventTypeMCPHeadersRefreshCompleted SessionEventTypeMCPHeadersRefreshRequired = rpc.SessionEventTypeMCPHeadersRefreshRequired @@ -879,6 +915,7 @@ const ( SessionEventTypeMCPResourcesListChanged = rpc.SessionEventTypeMCPResourcesListChanged SessionEventTypeMCPToolsListChanged = rpc.SessionEventTypeMCPToolsListChanged SessionEventTypeModelCallFailure = rpc.SessionEventTypeModelCallFailure + SessionEventTypeModelCallFinalResult = rpc.SessionEventTypeModelCallFinalResult SessionEventTypeModelCallFinished = rpc.SessionEventTypeModelCallFinished SessionEventTypeModelCallStart = rpc.SessionEventTypeModelCallStart SessionEventTypePendingMessagesModified = rpc.SessionEventTypePendingMessagesModified @@ -971,109 +1008,127 @@ const ( SessionEventTypeSystemMessage = rpc.SessionEventTypeSystemMessage SessionEventTypeSystemNotification = rpc.SessionEventTypeSystemNotification SessionEventTypeToolExecutionComplete = rpc.SessionEventTypeToolExecutionComplete - SessionEventTypeToolExecutionPartialResult = rpc.SessionEventTypeToolExecutionPartialResult - SessionEventTypeToolExecutionProgress = rpc.SessionEventTypeToolExecutionProgress - SessionEventTypeToolExecutionStart = rpc.SessionEventTypeToolExecutionStart - SessionEventTypeToolSearchActivated = rpc.SessionEventTypeToolSearchActivated - SessionEventTypeToolUserRequested = rpc.SessionEventTypeToolUserRequested - SessionEventTypeUIEphemeralQuery = rpc.SessionEventTypeUIEphemeralQuery - SessionEventTypeUserInputCompleted = rpc.SessionEventTypeUserInputCompleted - SessionEventTypeUserInputRequested = rpc.SessionEventTypeUserInputRequested - SessionEventTypeUserMessage = rpc.SessionEventTypeUserMessage - SessionLimitsExhaustedResponseActionAdd = rpc.SessionLimitsExhaustedResponseActionAdd - SessionLimitsExhaustedResponseActionCancel = rpc.SessionLimitsExhaustedResponseActionCancel - SessionLimitsExhaustedResponseActionSet = rpc.SessionLimitsExhaustedResponseActionSet - SessionLimitsExhaustedResponseActionUnset = rpc.SessionLimitsExhaustedResponseActionUnset - SessionModeAutopilot = rpc.SessionModeAutopilot - SessionModeInteractive = rpc.SessionModeInteractive - SessionModePlan = rpc.SessionModePlan - ShutdownTypeError = rpc.ShutdownTypeError - ShutdownTypeRoutine = rpc.ShutdownTypeRoutine - SkillInvokedTriggerAgentInvoked = rpc.SkillInvokedTriggerAgentInvoked - SkillInvokedTriggerContextLoad = rpc.SkillInvokedTriggerContextLoad - SkillInvokedTriggerUserInvoked = rpc.SkillInvokedTriggerUserInvoked - SkillSourceBuiltin = rpc.SkillSourceBuiltin - SkillSourceCustom = rpc.SkillSourceCustom - SkillSourceInherited = rpc.SkillSourceInherited - SkillSourcePersonalAgents = rpc.SkillSourcePersonalAgents - SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot - SkillSourcePlugin = rpc.SkillSourcePlugin - SkillSourceProject = rpc.SkillSourceProject - SkillSourceSDK = rpc.SkillSourceSDK - SubagentModelSelectionSourceAgentDefinitionDefault = rpc.SubagentModelSelectionSourceAgentDefinitionDefault - SubagentModelSelectionSourceComplementaryDefault = rpc.SubagentModelSelectionSourceComplementaryDefault - SubagentModelSelectionSourceConfiguredPreference = rpc.SubagentModelSelectionSourceConfiguredPreference - SubagentModelSelectionSourceConfiguredRequired = rpc.SubagentModelSelectionSourceConfiguredRequired - SubagentModelSelectionSourceExplicitOverride = rpc.SubagentModelSelectionSourceExplicitOverride - SubagentModelSelectionSourceRuntimePolicy = rpc.SubagentModelSelectionSourceRuntimePolicy - SubagentModelSelectionSourceSessionInheritance = rpc.SubagentModelSelectionSourceSessionInheritance - SubagentTaskModelSourceCustomAgentDefinition = rpc.SubagentTaskModelSourceCustomAgentDefinition - SubagentTaskModelSourceSubagentConfiguration = rpc.SubagentTaskModelSourceSubagentConfiguration - SubagentTaskModelSourceTaskArgument = rpc.SubagentTaskModelSourceTaskArgument - SubagentTaskModelSourceUnset = rpc.SubagentTaskModelSourceUnset - SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper - SystemMessageRoleSystem = rpc.SystemMessageRoleSystem - SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted - SystemNotificationAgentCompletedStatusFailed = rpc.SystemNotificationAgentCompletedStatusFailed - SystemNotificationFactoryCompletedStatusCancelled = rpc.SystemNotificationFactoryCompletedStatusCancelled - SystemNotificationFactoryCompletedStatusCompleted = rpc.SystemNotificationFactoryCompletedStatusCompleted - SystemNotificationFactoryCompletedStatusError = rpc.SystemNotificationFactoryCompletedStatusError - SystemNotificationFactoryCompletedStatusHalted = rpc.SystemNotificationFactoryCompletedStatusHalted - SystemNotificationFactoryCompletedStatusPaused = rpc.SystemNotificationFactoryCompletedStatusPaused - SystemNotificationFactoryPauseInfoTypeCheckpoint = rpc.SystemNotificationFactoryPauseInfoTypeCheckpoint - SystemNotificationFactoryPauseInfoTypeUser = rpc.SystemNotificationFactoryPauseInfoTypeUser - SystemNotificationTypeAgentCompleted = rpc.SystemNotificationTypeAgentCompleted - SystemNotificationTypeAgentIdle = rpc.SystemNotificationTypeAgentIdle - SystemNotificationTypeFactoryCompleted = rpc.SystemNotificationTypeFactoryCompleted - SystemNotificationTypeInstructionDiscovered = rpc.SystemNotificationTypeInstructionDiscovered - SystemNotificationTypeNewInboxMessage = rpc.SystemNotificationTypeNewInboxMessage - SystemNotificationTypeShellCompleted = rpc.SystemNotificationTypeShellCompleted - SystemNotificationTypeShellDetachedCompleted = rpc.SystemNotificationTypeShellDetachedCompleted - SystemNotificationTypeUnclassified = rpc.SystemNotificationTypeUnclassified - TaskBlockerKindPermissionRecovery = rpc.TaskBlockerKindPermissionRecovery - TaskCompletionOutcomeBlocked = rpc.TaskCompletionOutcomeBlocked - TaskCompletionOutcomeCompleted = rpc.TaskCompletionOutcomeCompleted - TaskCompletionOutcomeContinue = rpc.TaskCompletionOutcomeContinue - ToolExecutionCompleteContentResourceLinkIconThemeDark = rpc.ToolExecutionCompleteContentResourceLinkIconThemeDark - ToolExecutionCompleteContentResourceLinkIconThemeLight = rpc.ToolExecutionCompleteContentResourceLinkIconThemeLight - ToolExecutionCompleteContentTypeAudio = rpc.ToolExecutionCompleteContentTypeAudio - ToolExecutionCompleteContentTypeImage = rpc.ToolExecutionCompleteContentTypeImage - ToolExecutionCompleteContentTypeResource = rpc.ToolExecutionCompleteContentTypeResource - ToolExecutionCompleteContentTypeResourceLink = rpc.ToolExecutionCompleteContentTypeResourceLink - ToolExecutionCompleteContentTypeShellExit = rpc.ToolExecutionCompleteContentTypeShellExit - ToolExecutionCompleteContentTypeTerminal = rpc.ToolExecutionCompleteContentTypeTerminal - ToolExecutionCompleteContentTypeText = rpc.ToolExecutionCompleteContentTypeText - ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp - ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel - ToolExecutionStartToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityApp - ToolExecutionStartToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityModel - UIEphemeralQueryPhaseAborted = rpc.UIEphemeralQueryPhaseAborted - UIEphemeralQueryPhaseChunk = rpc.UIEphemeralQueryPhaseChunk - UIEphemeralQueryPhaseCompleted = rpc.UIEphemeralQueryPhaseCompleted - UIEphemeralQueryPhaseFailed = rpc.UIEphemeralQueryPhaseFailed - UIEphemeralQueryPhaseStarted = rpc.UIEphemeralQueryPhaseStarted - UserMessageAgentModeAutopilot = rpc.UserMessageAgentModeAutopilot - UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive - UserMessageAgentModePlan = rpc.UserMessageAgentModePlan - UserMessageAgentModeShell = rpc.UserMessageAgentModeShell - UserMessageDeliveryIdle = rpc.UserMessageDeliveryIdle - UserMessageDeliveryQueued = rpc.UserMessageDeliveryQueued - UserMessageDeliverySteering = rpc.UserMessageDeliverySteering - UserToolSessionApprovalKindCommands = rpc.UserToolSessionApprovalKindCommands - UserToolSessionApprovalKindCustomTool = rpc.UserToolSessionApprovalKindCustomTool - UserToolSessionApprovalKindExtensionEnvAccess = rpc.UserToolSessionApprovalKindExtensionEnvAccess - UserToolSessionApprovalKindExtensionManagement = rpc.UserToolSessionApprovalKindExtensionManagement - UserToolSessionApprovalKindExtensionPermissionAccess = rpc.UserToolSessionApprovalKindExtensionPermissionAccess - UserToolSessionApprovalKindFactory = rpc.UserToolSessionApprovalKindFactory - UserToolSessionApprovalKindMCP = rpc.UserToolSessionApprovalKindMCP - UserToolSessionApprovalKindMemory = rpc.UserToolSessionApprovalKindMemory - UserToolSessionApprovalKindRead = rpc.UserToolSessionApprovalKindRead - UserToolSessionApprovalKindWrite = rpc.UserToolSessionApprovalKindWrite - VerbosityHigh = rpc.VerbosityHigh - VerbosityLow = rpc.VerbosityLow - VerbosityMedium = rpc.VerbosityMedium - WorkingDirectoryContextHostTypeADO = rpc.WorkingDirectoryContextHostTypeADO - WorkingDirectoryContextHostTypeGitHub = rpc.WorkingDirectoryContextHostTypeGitHub - WorkspaceFileChangedOperationCreate = rpc.WorkspaceFileChangedOperationCreate - WorkspaceFileChangedOperationUpdate = rpc.WorkspaceFileChangedOperationUpdate + // Deprecated: SessionEventTypeToolExecutionPartialResult identifies a deprecated event. + SessionEventTypeToolExecutionPartialResult = rpc.SessionEventTypeToolExecutionPartialResult + SessionEventTypeToolExecutionProgress = rpc.SessionEventTypeToolExecutionProgress + SessionEventTypeToolExecutionStart = rpc.SessionEventTypeToolExecutionStart + SessionEventTypeToolSearchActivated = rpc.SessionEventTypeToolSearchActivated + SessionEventTypeToolShellOutput = rpc.SessionEventTypeToolShellOutput + SessionEventTypeToolUserRequested = rpc.SessionEventTypeToolUserRequested + SessionEventTypeUIEphemeralQuery = rpc.SessionEventTypeUIEphemeralQuery + SessionEventTypeUserInputCompleted = rpc.SessionEventTypeUserInputCompleted + SessionEventTypeUserInputRequested = rpc.SessionEventTypeUserInputRequested + SessionEventTypeUserMessage = rpc.SessionEventTypeUserMessage + SessionEventTypeWorkflowRunSettled = rpc.SessionEventTypeWorkflowRunSettled + SessionEventTypeWorkflowRunStarted = rpc.SessionEventTypeWorkflowRunStarted + SessionEventTypeWorkflowRunUpdated = rpc.SessionEventTypeWorkflowRunUpdated + SessionLimitsExhaustedResponseActionAdd = rpc.SessionLimitsExhaustedResponseActionAdd + SessionLimitsExhaustedResponseActionCancel = rpc.SessionLimitsExhaustedResponseActionCancel + SessionLimitsExhaustedResponseActionSet = rpc.SessionLimitsExhaustedResponseActionSet + SessionLimitsExhaustedResponseActionUnset = rpc.SessionLimitsExhaustedResponseActionUnset + SessionModeAutopilot = rpc.SessionModeAutopilot + SessionModeInteractive = rpc.SessionModeInteractive + SessionModePlan = rpc.SessionModePlan + ShutdownTypeError = rpc.ShutdownTypeError + ShutdownTypeRoutine = rpc.ShutdownTypeRoutine + SkillInvokedTriggerAgentInvoked = rpc.SkillInvokedTriggerAgentInvoked + SkillInvokedTriggerContextLoad = rpc.SkillInvokedTriggerContextLoad + SkillInvokedTriggerUserInvoked = rpc.SkillInvokedTriggerUserInvoked + SkillSourceBuiltin = rpc.SkillSourceBuiltin + SkillSourceCustom = rpc.SkillSourceCustom + SkillSourceInherited = rpc.SkillSourceInherited + SkillSourcePersonalAgents = rpc.SkillSourcePersonalAgents + SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot + SkillSourcePlugin = rpc.SkillSourcePlugin + SkillSourceProject = rpc.SkillSourceProject + SkillSourceSDK = rpc.SkillSourceSDK + SubagentModelSelectionSourceAgentDefinitionDefault = rpc.SubagentModelSelectionSourceAgentDefinitionDefault + SubagentModelSelectionSourceComplementaryDefault = rpc.SubagentModelSelectionSourceComplementaryDefault + SubagentModelSelectionSourceConfiguredPreference = rpc.SubagentModelSelectionSourceConfiguredPreference + SubagentModelSelectionSourceConfiguredRequired = rpc.SubagentModelSelectionSourceConfiguredRequired + SubagentModelSelectionSourceExplicitOverride = rpc.SubagentModelSelectionSourceExplicitOverride + SubagentModelSelectionSourceRuntimePolicy = rpc.SubagentModelSelectionSourceRuntimePolicy + SubagentModelSelectionSourceSessionInheritance = rpc.SubagentModelSelectionSourceSessionInheritance + SubagentTaskModelSourceCustomAgentDefinition = rpc.SubagentTaskModelSourceCustomAgentDefinition + SubagentTaskModelSourceSubagentConfiguration = rpc.SubagentTaskModelSourceSubagentConfiguration + SubagentTaskModelSourceTaskArgument = rpc.SubagentTaskModelSourceTaskArgument + SubagentTaskModelSourceUnset = rpc.SubagentTaskModelSourceUnset + SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper + SystemMessageRoleSystem = rpc.SystemMessageRoleSystem + SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted + SystemNotificationAgentCompletedStatusFailed = rpc.SystemNotificationAgentCompletedStatusFailed + SystemNotificationTypeAgentCompleted = rpc.SystemNotificationTypeAgentCompleted + SystemNotificationTypeAgentIdle = rpc.SystemNotificationTypeAgentIdle + SystemNotificationTypeInstructionDiscovered = rpc.SystemNotificationTypeInstructionDiscovered + SystemNotificationTypeNewInboxMessage = rpc.SystemNotificationTypeNewInboxMessage + SystemNotificationTypeShellCompleted = rpc.SystemNotificationTypeShellCompleted + SystemNotificationTypeShellDetachedCompleted = rpc.SystemNotificationTypeShellDetachedCompleted + SystemNotificationTypeUnclassified = rpc.SystemNotificationTypeUnclassified + SystemNotificationTypeWorkflowCompleted = rpc.SystemNotificationTypeWorkflowCompleted + SystemNotificationWorkflowCompletedStatusCancelled = rpc.SystemNotificationWorkflowCompletedStatusCancelled + SystemNotificationWorkflowCompletedStatusCompleted = rpc.SystemNotificationWorkflowCompletedStatusCompleted + SystemNotificationWorkflowCompletedStatusError = rpc.SystemNotificationWorkflowCompletedStatusError + SystemNotificationWorkflowCompletedStatusHalted = rpc.SystemNotificationWorkflowCompletedStatusHalted + SystemNotificationWorkflowCompletedStatusPaused = rpc.SystemNotificationWorkflowCompletedStatusPaused + SystemNotificationWorkflowPauseInfoTypeCheckpoint = rpc.SystemNotificationWorkflowPauseInfoTypeCheckpoint + SystemNotificationWorkflowPauseInfoTypeUser = rpc.SystemNotificationWorkflowPauseInfoTypeUser + TaskBlockerKindPermissionRecovery = rpc.TaskBlockerKindPermissionRecovery + TaskCompletionOutcomeBlocked = rpc.TaskCompletionOutcomeBlocked + TaskCompletionOutcomeCompleted = rpc.TaskCompletionOutcomeCompleted + TaskCompletionOutcomeContinue = rpc.TaskCompletionOutcomeContinue + ToolExecutionCompleteContentResourceLinkIconThemeDark = rpc.ToolExecutionCompleteContentResourceLinkIconThemeDark + ToolExecutionCompleteContentResourceLinkIconThemeLight = rpc.ToolExecutionCompleteContentResourceLinkIconThemeLight + ToolExecutionCompleteContentTypeAudio = rpc.ToolExecutionCompleteContentTypeAudio + ToolExecutionCompleteContentTypeImage = rpc.ToolExecutionCompleteContentTypeImage + ToolExecutionCompleteContentTypeResource = rpc.ToolExecutionCompleteContentTypeResource + ToolExecutionCompleteContentTypeResourceLink = rpc.ToolExecutionCompleteContentTypeResourceLink + ToolExecutionCompleteContentTypeShellExit = rpc.ToolExecutionCompleteContentTypeShellExit + ToolExecutionCompleteContentTypeTerminal = rpc.ToolExecutionCompleteContentTypeTerminal + ToolExecutionCompleteContentTypeText = rpc.ToolExecutionCompleteContentTypeText + ToolExecutionCompleteFileEditKindCreate = rpc.ToolExecutionCompleteFileEditKindCreate + ToolExecutionCompleteFileEditKindDelete = rpc.ToolExecutionCompleteFileEditKindDelete + ToolExecutionCompleteFileEditKindEdit = rpc.ToolExecutionCompleteFileEditKindEdit + ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp + ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel + ToolExecutionStartToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityApp + ToolExecutionStartToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityModel + ToolShellOutputStreamStderr = rpc.ToolShellOutputStreamStderr + ToolShellOutputStreamStdout = rpc.ToolShellOutputStreamStdout + ToolShellOutputStreamTerminal = rpc.ToolShellOutputStreamTerminal + UIEphemeralQueryPhaseAborted = rpc.UIEphemeralQueryPhaseAborted + UIEphemeralQueryPhaseChunk = rpc.UIEphemeralQueryPhaseChunk + UIEphemeralQueryPhaseCompleted = rpc.UIEphemeralQueryPhaseCompleted + UIEphemeralQueryPhaseFailed = rpc.UIEphemeralQueryPhaseFailed + UIEphemeralQueryPhaseStarted = rpc.UIEphemeralQueryPhaseStarted + UserMessageAgentModeAutopilot = rpc.UserMessageAgentModeAutopilot + UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive + UserMessageAgentModePlan = rpc.UserMessageAgentModePlan + UserMessageAgentModeShell = rpc.UserMessageAgentModeShell + UserMessageDeliveryIdle = rpc.UserMessageDeliveryIdle + UserMessageDeliveryQueued = rpc.UserMessageDeliveryQueued + UserMessageDeliverySteering = rpc.UserMessageDeliverySteering + UserToolSessionApprovalKindCommands = rpc.UserToolSessionApprovalKindCommands + UserToolSessionApprovalKindCustomTool = rpc.UserToolSessionApprovalKindCustomTool + UserToolSessionApprovalKindExtensionEnvAccess = rpc.UserToolSessionApprovalKindExtensionEnvAccess + UserToolSessionApprovalKindExtensionManagement = rpc.UserToolSessionApprovalKindExtensionManagement + UserToolSessionApprovalKindExtensionPermissionAccess = rpc.UserToolSessionApprovalKindExtensionPermissionAccess + UserToolSessionApprovalKindMCP = rpc.UserToolSessionApprovalKindMCP + UserToolSessionApprovalKindMemory = rpc.UserToolSessionApprovalKindMemory + UserToolSessionApprovalKindRead = rpc.UserToolSessionApprovalKindRead + UserToolSessionApprovalKindWorkflow = rpc.UserToolSessionApprovalKindWorkflow + UserToolSessionApprovalKindWrite = rpc.UserToolSessionApprovalKindWrite + VerbosityHigh = rpc.VerbosityHigh + VerbosityLow = rpc.VerbosityLow + VerbosityMedium = rpc.VerbosityMedium + WorkflowPermissionOperationAuthor = rpc.WorkflowPermissionOperationAuthor + WorkflowPermissionOperationRun = rpc.WorkflowPermissionOperationRun + WorkflowRunSettledStatusCancelled = rpc.WorkflowRunSettledStatusCancelled + WorkflowRunSettledStatusCompleted = rpc.WorkflowRunSettledStatusCompleted + WorkflowRunSettledStatusError = rpc.WorkflowRunSettledStatusError + WorkflowRunSettledStatusHalted = rpc.WorkflowRunSettledStatusHalted + WorkflowRunSettledStatusPaused = rpc.WorkflowRunSettledStatusPaused + WorkingDirectoryContextHostTypeADO = rpc.WorkingDirectoryContextHostTypeADO + WorkingDirectoryContextHostTypeGitHub = rpc.WorkingDirectoryContextHostTypeGitHub + WorkspaceFileChangedOperationCreate = rpc.WorkspaceFileChangedOperationCreate + WorkspaceFileChangedOperationUpdate = rpc.WorkspaceFileChangedOperationUpdate ) diff --git a/java/AGENTS.md b/java/AGENTS.md index 4b26573ecd..45b9fb9709 100644 --- a/java/AGENTS.md +++ b/java/AGENTS.md @@ -62,9 +62,11 @@ Run `verify` without `-q` or piping through `grep` so failures remain visible. source changes. Tests must exercise public APIs, not expose internals solely for tests. -For replay-backed Java integration tests, use the on-demand +Add new Java E2E tests for Java SDK surface behavior, not shared runtime +functionality; new SDK-accessible runtime E2Es belong in +`../nodejs/test/e2e/`. For replay-backed Java integration tests, use the on-demand [`sdk-java-e2e-test` skill](../.github/skills/sdk-java-e2e-test/SKILL.md). -Its snapshot workflow and companion examples are not required for unrelated +Its snapshot workflow is not required for unrelated Java edits. For JDK 17 compatibility testing, run the JDK 25-built artifact on JDK 17 without recompiling it, following [Development Setup](README.md#development-setup). diff --git a/java/README.md b/java/README.md index 2df6547446..18749e0ac2 100644 --- a/java/README.md +++ b/java/README.md @@ -15,6 +15,8 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. The Java SDK tracks the official GitHub Copilot SDK family (TypeScript, Python, Go, .NET, and Rust). +The generated `SessionFsSetProviderCapabilities.binary` field describes the runtime protocol, not a Java provider API. The Java SDK cannot currently register a session filesystem provider or handle `sessionFs.readFileBytes` or `sessionFs.writeFileBytes`; do not advertise binary operations from Java. Java provider and binary support is deferred until the SDK exposes provider registration and reverse-RPC dispatch. Provider-only images currently require a provider-capable SDK. + ## Prerequisites To use the SDK, you'll need: @@ -74,7 +76,7 @@ implementation 'com.github:copilot-sdk-java:1.0.15-preview.1-SNAPSHOT' ## In-process mode (experimental) -The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and supported on **linux-x64** (glibc), **linux-arm64** (glibc), **linuxmusl-x64**, **win32-x64**, **win32-arm64**, **darwin-x64**, and **darwin-arm64**. +The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and supported on **linux-x64** (glibc), **linux-arm64** (glibc), **linuxmusl-x64**, **linuxmusl-arm64**, **win32-x64**, **win32-arm64**, **darwin-x64**, and **darwin-arm64**. Because in-process mode is experimental, see the [Using experimental APIs](#using-experimental-apis) section for how to opt in. @@ -97,7 +99,7 @@ Add both the SDK and the platform-specific native runtime to your project: ${copilot.version} linux-x64 - + net.java.dev.jna @@ -121,6 +123,18 @@ client.start().get(); ## Quick Start +For experimental in-process AHP hosting, select a transport explicitly: +`client.startAhpHost(new AhpHostOptions().setLocalServer(new HostLocalServerOptions(null, null, null, null)))`. +The transport types are in `com.github.copilot.generated.rpc`. For GitHub Mission +Control, use `.setGithubEnvironment(new HostGitHubEnvironmentOptions("My host", "compute-id"))` +instead, or configure both transports. GitHub environment name and compute ID are +required; there is no implicit local listener. The host's `getUrl()`, `getToken()`, +and `getPid()` may return `null`; `getEnvironmentId()` returns 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. + ```java import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; @@ -178,16 +192,79 @@ directly. `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. +### Subagent lifecycle hooks + +Register `setOnSubagentStart` and `setOnSubagentStop` on `SessionHooks` to inspect +the parent session and subagent metadata around each child run: + +```java +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SessionHooks; +import com.github.copilot.rpc.SubagentStartHookOutput; +import com.github.copilot.rpc.SubagentStopHookOutput; +import java.util.concurrent.CompletableFuture; + +var hooks = new SessionHooks() + .setOnSubagentStart((input, invocation) -> + CompletableFuture.completedFuture( + new SubagentStartHookOutput("Check the relevant tests first."))) + .setOnSubagentStop((input, invocation) -> + CompletableFuture.completedFuture( + new SubagentStopHookOutput(null, null, input.response()))); +var config = new SessionConfig().setHooks(hooks); +``` + +`SubagentStartHookInput` includes the parent `sessionId`, `timestamp`, `cwd`, +`transcriptPath`, `agentName`, and optional display name and description. Its +`additionalContext` output is prepended to the child's first prompt. +`SubagentStopHookInput` also includes `agentType`, optional `agentId`, +`stopReason`, and the last assistant `response`. Return a stop output with +`decision` set to `"block"` and a nonempty `reason` to request another child +turn, or set `modifiedResponse` to replace the final response. A block takes +precedence over a rewrite. + `CopilotClientOptions.setExtensionLaunchProvider(...)` configures an experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can be created. +### Installation confirmation (experimental) + +`CopilotClientOptions.setInstallationConfirmationHandler(...)` configures the +connection-global `installations.confirm` receiver. The handler receives the +generated `InstallationConfirmationRequest` and an +`InstallationConfirmationContext`, then returns only an explicit +generated `InstallationDecision.CONFIRM`, `InstallationDecision.DECLINE` or +`InstallationDecision.CANCEL`. The SDK echoes the original challenge and review +fingerprint; it never infers approval and does not enable installation +capabilities or call a runtime registration RPC. + +Match `operationId` and `policySessionId` against the original action on this +exact connection before presenting the complete review. Missing legacy session +metadata does not select a default session. Refuse unknown operations or +incomplete reviews. + +Concurrent reviews are independent and do not block other client-global RPCs. +`context.getCancelled()` returns the single cancellation signal for the review. +It completes when the runtime's numeric `$/cancelRequest`, runtime-enforced +expiry, or loss of the original connection retires the review. Separately +spawned UI work should observe this signal and retire itself when it completes. +Dropping an outbound installation or OAuth future does not cancel that +operation. + `SessionConfig.setAskUserVariant(AskUserVariant.ELICITATION)` selects the structured form-based `ask_user` tool when an elicitation handler is also set. The default is `AskUserVariant.LEGACY`. Re-supply the option and handler through `ResumeSessionConfig` on a cold resume. +`ResumeSessionConfig.setContinuePendingWork(false)` interrupts work still in +flight when resuming (the default), while preserving completed tool results +already durably recorded by the runtime. +Pass `new ResumeSessionConfig().setContinuePendingWork(true)` to +`resumeSession` to keep waiting for pending tool calls and permission requests +instead. Leave the option unset to use the runtime default. Re-register any +external tools needed to handle continued work. + For rotating per-session GitHub credentials, use `SessionConfig.setGitHubTokenProvider(...)` (or the equivalent `ResumeSessionConfig` setter) instead of `setGitHubToken(...)`: @@ -209,6 +286,69 @@ provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +### Skill providers (experimental) + +`SessionConfig.setSkillProvider(...)` registers session-scoped skills that the +runtime can list and read on demand. The API is experimental; see +[Using experimental APIs](#using-experimental-apis) before compiling code that +references it. + +```java +import com.github.copilot.SkillProvider; +import com.github.copilot.SkillProviderDescriptor; + +SkillProvider provider = new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of( + new SkillProviderDescriptor("team-plan", "Team planning guidance", true, false, null))); + } + + @Override + public CompletableFuture readSkill(String name) { + if (!name.equals("team-plan")) { + return CompletableFuture.completedFuture(null); // not found + } + return CompletableFuture.completedFuture("# Team plan\nWrite a concise monthly team plan."); + } +}; + +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setEnableSkills(true) + .setSkillProvider(provider); +``` + +Provider callbacks may be invoked concurrently, so implementations should be +thread-safe. When the runtime cancels a call, for example after its 30-second +limit or when the session disconnects, the SDK calls `cancel(true)` on the +returned future; check `isCancelled()` or attach a completion callback to stop +early. The provider is not persisted with the session; pass it again via +`ResumeSessionConfig.setSkillProvider(...)` on every resume. Resuming without a +provider unbinds any provider the session had. In +`CopilotClientMode.EMPTY`, explicitly set `setEnableSkills(true)` if the +assistant should use skills, because empty mode disables skill loading by +default. Skill providers are local-session only and are rejected for cloud +sessions. + +### Typed MCP installation and removal payloads (breaking change) + +Three payloads in the experimental MCP installation and removal workflow are now sealed +interfaces with one record per variant, instead of `Object`, which brings Java into line +with the other SDKs. No other generated type changes. + +| Field | Before | After | +| --- | --- | --- | +| `InstallationConfirmationRequest.review()` | `Object` | `InstallationReview` sealed interface (`InstallationReviewMcp` / `InstallationReviewSkill`, by `resource`); the MCP variant carries `McpInstallationReview` (`McpInstallationReviewInstall` / `McpInstallationReviewUninstall`, by `action`) and the Skill variant carries `SkillInstallationReview` | +| `McpInstallPlan.transportChoices()` | `List` | `List` (`McpPlanTransportChoicePackage` / `McpPlanTransportChoiceRemote`, by `installMethod`) | +| `McpInstallationManagementResultOutcome.getOutcome()` / `setOutcome(...)` | `Object` | `McpInstallationManagementOutcome`, whose operation variant carries `McpInstallationOperationStatus` (by `phase`) | + +## System Message Customization + +Use `SessionConfig.setSystemMessage(...)` with a `SystemMessageConfig` in `SystemMessageMode.CUSTOMIZE` to override individual prompt sections. Section keys are defined by `SystemMessageSections`; use `SectionOverride.setAction(...)` for static overrides or `SectionOverride.setTransform(...)` for a transform callback. + +`SystemMessageSections.LAST_INSTRUCTIONS` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a transform callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SystemMessageSections.RUNTIME_INSTRUCTIONS` is a separate section: removing it does not remove `SystemMessageSections.LAST_INSTRUCTIONS`. + ## Message source Use `MessageSource.SYSTEM` for application-generated system context and @@ -255,6 +395,17 @@ Inventory inventory = session.sendAndWait( ).get(); ``` +On resume, transcript recovery defaults to true in all modes. +Use `ResumeSessionConfig.setAllowTranscriptRecovery(false)` to reject recovery. +A repaired session exposes +`getTranscriptRecovery()` (or null), with `plannedBackupPath`, +`invalidLineNumbers` (including discarded torn-tail lines), and +`sessionStartMoved`. A rejected resume retains the existing error message; +`JsonRpcException.getCode()` and `getData()` expose the server's code and +`invalidLineNumbers` / `sessionStartMoved` data. +Disabling recovery still permits adding a missing newline after an intact final +record; it rejects torn tails. + Enable annotation processing with `CopilotResponseProcessor` (automatically discoverable alongside the SDK's existing processors), and opt in to experimental APIs as described below. The processor reuses the custom-tool `SchemaGenerator`, @@ -410,6 +561,54 @@ Chain fluent modifiers to set tool options: For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md). +### String-schema `apply_patch` overrides + +An explicit `apply_patch` override can declare `Map.of("type", "string")`. +The model sees a required `input` property, but the runtime restores the scalar +patch text before dispatch. Use `ToolInvocation.getArgumentsAs(String.class)` +to read it; `getArguments()` is for object-shaped arguments. This example +returns trimmed patch text; replace the handler body with your own patch +implementation: + +```java +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import com.github.copilot.rpc.ToolDefinition; + +ToolDefinition applyPatch = ToolDefinition.create("apply_patch", "Apply a patch", Map.of("type", "string"), + invocation -> { + String patch = invocation.getArgumentsAs(String.class); + return CompletableFuture.completedFuture(patch.trim()); + }).overridesBuiltInTool(true); +``` + +Register this definition with `SessionConfig.setTools(...)`. String-schema +`apply_patch` overrides cannot contain JSON Schema references; use an object +schema if references are needed. + +## Changing tools on a live session (experimental) + +`CopilotSession.setTools(List)` replaces the external tools +registered by this Java client without recreating the session: + +```java +session.setTools(List.of( + ToolDefinition.from("lookup_fruit", "Looks up fruit by code", + Param.of(Integer.class, "code", "Fruit code"), + code -> "dragonfruit") +)).get(); +``` + +The list is a complete replacement for this client only. Built-in tools, MCP +tools, plugin tools, and tools registered by other connections are unchanged. +Pass an empty list to remove this client's tools. Handlers switch after the +runtime accepts the replacement; rejected replacements leave the previous +handlers in place. Tool calls already running finish on the handler that started +them. + +For cross-SDK behavior and active-turn caveats, see +[changing tools](../docs/features/changing-tools.md). + ## Auto routing tiers Use `CapiSessionOptions.setAutoTier(...)` to select `AutoTier.EFFICIENCY`, @@ -564,6 +763,11 @@ observers share the failed future. Some SDK APIs are marked as experimental with `@CopilotExperimental`. These APIs may change or be removed in future versions without notice. +An otherwise stable session event can contain experimental properties. Generated +event payload records preserve each property's schema stability marker on its +record component and generated accessor; the enclosing event and its other +properties remain stable. + By default, referencing an experimental API from your code causes a **compile-time error**: ``` @@ -716,7 +920,7 @@ CI enforces both checks. Spotless runs explicitly in CI; `mvn verify` alone does Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js in addition to JDK 25 and Maven. In a standalone SDK checkout, `copilot-native/scripts/fetch-native.mjs` retrieves the pinned runtime package from the corresponding GitHub release. When the SDK is nested in `copilot-agent-runtime`, it instead stages the same-checkout artifacts from `dist-cli`; run `pnpm run build:cli` from the runtime repository first. -On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On a Linux musl x64 host, Maven activates `native-linuxmusl-x64` when `copilot.native.libc=musl` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, stages the platform package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. +On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On a Linux musl host, Maven activates `native-linuxmusl-x64` or `native-linuxmusl-arm64` for the matching architecture when `copilot.native.libc=musl` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, stages the platform package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Before opting in, validate that Node.js reports glibc for the build host: @@ -751,14 +955,14 @@ node copilot-native/scripts/validate-native-host.mjs linux-arm64 mvn -Pinprocess clean verify -Dcopilot.native.libc=glibc ``` -The same command validates in-process mode on Linux musl x64: +The same command validates in-process mode on Linux musl x64 or ARM64: ```bash -node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 +node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 # Use linuxmusl-arm64 on ARM64 mvn -Pinprocess clean verify -Dcopilot.native.libc=musl ``` -On Linux musl ARM64 and other unsupported hosts, do not set `copilot.native.libc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. +On unsupported hosts, do not set `copilot.native.libc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. To build only the OS-neutral artifacts on any host, or override the glibc opt-in, disable native download and packaging: @@ -780,17 +984,18 @@ Each classifier JAR includes `runtime.node`, `platform.properties`, and `copilot ### Versioning and releases -The Java SDK uses [Maven CI-friendly versions](https://maven.apache.org/maven-ci-friendly.html). Every module declares `${revision}`, and the single source of truth is the `` property in `java/pom.xml`. The committed value stays a `-SNAPSHOT` (for example `1.0.14-SNAPSHOT`) and is only used for local development and the daily snapshot publish. +The Java SDK uses [Maven CI-friendly versions](https://maven.apache.org/maven-ci-friendly.html). Every module declares `${revision}`, and the single source of truth is the `` property in `java/pom.xml`. The committed value stays a `-SNAPSHOT` for local development. Published artifacts contain a concrete version rather than the unresolved `${revision}` property. -Releasing is intentionally a **read-only** operation that never mutates the repository: +Stable, prerelease, and public unstable releases include all six SDKs. SDK and runtime versions are numbered independently; each Java release contains the matching runtime artifacts in its native classifier JARs. Public unstable versions use `X.Y.Z-unstable..g`. Maven `-SNAPSHOT` builds are a separate development channel. -- The release version is computed by the shared release pipeline (`.github/workflows/publish.yml`) — the same version used by every other language SDK — and injected at build time with `-Drevision=X.Y.Z`. The POM is **not** edited or committed. -- `.github/workflows/java-publish-maven.yml` builds every native classifier and the primary artifact from a single immutable source commit and publishes to Maven Central. It creates no commits, no branch-protection bypass, and requires no elevated repository token. -- The `java/vX.Y.Z` traceability tag and the cross-language `vX.Y.Z` GitHub Release are created by `publish.yml` **after** publication succeeds, pointing at the original release commit. +Release artifacts and source references are public: -For an independent Java publication retry, dispatch `java-publish-maven.yml` from `main` with the original `releaseVersion` and full `sourceSha`. The source must be a commit already in `main`'s history. Unmerged commits, branch names, and tag names are rejected before builds run. +- Java packages are available from Maven Central. +- The `java/v` and `v` tags identify the corresponding public SDK source snapshot in [`github/copilot-sdk`](https://github.com/github/copilot-sdk). +- Stable/prerelease versions also have a combined `v` [GitHub release](https://github.com/github/copilot-sdk/releases) and versioned Java documentation. Prerelease documentation does not replace the latest documentation. +- Unstable releases create source tags without advancing SDK `main`, creating an SDK GitHub release announcement, or deploying Java documentation. Their runtime assets are available in the separate `runtime-` release. -Because there is no `maven-release-plugin` and no `release:prepare` ceremony, the POM deliberately does not track the "next" release version. To validate a build with an explicit version locally, without publishing: +To validate a build with an explicit version locally, without changing the checked-in POM or publishing: ```bash # Build and verify with an explicit version, without touching the POM @@ -802,7 +1007,7 @@ cat sdk/.flattened-pom.xml copilot-native/.flattened-pom.xml These commands do not upload artifacts. Do not use `deploy` for local validation: the Central publishing plugin is configured with `autoPublish=true`. -`flatten-maven-plugin` (ossrh mode) resolves `${revision}` into the installed and published POMs, so downstream consumers never see the unresolved property. Documentation version references are updated through a normal reviewed pull request (see `scripts/update-documentation-versions.sh`), not as a side effect of publishing. +`flatten-maven-plugin` (ossrh mode) resolves `${revision}` into the installed and published POMs. Documentation version references are updated through a normal reviewed pull request (see `scripts/update-documentation-versions.sh`), not as a side effect of publishing. ## License diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 2c750e4e47..41af2dd6cb 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -467,6 +467,64 @@ + + native-linuxmusl-arm64 + + + Linux + aarch64 + + + copilot.native.libc + musl + + + + linuxmusl-arm64 + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-native-host + validate + + + fetch-native + generate-resources + + + test-fetch-native + test + + + + + org.apache.maven.plugins + maven-jar-plugin + + + jar-native + package + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-native-jars + package + + + + + + native-win32-x64 @@ -809,6 +867,68 @@ + + + attach-external-linuxmusl-arm64-classifier + + + copilot.native.external.linuxmusl.arm64.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-linuxmusl-arm64-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + linuxmusl-arm64 + ${copilot.native.external.linuxmusl.arm64.classifier.path} + ${project.build.finalName}-linuxmusl-arm64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-linuxmusl-arm64-classifier + package + + attach-artifact + + + + + ${copilot.native.external.linuxmusl.arm64.classifier.path} + jar + linuxmusl-arm64 + + + + + + + + + + jna + ${project.build.directory}/consumer-dependencies + + + + @@ -680,7 +701,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 @@ -720,6 +740,22 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the linuxmusl-x64 + + native-linuxmusl-arm64 + + + Linux + aarch64 + + + copilot.native.libc + musl + + + + linuxmusl-arm64 + + native-win32-x64 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 5ad9beee17..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; /** @@ -46,6 +47,9 @@ public record AssistantFusionPhaseCompletedEventData( @JsonProperty("conversationScope") FusionConversationScope conversationScope, /** 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, /** Provider-normalized textual output produced by the phase. */ 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 17de3c7190..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; /** @@ -46,6 +47,9 @@ public record AssistantFusionPhaseFailedEventData( @JsonProperty("conversationScope") FusionConversationScope conversationScope, /** 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, /** Stable machine-readable reason for the phase failure. */ 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 88c4064a73..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; /** @@ -47,7 +48,10 @@ public record AssistantFusionPhaseStartedEventData( /** Conversation scope in which the phase executes. */ @JsonProperty("conversationScope") FusionConversationScope conversationScope, /** Concrete model executing the phase. */ - @JsonProperty("model") String model + @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/AssistantTurnEndEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnEndEvent.java index 082f62b476..8ef01eccde 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnEndEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnEndEvent.java @@ -37,7 +37,9 @@ public record AssistantTurnEndEventData( /** Identifier of the turn that has ended, matching the corresponding assistant.turn_start event */ @JsonProperty("turnId") String turnId, /** Model identifier used for this turn, when known */ - @JsonProperty("model") String model + @JsonProperty("model") String model, + /** Parent task tool call ID when this turn belongs to a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId ) { } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java index a9c6b2932d..d020391606 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java @@ -39,7 +39,9 @@ public record AssistantTurnStartEventData( /** Model identifier used for this turn, when known */ @JsonProperty("model") String model, /** CAPI interaction ID for correlating this turn with upstream telemetry */ - @JsonProperty("interactionId") String interactionId + @JsonProperty("interactionId") String interactionId, + /** Parent task tool call ID when this turn belongs to a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId ) { } } 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/FusionCritic.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionCritic.java new file mode 100644 index 0000000000..b6a7f2e368 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionCritic.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Data type {@code FusionCritic}. + * + * @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 FusionCritic( + /** Unique execution phase identifier for this critic. */ + @JsonProperty("phaseId") String phaseId, + /** Concrete model selected for this critic. */ + @JsonProperty("model") String model, + /** Explicit reasoning effort selected for this critic, if supplied. */ + @JsonProperty("reasoningEffort") String reasoningEffort +) { +} 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/HumanResponseRecordedResponse.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java new file mode 100644 index 0000000000..65efb2e2e0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.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; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Exact runtime-owned question or reviewed plan paired with the typed response that settled it. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "responseKind", visible = true) +@JsonSubTypes({ + @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") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class HumanResponseRecordedResponse { + + /** + * Returns the discriminator value for this variant. + * + * @return the responseKind discriminator + */ + 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/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java index 5ea2c39f51..2354c95750 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, builtin, or managed + * Configuration source: user, workspace, plugin, builtin, managed, or account * * @since 1.0.0 */ @@ -25,7 +25,9 @@ public enum McpServerSource { /** The {@code builtin} variant. */ BUILTIN("builtin"), /** The {@code managed} variant. */ - MANAGED("managed"); + MANAGED("managed"), + /** The {@code account} variant. */ + ACCOUNT("account"); private final String value; McpServerSource(String value) { this.value = value; } 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 d3049cd472..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; @@ -37,6 +38,8 @@ public final class ModelCallFailureEvent extends SessionEvent { public record ModelCallFailureEventData( /** Model identifier used for the failed API call */ @JsonProperty("model") String model, + /** Parent task tool call ID when this failed model call belongs to a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId, /** What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ @JsonProperty("initiator") String initiator, /** Completion ID from the model provider (e.g., chatcmpl-abc123) */ @@ -63,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 */ @@ -84,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 a68e455420..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,7 +42,10 @@ 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. */ - @JsonProperty("fusion") FusionAttribution fusion + @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/ModelChangeSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelChangeSource.java index 6279057df9..7ee778b7fe 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ModelChangeSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelChangeSource.java @@ -39,7 +39,9 @@ public enum ModelChangeSource { /** The {@code changeboarding_shortcut} variant. */ CHANGEBOARDING_SHORTCUT("changeboarding_shortcut"), /** The {@code sdk} variant. */ - SDK("sdk"); + SDK("sdk"), + /** The {@code auto_tier_recommendation} variant. */ + AUTO_TIER_RECOMMENDATION("auto_tier_recommendation"); private final String value; ModelChangeSource(String value) { this.value = value; } 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 6e8d6aecb6..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,10 +10,11 @@ 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 event "permission.assentDetected". Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation. + * Session event "permission.assentDetected". Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -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 27a5d0df91..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,10 +10,11 @@ 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 event "permission.carriedForward". Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. + * Session event "permission.carriedForward". Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -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 2bee51662f..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,10 +10,11 @@ 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 event "permission.contextualAuthorization". Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify. + * Session event "permission.contextualAuthorization". Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -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 dc37375dcf..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,10 +10,11 @@ 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 event "permission.messageAuthorizationDegraded". Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. + * Session event "permission.messageAuthorizationDegraded". Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -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 c87dce05cb..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,11 +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 java.util.List; import javax.annotation.processing.Generated; /** - * Session event "permission.messageAuthorization". Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. + * Session event "permission.messageAuthorization". Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -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/PermissionMessageAuthorizationPolarity.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationPolarity.java index e142238e7b..59183322fa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationPolarity.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationPolarity.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * Which direction a message-backed authorization claim moves authority in. + * Direction stored in a historical extractor claim. Current runtimes do not apply it. * * @apiNote This type is experimental and may change in a future version. * 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 afa6ea4735..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,10 +10,11 @@ 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 event "permission.messageAuthorizationRead". Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction. + * Session event "permission.messageAuthorizationRead". Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -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/SessionCompactionCompleteEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompactionCompleteEvent.java index 6b21976b0c..046317ee26 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompactionCompleteEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompactionCompleteEvent.java @@ -54,7 +54,9 @@ public record SessionCompactionCompleteEventData( @JsonProperty("summaryContent") String summaryContent, /** Reasoning baseline on the replacement summary, preserved when replay skips the compacted history */ @JsonProperty("responsesReasoning") ResponsesReasoning responsesReasoning, - /** Authoritative active-factory reminder appended to the compacted context */ + /** Authoritative active-workflow reminder appended to the compacted context */ + @JsonProperty("activeWorkflowSummary") String activeWorkflowSummary, + /** Legacy active-workflow reminder retained for replay compatibility */ @JsonProperty("activeFactorySummary") String activeFactorySummary, /** Canonical model identifier used for model-specific behavior when replaying compaction */ @JsonProperty("behaviorModelId") String behaviorModelId, 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 b8419f2ab0..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,11 +153,12 @@ @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 = FactoryRunUpdatedEvent.class, name = "factory.run_updated"), - @JsonSubTypes.Type(value = FactoryRunStartedEvent.class, name = "factory.run_started"), - @JsonSubTypes.Type(value = FactoryRunSettledEvent.class, name = "factory.run_settled"), + @JsonSubTypes.Type(value = WorkflowRunUpdatedEvent.class, name = "workflow.run_updated"), + @JsonSubTypes.Type(value = WorkflowRunStartedEvent.class, name = "workflow.run_started"), + @JsonSubTypes.Type(value = WorkflowRunSettledEvent.class, name = "workflow.run_settled"), @JsonSubTypes.Type(value = SessionSkillsLoadedEvent.class, name = "session.skills_loaded"), @JsonSubTypes.Type(value = SessionCustomAgentsUpdatedEvent.class, name = "session.custom_agents_updated"), @JsonSubTypes.Type(value = SessionMcpServersLoadedEvent.class, name = "session.mcp_servers_loaded"), @@ -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,11 +308,12 @@ public abstract sealed class SessionEvent permits CapabilitiesChangedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent, + HumanResponseRecordedEvent, SessionToolsUpdatedEvent, SessionBackgroundTasksChangedEvent, - FactoryRunUpdatedEvent, - FactoryRunStartedEvent, - FactoryRunSettledEvent, + WorkflowRunUpdatedEvent, + WorkflowRunStartedEvent, + WorkflowRunSettledEvent, SessionSkillsLoadedEvent, SessionCustomAgentsUpdatedEvent, SessionMcpServersLoadedEvent, 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 d9f5ff91cd..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,13 +65,24 @@ 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 the review or judge phase, when required. */ + /** 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, /** Concrete model recommended for eligible follow-up turns. */ 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 b084652db1..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 @@ -39,7 +39,11 @@ public record SessionMcpServerStatusChangedEventData( /** Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured */ @JsonProperty("status") McpServerStatus status, /** Error message if the server entered a failed state */ - @JsonProperty("error") String error + @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 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/SubagentStartedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentStartedEvent.java index 7d8f5afe9e..8c8cde1c90 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentStartedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentStartedEvent.java @@ -44,12 +44,14 @@ public record SubagentStartedEventData( @JsonProperty("agentDescription") String agentDescription, /** Model the sub-agent will run with, when known at start. */ @JsonProperty("model") String model, - /** Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and factory agents); absent for sub-agents created through other runtime paths. */ + /** Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and workflow agents); absent for sub-agents created through other runtime paths. */ @JsonProperty("taskModelSource") SubagentTaskModelSource taskModelSource, /** Authority or runtime mechanism responsible for sub-agent model selection, when known at start. */ @JsonProperty("modelSelectionSource") SubagentModelSelectionSource modelSelectionSource, - /** Root id of the factory run that spawned this sub-agent, when it was spawned by one. */ + /** Legacy root id of the workflow run that spawned this sub-agent. New consumers should use workflowRunId. */ @JsonProperty("factoryRunId") String factoryRunId, + /** Root id of the workflow run that spawned this sub-agent, when it was spawned by one. */ + @JsonProperty("workflowRunId") String workflowRunId, /** Task-registry ID of the spawning sub-agent. Absent when the root session spawned this child. */ @JsonProperty("parentId") String parentId, /** Whether this sub-agent can be resumed. Currently always false. */ 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/FactoryRunSettledEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunSettledEvent.java similarity index 68% rename from java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledEvent.java rename to java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunSettledEvent.java index 58ffa9cb3c..7cfef0df02 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunSettledEvent.java @@ -13,38 +13,38 @@ import javax.annotation.processing.Generated; /** - * Session event "factory.run_settled". Ephemeral signal that a factory run reached a terminal status. + * Session event "workflow.run_settled". Ephemeral signal that a workflow run reached a terminal status. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class FactoryRunSettledEvent extends SessionEvent { +public final class WorkflowRunSettledEvent extends SessionEvent { @Override - public String getType() { return "factory.run_settled"; } + public String getType() { return "workflow.run_settled"; } @JsonProperty("data") - private FactoryRunSettledEventData data; + private WorkflowRunSettledEventData data; - public FactoryRunSettledEventData getData() { return data; } - public void setData(FactoryRunSettledEventData data) { this.data = data; } + public WorkflowRunSettledEventData getData() { return data; } + public void setData(WorkflowRunSettledEventData data) { this.data = data; } - /** Data payload for {@link FactoryRunSettledEvent}. */ + /** Data payload for {@link WorkflowRunSettledEvent}. */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) - public record FactoryRunSettledEventData( - /** Identifier of the factory run that settled. */ + public record WorkflowRunSettledEventData( + /** Identifier of the workflow run that settled. */ @JsonProperty("runId") String runId, /** Terminal status the run committed. */ - @JsonProperty("status") FactoryRunSettledStatus status, + @JsonProperty("status") WorkflowRunSettledStatus status, /** Subagents this run consumed against its limits. */ @JsonProperty("consumedSubagents") Long consumedSubagents, /** AI credits this run consumed, in nano-AIU. */ @JsonProperty("consumedNanoAiu") Long consumedNanoAiu, /** Active milliseconds accumulated across every attempt of this run. */ @JsonProperty("elapsedMs") Long elapsedMs, - /** Typed failure class recorded on the run, when it failed with one (e.g. `factory_limit_reached`). */ + /** Typed failure class recorded on the run, when it failed with one (e.g. `workflow_limit_reached`). */ @JsonProperty("failureType") String failureType ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunSettledStatus.java similarity index 68% rename from java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledStatus.java rename to java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunSettledStatus.java index 828ddc9ee3..05dee5d53a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunSettledStatus.java @@ -10,12 +10,12 @@ import javax.annotation.processing.Generated; /** - * Terminal status a factory run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. + * 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. * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum FactoryRunSettledStatus { +public enum WorkflowRunSettledStatus { /** The {@code completed} variant. */ COMPLETED("completed"), /** The {@code halted} variant. */ @@ -28,14 +28,14 @@ public enum FactoryRunSettledStatus { ERROR("error"); private final String value; - FactoryRunSettledStatus(String value) { this.value = value; } + WorkflowRunSettledStatus(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator - public static FactoryRunSettledStatus fromValue(String value) { - for (FactoryRunSettledStatus v : values()) { + public static WorkflowRunSettledStatus fromValue(String value) { + for (WorkflowRunSettledStatus v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown FactoryRunSettledStatus value: " + value); + throw new IllegalArgumentException("Unknown WorkflowRunSettledStatus value: " + value); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunStartedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunStartedEvent.java similarity index 59% rename from java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunStartedEvent.java rename to java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunStartedEvent.java index 691c7185d4..656b51f596 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunStartedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunStartedEvent.java @@ -13,31 +13,31 @@ import javax.annotation.processing.Generated; /** - * Session event "factory.run_started". Ephemeral signal that a factory run attempt began executing. + * Session event "workflow.run_started". Ephemeral signal that a workflow run attempt began executing. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class FactoryRunStartedEvent extends SessionEvent { +public final class WorkflowRunStartedEvent extends SessionEvent { @Override - public String getType() { return "factory.run_started"; } + public String getType() { return "workflow.run_started"; } @JsonProperty("data") - private FactoryRunStartedEventData data; + private WorkflowRunStartedEventData data; - public FactoryRunStartedEventData getData() { return data; } - public void setData(FactoryRunStartedEventData data) { this.data = data; } + public WorkflowRunStartedEventData getData() { return data; } + public void setData(WorkflowRunStartedEventData data) { this.data = data; } - /** Data payload for {@link FactoryRunStartedEvent}. */ + /** Data payload for {@link WorkflowRunStartedEvent}. */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) - public record FactoryRunStartedEventData( - /** Identifier of the factory run that started. */ + public record WorkflowRunStartedEventData( + /** Identifier of the workflow run that started. */ @JsonProperty("runId") String runId, - /** Name of the factory this run executes. Low cardinality by construction. */ - @JsonProperty("factoryName") String factoryName, + /** Name of the workflow this run executes. Low cardinality by construction. */ + @JsonProperty("workflowName") String workflowName, /** Attempt number this start committed; a resumed run increments it. */ @JsonProperty("attempt") Long attempt ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunUpdatedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunUpdatedEvent.java similarity index 65% rename from java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunUpdatedEvent.java rename to java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunUpdatedEvent.java index 33b542058b..6a51db49f6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunUpdatedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkflowRunUpdatedEvent.java @@ -13,28 +13,28 @@ import javax.annotation.processing.Generated; /** - * Session event "factory.run_updated". Ephemeral invalidation signal for a changed factory run. + * Session event "workflow.run_updated". Ephemeral invalidation signal for a changed workflow run. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class FactoryRunUpdatedEvent extends SessionEvent { +public final class WorkflowRunUpdatedEvent extends SessionEvent { @Override - public String getType() { return "factory.run_updated"; } + public String getType() { return "workflow.run_updated"; } @JsonProperty("data") - private FactoryRunUpdatedEventData data; + private WorkflowRunUpdatedEventData data; - public FactoryRunUpdatedEventData getData() { return data; } - public void setData(FactoryRunUpdatedEventData data) { this.data = data; } + public WorkflowRunUpdatedEventData getData() { return data; } + public void setData(WorkflowRunUpdatedEventData data) { this.data = data; } - /** Data payload for {@link FactoryRunUpdatedEvent}. */ + /** Data payload for {@link WorkflowRunUpdatedEvent}. */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) - public record FactoryRunUpdatedEventData( - /** Factory run identifier. */ + public record WorkflowRunUpdatedEventData( + /** Workflow run identifier. */ @JsonProperty("runId") String runId, /** Monotonic revision now available for the run. */ @JsonProperty("revision") Long revision 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/AccountKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountKind.java new file mode 100644 index 0000000000..e635762661 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountKind.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 com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The provider kind stamped on a signed-in account. + * + * @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 AccountKind { + /** The {@code githubDotCom} variant. */ + GITHUBDOTCOM("githubDotCom"), + /** The {@code proxima} variant. */ + PROXIMA("proxima"), + /** The {@code entraEmu} variant. */ + ENTRAEMU("entraEmu"), + /** The {@code entra} variant. */ + ENTRA("entra"), + /** The {@code loki} variant. */ + LOKI("loki"); + + private final String value; + AccountKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AccountKind fromValue(String value) { + for (AccountKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AccountKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountStatus.java similarity index 53% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentOptions.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountStatus.java index 35470e7199..cb0868ce74 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountStatus.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Options for one factory-scoped subagent call. + * One signed-in account in the roster forest. * * @apiNote This type is experimental and may change in a future version. * @@ -24,18 +24,18 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryAgentOptions( - /** Optional label distinguishing otherwise identical memoized agent calls. */ - @JsonProperty("label") String label, - /** Optional JSON Schema for structured agent output. */ - @JsonProperty("schema") Object schema, - /** Optional model identifier for the subagent. */ - @JsonProperty("model") String model, - /** Optional reasoning effort override for the subagent. */ - @JsonProperty("reasoningEffort") String reasoningEffort, - /** Optional context tier override for the subagent. */ - @JsonProperty("contextTier") ContextTier contextTier, - /** Optional built-in or custom agent name whose definition configures the subagent. */ - @JsonProperty("agent") String agent +public record AccountStatus( + /** Authentication host URL. */ + @JsonProperty("host") String host, + /** Authenticated login/username. */ + @JsonProperty("login") String login, + /** The provider kind of this account. */ + @JsonProperty("kind") AccountKind kind, + /** 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. */ + @JsonProperty("derivedFrom") String derivedFrom, + /** Whether this is the active account. */ + @JsonProperty("active") Boolean active, + /** Opaque selection id used to switch to, or log out, this account. */ + @JsonProperty("selectionId") String selectionId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionParams.java new file mode 100644 index 0000000000..1555b36663 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionParams.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 models a custom agent asks for, and the models actually 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) +record AgentsCustomAgentInitialModelDecisionParams( + /** The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. */ + @JsonProperty("agentModelsJson") String agentModelsJson, + /** The models available to this session, serialized in the shape the model list carries. */ + @JsonProperty("availableModelsJson") String availableModelsJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionResult.java new file mode 100644 index 0000000000..afd26e66af --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionResult.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 javax.annotation.processing.Generated; + +/** + * The model to switch to, and the warning to show when the agent's preference could not be met. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsCustomAgentInitialModelDecisionResult( + /** The first available model that matches the agent's preferences. Absent when none of the requested models is available. */ + @JsonProperty("targetModel") String targetModel, + /** The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. */ + @JsonProperty("warning") String warning +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsParams.java new file mode 100644 index 0000000000..fee8d13df9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsParams.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.Map; +import javax.annotation.processing.Generated; + +/** + * The feature flags to evaluate shipped agents against. + * + * @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 AgentsGetAvailableBuiltinsParams( + /** Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. */ + @JsonProperty("featureFlags") Map featureFlags, + /** Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. */ + @JsonProperty("overrides") Map overrides, + /** The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. */ + @JsonProperty("context") String context +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsResult.java new file mode 100644 index 0000000000..14667e1269 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * The shipped agents available under the requested flags. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetAvailableBuiltinsResult( + /** Available shipped agents, in the runtime's own order. */ + @JsonProperty("agents") List agents +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionParams.java new file mode 100644 index 0000000000..cc69529078 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionParams.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; + +/** + * The shipped agent whose definition to load. + * + * @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 AgentsGetBuiltinDefinitionParams( + /** The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionResult.java new file mode 100644 index 0000000000..2fd4d55fa3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionResult.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; + +/** + * One shipped agent's definition. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinDefinitionResult( + /** The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. */ + @JsonProperty("definitionJson") String definitionJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionParams.java new file mode 100644 index 0000000000..9258f87d1b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionParams.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; + +/** + * The shipped agent whose listing entry to load. + * + * @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 AgentsGetBuiltinListingDefinitionParams( + /** The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionResult.java new file mode 100644 index 0000000000..f49d37d53b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionResult.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; + +/** + * One shipped agent, projected for a listing. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinListingDefinitionResult( + /** The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. */ + @JsonProperty("definitionJson") String definitionJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinsResult.java new file mode 100644 index 0000000000..d0148ac6d7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinsResult.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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * The agents this runtime ships, named so a consumer can tell them apart from authored ones. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinsResult( + /** Every agent name this runtime ships. */ + @JsonProperty("names") List names, + /** The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. */ + @JsonProperty("disableableNames") List disableableNames, + /** The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. */ + @JsonProperty("yamlBasedNames") List yamlBasedNames +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValue.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValue.java new file mode 100644 index 0000000000..f84ccee737 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValue.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The enumerated collection, keyed by the same selector as the query. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AuthEnumerateValueAccounts.class, name = "accounts"), + @JsonSubTypes.Type(value = AuthEnumerateValueProviders.class, name = "providers") +}) +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class AuthEnumerateValue { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValueAccounts.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValueAccounts.java new file mode 100644 index 0000000000..11afc36f14 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValueAccounts.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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code accounts} of {@link AuthEnumerateValue}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthEnumerateValueAccounts extends AuthEnumerateValue { + + @JsonProperty("kind") + private final String kind = "accounts"; + + @Override + public String getKind() { return kind; } + + /** The signed-in account forest; empty when not logged in. */ + @JsonProperty("items") + private List items; + + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValueProviders.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValueProviders.java new file mode 100644 index 0000000000..965684e8ed --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthEnumerateValueProviders.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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code providers} of {@link AuthEnumerateValue}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthEnumerateValueProviders extends AuthEnumerateValue { + + @JsonProperty("kind") + private final String kind = "providers"; + + @Override + public String getKind() { return kind; } + + /** The providers offered for interactive login. */ + @JsonProperty("items") + private List items; + + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } +} 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..7768741926 --- /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) +public 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/FactoryLogLine.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentityMetadata.java similarity index 74% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLine.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentityMetadata.java index 1f7afdd583..7ccad64df8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLine.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentityMetadata.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * One ordered factory progress line. + * Credential-free identity metadata. * * @apiNote This type is experimental and may change in a future version. * @@ -24,12 +24,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryLogLine( - /** Monotonic sequence number within the factory run. */ - @JsonProperty("seq") Long seq, - /** Progress line kind. */ - @JsonProperty("kind") FactoryLogLineKind kind, - /** Progress text. */ - @JsonProperty("text") String text +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 new file mode 100644 index 0000000000..ecbb106f12 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.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 javax.annotation.processing.Generated; + +/** + * 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. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AuthLoginResultDto( + /** 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, + /** 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 new file mode 100644 index 0000000000..07becf2fb9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.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; + +/** + * Disposition of a login attempt, including pending user decisions. + * + * @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 AuthLoginResultStatus { + /** The {@code completed} variant. */ + 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"); + + private final String value; + AuthLoginResultStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AuthLoginResultStatus fromValue(String value) { + for (AuthLoginResultStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AuthLoginResultStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStep.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStep.java new file mode 100644 index 0000000000..f13d72f1b9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStep.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * 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). + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AuthLoginStepOpenUrl.class, name = "open-url"), + @JsonSubTypes.Type(value = AuthLoginStepInputRequired.class, name = "input-required"), + @JsonSubTypes.Type(value = AuthLoginStepAwaiting.class, name = "awaiting"), + @JsonSubTypes.Type(value = AuthLoginStepNeedsInteraction.class, name = "needs-interaction"), + @JsonSubTypes.Type(value = AuthLoginStepCompleted.class, name = "completed"), + @JsonSubTypes.Type(value = AuthLoginStepError.class, name = "error") +}) +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class AuthLoginStep { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepAwaiting.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepAwaiting.java new file mode 100644 index 0000000000..10d61f66a0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepAwaiting.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; + +/** + * Variant {@code awaiting} of {@link AuthLoginStep}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthLoginStepAwaiting extends AuthLoginStep { + + @JsonProperty("kind") + private final String kind = "awaiting"; + + @Override + public String getKind() { return kind; } +} 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 new file mode 100644 index 0000000000..a8ea53e123 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.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 javax.annotation.processing.Generated; + +/** + * Variant {@code completed} of {@link AuthLoginStep}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthLoginStepCompleted extends AuthLoginStep { + + @JsonProperty("kind") + private final String kind = "completed"; + + @Override + public String getKind() { return kind; } + + /** 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; + + public AuthLoginResultDto getResult() { return result; } + public void setResult(AuthLoginResultDto result) { this.result = result; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepError.java new file mode 100644 index 0000000000..1d6d425122 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepError.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 javax.annotation.processing.Generated; + +/** + * Variant {@code error} of {@link AuthLoginStep}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthLoginStepError extends AuthLoginStep { + + @JsonProperty("kind") + private final String kind = "error"; + + @Override + public String getKind() { return kind; } + + /** Human-readable failure message. */ + @JsonProperty("message") + private String message; + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepInputRequired.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepInputRequired.java new file mode 100644 index 0000000000..f2fe79429b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepInputRequired.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 javax.annotation.processing.Generated; + +/** + * Variant {@code input-required} of {@link AuthLoginStep}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthLoginStepInputRequired extends AuthLoginStep { + + @JsonProperty("kind") + private final String kind = "input-required"; + + @Override + public String getKind() { return kind; } + + /** Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. a GitHub Enterprise Cloud host, *.ghe.com). */ + @JsonProperty("prompt") + private String prompt; + + public String getPrompt() { return prompt; } + public void setPrompt(String prompt) { this.prompt = prompt; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepNeedsInteraction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepNeedsInteraction.java new file mode 100644 index 0000000000..27902574e1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepNeedsInteraction.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; + +/** + * Variant {@code needs-interaction} of {@link AuthLoginStep}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthLoginStepNeedsInteraction extends AuthLoginStep { + + @JsonProperty("kind") + private final String kind = "needs-interaction"; + + @Override + public String getKind() { return kind; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepOpenUrl.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepOpenUrl.java new file mode 100644 index 0000000000..977ee8caf0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepOpenUrl.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 javax.annotation.processing.Generated; + +/** + * Variant {@code open-url} of {@link AuthLoginStep}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthLoginStepOpenUrl extends AuthLoginStep { + + @JsonProperty("kind") + private final String kind = "open-url"; + + @Override + public String getKind() { return kind; } + + /** Authorize URL the consumer should open in a browser (consumer-driven browser-open). */ + @JsonProperty("url") + private String url; + + public String getUrl() { return url; } + public void setUrl(String url) { this.url = url; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValue.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValue.java new file mode 100644 index 0000000000..4e19659e4b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValue.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The read result, keyed by the same selector as the query. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AuthReadValueActiveAccount.class, name = "activeAccount"), + @JsonSubTypes.Type(value = AuthReadValueStatus.class, name = "status"), + @JsonSubTypes.Type(value = AuthReadValueLastErrors.class, name = "lastErrors") +}) +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class AuthReadValue { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueActiveAccount.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueActiveAccount.java new file mode 100644 index 0000000000..e10579d0e6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueActiveAccount.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Variant {@code activeAccount} of {@link AuthReadValue}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthReadValueActiveAccount extends AuthReadValue { + + @JsonProperty("kind") + private final String kind = "activeAccount"; + + @Override + public String getKind() { return kind; } + + /** The active account, or absent when not logged in. */ + @JsonProperty("account") + private AccountStatus account; + + /** Credential-free identity metadata for the active account, including resolved Copilot user information when available. */ + @JsonProperty("authInfo") + private AuthIdentity authInfo; + + public AccountStatus getAccount() { return account; } + public void setAccount(AccountStatus account) { this.account = account; } + + public AuthIdentity getAuthInfo() { return authInfo; } + public void setAuthInfo(AuthIdentity authInfo) { this.authInfo = authInfo; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryDeclaredLimits.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueLastErrors.java similarity index 61% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryDeclaredLimits.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueLastErrors.java index 7e2e4e6b5b..d7a84eae33 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryDeclaredLimits.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueLastErrors.java @@ -11,27 +11,32 @@ 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; /** - * Declared or approved factory resource ceilings. + * Variant {@code lastErrors} of {@link AuthReadValue}. * * @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 FactoryDeclaredLimits( - /** Maximum concurrently active subagents. */ - @JsonProperty("maxConcurrentSubagents") Long maxConcurrentSubagents, - /** Maximum total subagents spawned by the run. */ - @JsonProperty("maxTotalSubagents") Long maxTotalSubagents, - /** Maximum accumulated active execution time in seconds. */ - @JsonProperty("timeoutSeconds") Double timeoutSeconds, - /** Maximum AI credits consumed by subagents and descendants. */ - @JsonProperty("maxAiCredits") Double maxAiCredits -) { +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthReadValueLastErrors extends AuthReadValue { + + @JsonProperty("kind") + private final String kind = "lastErrors"; + + @Override + public String getKind() { return kind; } + + /** Validation errors from the most recent authentication attempt. */ + @JsonProperty("errors") + private List errors; + + public List getErrors() { return errors; } + public void setErrors(List errors) { this.errors = errors; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueStatus.java new file mode 100644 index 0000000000..01b03f041a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueStatus.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 javax.annotation.processing.Generated; + +/** + * Variant {@code status} of {@link AuthReadValue}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AuthReadValueStatus extends AuthReadValue { + + @JsonProperty("kind") + private final String kind = "status"; + + @Override + public String getKind() { return kind; } + + /** The neutral authentication status summary. */ + @JsonProperty("status") + private AuthStatusDto status; + + public AuthStatusDto getStatus() { return status; } + public void setStatus(AuthStatusDto status) { this.status = status; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthStatusDto.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthStatusDto.java new file mode 100644 index 0000000000..35a4113c80 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthStatusDto.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; + +/** + * Neutral authentication status summary. + * + * @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 AuthStatusDto( + /** Whether the session has resolved authentication. */ + @JsonProperty("isAuthenticated") Boolean isAuthenticated, + /** Active account login, if authenticated. */ + @JsonProperty("activeLogin") String activeLogin, + /** Active account host, if authenticated. */ + @JsonProperty("activeHost") String activeHost, + /** Copilot plan tier of the active account, if known. */ + @JsonProperty("copilotPlan") String copilotPlan, + /** Number of signed-in accounts in the roster. */ + @JsonProperty("accountCount") Long accountCount +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinAgentSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinAgentSummary.java new file mode 100644 index 0000000000..65e75ff6f7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinAgentSummary.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; + +/** + * A shipped agent, named and described. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record BuiltinAgentSummary( + /** The agent name, as it appears in `getBuiltins`. */ + @JsonProperty("name") String name, + /** One-line description of what the agent does. */ + @JsonProperty("description") String description +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java index 1051020dd5..6d8eb53f2f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * An inert AI skill catalog result. AI skills are discovery-only and cannot be represented as installable through this surface. + * An inert AI skill catalog result. Verified Skill candidates may be installable only when the runtime reports installability and the selected session is permitted to plan installation. * * @since 1.0.0 */ @@ -40,7 +40,7 @@ public final class CatalogAiSkillCandidate extends CatalogCandidate { @JsonProperty("mediaType") private CatalogAiSkillMediaType mediaType; - /** AI skills are discovery-only and cannot be installed through this surface */ + /** Whether this AI skill candidate can be planned for verified installation in the selected session. */ @JsonProperty("installability") private CatalogAiSkillInstallability installability; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillInstallability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillInstallability.java index 64c8f6e994..67df7b2023 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillInstallability.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillInstallability.java @@ -10,12 +10,20 @@ import javax.annotation.processing.Generated; /** - * Typed non-installable state for an AI skill candidate + * Typed installability state for an AI skill candidate * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum CatalogAiSkillInstallability { + /** The {@code installable} variant. */ + INSTALLABLE("installable"), + /** The {@code feature-disabled} variant. */ + FEATURE_DISABLED("feature-disabled"), + /** The {@code materialisation-unavailable} variant. */ + MATERIALISATION_UNAVAILABLE("materialisation-unavailable"), + /** The {@code policy-forbids} variant. */ + POLICY_FORBIDS("policy-forbids"), /** The {@code not-installable-kind} variant. */ NOT_INSTALLABLE_KIND("not-installable-kind"); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCapability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCapability.java index 75def2fa38..e782f16aae 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCapability.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCapability.java @@ -35,7 +35,15 @@ public enum CatalogCapability { /** The {@code catalog-selection} variant. */ CATALOG_SELECTION("catalog-selection"), /** The {@code catalog-search-credential-required} variant. */ - CATALOG_SEARCH_CREDENTIAL_REQUIRED("catalog-search-credential-required"); + CATALOG_SEARCH_CREDENTIAL_REQUIRED("catalog-search-credential-required"), + /** The {@code catalog-search-session-bound} variant. */ + CATALOG_SEARCH_SESSION_BOUND("catalog-search-session-bound"), + /** The {@code mcp-confirmed-remote-installation} variant. */ + MCP_CONFIRMED_REMOTE_INSTALLATION("mcp-confirmed-remote-installation"), + /** The {@code mcp-configured-remote-installation} variant. */ + MCP_CONFIGURED_REMOTE_INSTALLATION("mcp-configured-remote-installation"), + /** The {@code skill-confirmed-installation} variant. */ + SKILL_CONFIRMED_INSTALLATION("skill-confirmed-installation"); private final String value; CatalogCapability(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogInvalidRequestField.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogInvalidRequestField.java index 7252d66b28..8a6cb5d518 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogInvalidRequestField.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogInvalidRequestField.java @@ -16,6 +16,8 @@ */ @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum CatalogInvalidRequestField { + /** The {@code policySessionId} variant. */ + POLICYSESSIONID("policySessionId"), /** The {@code query} variant. */ QUERY("query"), /** The {@code limit} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPage.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPage.java index da5a2f13fa..6e47166edc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPage.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPage.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * An explicit numbered-page request. The SDK treats the token as opaque; only the runtime decodes it and changes its targetPage. Authority validation binds navigation to the original search. No snapshot stability or token TTL is promised. + * An explicit numbered-page request. SDK consumers treat the token as opaque. For bound search, the runtime unwraps an expiring owner-bound reference to the private authority token; only the runtime changes the authority token's targetPage. Legacy unbound navigation keeps its authority-issued token semantics. No snapshot stability is promised. * * @since 1.0.0 */ @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record CatalogSearchPage( - /** Opaque authority-issued pagination token from an earlier response. Never decode, modify or log it in an SDK consumer. */ + /** Opaque pagination token from an earlier response, owner-bound when session-bound search was requested. Never decode, modify or log it in an SDK consumer. Expired or foreign bound references require a fresh bound search, not a legacy retry. */ @JsonProperty("token") String token, /** Requested one-based page. Must not exceed either the token's signed pageCount or the navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly available pages beyond that signed pageCount. */ @JsonProperty("number") Long number diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPagination.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPagination.java index 445ed51373..14b7543d0f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPagination.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchPagination.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record CatalogSearchPagination( - /** Opaque authority-issued pagination token. Only the runtime decodes it or changes targetPage; SDK consumers must not decode, modify or log it. It has no runtime-created expiry or cache. */ + /** Opaque pagination token. Session-bound search returns an expiring runtime-owned reference retaining the exact private authority token, original search and authority. Legacy unbound search returns the authority token unchanged, without a runtime-created expiry. Only the runtime unwraps tokens or changes targetPage; SDK consumers must not decode, modify or log them. */ @JsonProperty("token") String token, /** One-based page returned by the authority. */ @JsonProperty("currentPage") Long currentPage, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchRequest.java new file mode 100644 index 0000000000..bc53c4ba95 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchRequest.java @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * 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.List; +import java.util.Objects; +import javax.annotation.processing.Generated; + +/** + * 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. + *

+ * 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 CatalogSearchRequest { + + /** Protocol version and capabilities the caller requires. */ + @JsonProperty("contract") + private final CatalogClientContract contract; + + /** Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ + @JsonProperty("query") + private final String query; + + /** Maximum number of candidates to return. Defaults to 10 when omitted. */ + @JsonProperty("limit") + private Long limit; + + /** Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. */ + @JsonProperty("kinds") + private List kinds; + + /** Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh first-page search. */ + @JsonProperty("page") + private CatalogSearchPage page; + + /** Select an existing attached local session. Requires authenticated, session-bound search. The runtime never creates, resumes or reconfigures a session to honour this selector. */ + @JsonProperty("policySessionId") + private String policySessionId; + + /** + * Creates a request with its required inputs. + * + * @param contract Protocol version and capabilities the caller requires. + * @param query Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. + */ + public CatalogSearchRequest(CatalogClientContract contract, String query) { + this.contract = Objects.requireNonNull(contract, "contract"); + this.query = Objects.requireNonNull(query, "query"); + } + + /** + * Returns the {@code contract} property. + * + * @return Protocol version and capabilities the caller requires. + */ + public CatalogClientContract getContract() { + return contract; + } + + /** + * Returns the {@code query} property. + * + * @return Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. + */ + public String getQuery() { + return query; + } + + /** + * Returns the {@code limit} property. + * + * @return Maximum number of candidates to return. Defaults to 10 when omitted. + */ + public Long getLimit() { + return limit; + } + + /** + * Returns the {@code kinds} property. + * + * @return Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. + */ + public List getKinds() { + return kinds; + } + + /** + * Returns the {@code page} property. + * + * @return Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh first-page search. + */ + public CatalogSearchPage getPage() { + return page; + } + + /** + * Returns the {@code policySessionId} property. + * + * @return Select an existing attached local session. Requires authenticated, session-bound search. The runtime never creates, resumes or reconfigures a session to honour this selector. + */ + public String getPolicySessionId() { + return policySessionId; + } + + /** + * Sets the {@code limit} property. + * + * @param value Maximum number of candidates to return. Defaults to 10 when omitted. + * @return this request + */ + public CatalogSearchRequest setLimit(Long value) { + this.limit = value; + return this; + } + + /** + * Sets the {@code kinds} property. + * + * @param value Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. + * @return this request + */ + public CatalogSearchRequest setKinds(List value) { + this.kinds = value; + return this; + } + + /** + * Sets the {@code page} property. + * + * @param value Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh first-page search. + * @return this request + */ + public CatalogSearchRequest setPage(CatalogSearchPage value) { + this.page = value; + return this; + } + + /** + * Sets the {@code policySessionId} property. + * + * @param value Select an existing attached local session. Requires authenticated, session-bound search. The runtime never creates, resumes or reconfigures a session to honour this selector. + * @return this request + */ + public CatalogSearchRequest setPolicySessionId(String value) { + this.policySessionId = value; + return this; + } +} 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/FactoryCurrentPhase.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAccount.java similarity index 77% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryCurrentPhase.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAccount.java index c15631835d..f41fd3dd79 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryCurrentPhase.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAccount.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Current factory phase identity. + * Eligible account. * * @apiNote This type is experimental and may change in a future version. * @@ -24,10 +24,10 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryCurrentPhase( - /** Current phase identifier. */ - @JsonProperty("id") String id, - /** Zero-based declared phase ordinal, or null for an undeclared phase. */ - @JsonProperty("ordinal") Long ordinal +public record ConnectorDiscoveryAccount( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId, + /** Account metadata. */ + @JsonProperty("authInfo") ConnectorDiscoveryAuthInfo authInfo ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryToolRunOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAuthInfo.java similarity index 72% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryToolRunOptions.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAuthInfo.java index e2444c7053..3857e88ff1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryToolRunOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAuthInfo.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Options for an internal tool-originated factory invocation. + * Account metadata. * * @apiNote This type is experimental and may change in a future version. * @@ -24,10 +24,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryToolRunOptions( - /** Per-invocation resource ceiling overrides. */ - @JsonProperty("limits") FactoryRunLimits limits, - /** Run identifier whose journal and progress should seed this resumed run. */ - @JsonProperty("resumeFromRunId") String resumeFromRunId +public record ConnectorDiscoveryAuthInfo( + /** Authentication type. */ + @JsonProperty("type") AuthInfoType type, + /** Host. */ + @JsonProperty("host") String host, + /** Login. */ + @JsonProperty("login") String login ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAvailability.java similarity index 59% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseStatus.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAvailability.java index 8432507176..00162fc078 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAvailability.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * Derived lifecycle state of a factory phase. + * Availability. * * @apiNote This type is experimental and may change in a future version. * @@ -19,25 +19,23 @@ */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum FactoryPhaseStatus { - /** The {@code pending} variant. */ - PENDING("pending"), - /** The {@code active} variant. */ - ACTIVE("active"), - /** The {@code completed} variant. */ - COMPLETED("completed"), - /** The {@code skipped} variant. */ - SKIPPED("skipped"); +public enum ConnectorDiscoveryAvailability { + /** The {@code enabled} variant. */ + ENABLED("enabled"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); private final String value; - FactoryPhaseStatus(String value) { this.value = value; } + ConnectorDiscoveryAvailability(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator - public static FactoryPhaseStatus fromValue(String value) { - for (FactoryPhaseStatus v : values()) { + public static ConnectorDiscoveryAvailability fromValue(String value) { + for (ConnectorDiscoveryAvailability v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown FactoryPhaseStatus value: " + value); + throw new IllegalArgumentException("Unknown ConnectorDiscoveryAvailability value: " + value); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryCatalogEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryCatalogEntry.java new file mode 100644 index 0000000000..76c137c5e2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryCatalogEntry.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; + +/** + * Entry. + * + * @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 ConnectorDiscoveryCatalogEntry( + /** Name. */ + @JsonProperty("name") String name, + /** Display name. */ + @JsonProperty("displayName") String displayName, + /** Description. */ + @JsonProperty("description") String description, + /** Logo. */ + @JsonProperty("logo") String logo, + /** Tier. */ + @JsonProperty("tier") String tier, + /** Release tag. */ + @JsonProperty("releaseTag") String releaseTag, + /** Status. */ + @JsonProperty("status") ConnectorCatalogStatus status +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunConsumed.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorSessionAccount.java similarity index 71% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunConsumed.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorSessionAccount.java index 5ffc43435d..fcf63dc7d4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunConsumed.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorSessionAccount.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Durable factory resource consumption. + * Session account selection. * * @apiNote This type is experimental and may change in a future version. * @@ -24,12 +24,10 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryRunConsumed( - /** Accumulated active execution time in milliseconds. */ - @JsonProperty("activeMs") Long activeMs, - /** Total subagents spawned by the run. */ - @JsonProperty("subagents") Long subagents, - /** AI usage consumed by the run in nano-AIU. */ - @JsonProperty("nanoAiu") Long nanoAiu +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/ConnectorsGetAccountsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetAccountsResult.java new file mode 100644 index 0000000000..1334b673fa --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetAccountsResult.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; + +/** + * Eligible accounts. + * + * @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 ConnectorsGetAccountsResult( + /** Availability. */ + @JsonProperty("availability") ConnectorDiscoveryAvailability availability, + /** Eligible accounts. */ + @JsonProperty("accounts") List accounts +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetCapabilitiesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetCapabilitiesResult.java new file mode 100644 index 0000000000..dc1b256e27 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetCapabilitiesResult.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; + +/** + * Feature availability. + * + * @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 ConnectorsGetCapabilitiesResult( + /** API version. */ + @JsonProperty("apiVersion") Long apiVersion, + /** Availability. */ + @JsonProperty("availability") ConnectorDiscoveryAvailability availability, + /** Whether accounts are selected by opaque ID. */ + @JsonProperty("opaqueAccountSelection") Boolean opaqueAccountSelection, + /** Whether results are cached. */ + @JsonProperty("conditionalCache") Boolean conditionalCache +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryExecuteResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListParams.java similarity index 84% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryExecuteResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListParams.java index b47b9fb073..39a2c4a966 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryExecuteResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Result returned by an extension factory closure. + * Selected account. * * @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 FactoryExecuteResult( - /** Factory result value. */ - @JsonProperty("result") Object result +public record ConnectorsListParams( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId ) { } 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/ConnectorsListResult.java similarity index 68% 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/ConnectorsListResult.java index 5f6fa34306..ccba6073d4 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/ConnectorsListResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + * Entries for the selected account. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -24,10 +24,14 @@ @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 ConnectorsListResult( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId, + /** Revision. */ + @JsonProperty("revision") Long revision, + /** Refresh time in Unix epoch milliseconds. */ + @JsonProperty("refreshedAtMs") Long refreshedAtMs, + /** Entries. */ + @JsonProperty("connectors") List connectors ) { } 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/ConnectorsRefreshParams.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/ConnectorsRefreshParams.java index a22656359f..e5384ba3f2 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/ConnectorsRefreshParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Result of configuring GitHub MCP. + * Selected account. * * @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 ConnectorsRefreshParams( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsSetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshResult.java similarity index 68% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsSetResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshResult.java index c5ab98621a..a14c50fd28 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsSetResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Outcome of writing user settings. + * Entries for the selected account. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -24,8 +24,14 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record UserSettingsSetResult( - /** Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. */ - @JsonProperty("shadowedKeys") List shadowedKeys +public record ConnectorsRefreshResult( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId, + /** Revision. */ + @JsonProperty("revision") Long revision, + /** Refresh time in Unix epoch milliseconds. */ + @JsonProperty("refreshedAtMs") Long refreshedAtMs, + /** Entries. */ + @JsonProperty("connectors") List connectors ) { } 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/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/EnvironmentsDeleteParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsDeleteParams.java new file mode 100644 index 0000000000..cfe9fe5da3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsDeleteParams.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 user-managed Mission Control environment to delete. + * + * @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 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/FactoryAgentSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentSummary.java deleted file mode 100644 index d063fc9c6b..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentSummary.java +++ /dev/null @@ -1,57 +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; - -/** - * Prompt-safe durable identity and live status for a direct factory agent. - * - * @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 FactoryAgentSummary( - /** Stable direct-agent identifier. */ - @JsonProperty("agentId") String agentId, - /** Tool-call identifier that launched the agent. */ - @JsonProperty("toolCallId") String toolCallId, - /** Owning factory run identifier. */ - @JsonProperty("runId") String runId, - /** Phase identifier active when the agent was launched, or null. */ - @JsonProperty("phaseId") String phaseId, - /** Friendly, non-unique name intended for display */ - @JsonProperty("label") String label, - /** Friendly, non-unique name intended for display */ - @JsonProperty("displayName") String displayName, - /** Registered agent type. */ - @JsonProperty("agentType") String agentType, - /** Current durable or live agent status. */ - @JsonProperty("status") String status, - /** Model requested when the agent was launched. */ - @JsonProperty("requestedModel") String requestedModel, - /** Concrete model resolved for the agent. */ - @JsonProperty("resolvedModel") String resolvedModel, - /** Epoch milliseconds when the agent started. */ - @JsonProperty("startedAt") Long startedAt, - /** Epoch milliseconds when the agent completed. */ - @JsonProperty("completedAt") Long completedAt, - /** Accumulated active agent time in milliseconds. */ - @JsonProperty("activeMs") Long activeMs, - /** Prompt-safe live activity text. */ - @JsonProperty("activity") String activity -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseObservation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseObservation.java deleted file mode 100644 index 167766885c..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseObservation.java +++ /dev/null @@ -1,55 +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; - -/** - * Durable lifecycle and timing for one factory phase. - * - * @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 FactoryPhaseObservation( - /** Phase identifier. */ - @JsonProperty("id") String id, - /** Zero-based declared phase ordinal, or null for an undeclared phase. */ - @JsonProperty("ordinal") Long ordinal, - /** Human-readable phase title. */ - @JsonProperty("title") String title, - /** Optional human-readable phase detail. */ - @JsonProperty("detail") String detail, - /** Derived lifecycle state of the phase. */ - @JsonProperty("status") FactoryPhaseStatus status, - /** Most recent run attempt that entered this phase, or `0` if the phase has never been entered. */ - @JsonProperty("lastEnteredRunAttempt") Long lastEnteredRunAttempt, - /** Number of times execution entered this phase. */ - @JsonProperty("entryCount") Long entryCount, - /** Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip timestamp (equal to `completedAt`). */ - @JsonProperty("startedAt") Long startedAt, - /** Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip timestamp (equal to `startedAt`). */ - @JsonProperty("completedAt") Long completedAt, - /** Completed active time accumulated by this phase in milliseconds. */ - @JsonProperty("accumulatedActiveMs") Long accumulatedActiveMs, - /** Current live active time for this phase in milliseconds. */ - @JsonProperty("currentActiveMs") Long currentActiveMs, - /** Total direct agents associated with this phase. */ - @JsonProperty("totalAgentCount") Long totalAgentCount, - /** Direct agents in this phase that are currently live. */ - @JsonProperty("liveAgentCount") Long liveAgentCount -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunLimits.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunLimits.java deleted file mode 100644 index 300e4977b3..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunLimits.java +++ /dev/null @@ -1,37 +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; - -/** - * Wire-only per-invocation factory resource ceiling overrides. - * - * @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 FactoryRunLimits( - /** Maximum number of factory subagents that may run concurrently. */ - @JsonProperty("maxConcurrentSubagents") Long maxConcurrentSubagents, - /** Maximum total number of factory subagents that may be admitted. */ - @JsonProperty("maxTotalSubagents") Long maxTotalSubagents, - /** 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. */ - @JsonProperty("timeoutSeconds") Double timeoutSeconds, - /** Maximum AI credits consumed by factory subagents and their descendants. The post-paid ceiling is soft: parallel turns can settle beyond it before the run stops. */ - @JsonProperty("maxAiCredits") Double maxAiCredits -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java deleted file mode 100644 index 436f969f61..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.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 javax.annotation.processing.Generated; - -/** - * Complete current or terminal factory run envelope. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryRunResult( - /** Factory 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 factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Completed factory 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/FactoryRunSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunSummary.java deleted file mode 100644 index 56d1f91763..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunSummary.java +++ /dev/null @@ -1,69 +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; - -/** - * Durable factory run summary with read-time live overlays. - * - * @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 FactoryRunSummary( - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Registered factory name. */ - @JsonProperty("factoryName") String factoryName, - /** Human-readable factory description. */ - @JsonProperty("description") String description, - /** Current factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Monotonic durable run revision. */ - @JsonProperty("revision") Long revision, - /** Epoch milliseconds when the run was created. */ - @JsonProperty("createdAt") Long createdAt, - /** Epoch milliseconds when execution first started, or null before start. */ - @JsonProperty("startedAt") Long startedAt, - /** Epoch milliseconds when the durable run was last updated. */ - @JsonProperty("updatedAt") Long updatedAt, - /** Epoch milliseconds when the run completed, or null while nonterminal. */ - @JsonProperty("completedAt") Long completedAt, - /** Current phase identity, or null before any phase is entered. */ - @JsonProperty("currentPhase") FactoryCurrentPhase currentPhase, - /** Number of phases declared by the factory. */ - @JsonProperty("declaredPhaseCount") Long declaredPhaseCount, - /** Number of direct factory agents currently live. */ - @JsonProperty("liveAgentCount") Long liveAgentCount, - /** Total direct factory agents spawned across all attempts. */ - @JsonProperty("totalSpawnedAgentCount") Long totalSpawnedAgentCount, - /** Durable resource consumption. */ - @JsonProperty("consumed") FactoryRunConsumed consumed, - /** Resource ceilings declared by the factory. */ - @JsonProperty("declaredLimits") FactoryDeclaredLimits declaredLimits, - /** Approved effective resource ceilings, or null until approved. */ - @JsonProperty("approved") FactoryDeclaredLimits approved, - /** Epoch milliseconds when this live-overlay snapshot was observed. */ - @JsonProperty("observedAt") Long observedAt, - /** Epoch milliseconds when the current active segment started, or null while inactive. */ - @JsonProperty("activeSegmentStartedAt") Long activeSegmentStartedAt, - /** Terminal run outcome, or null while nonterminal. */ - @JsonProperty("terminal") FactoryRunTerminal terminal, - /** Whether the durable run state currently passes runtime resume eligibility checks. */ - @JsonProperty("canResume") Boolean canResume -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunTerminal.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunTerminal.java deleted file mode 100644 index 0b0db7d9b7..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunTerminal.java +++ /dev/null @@ -1,39 +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; - -/** - * Prompt-safe terminal factory outcome. - * - * @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 FactoryRunTerminal( - /** Human-readable terminal reason. */ - @JsonProperty("reason") String reason, - /** Machine-readable terminal failure. */ - @JsonProperty("failure") Object failure, - /** Human-readable terminal error. */ - @JsonProperty("error") String error, - /** Prompt-safe preview of the completed result. */ - @JsonProperty("resultPreview") String resultPreview, - /** Pause initiator metadata, or null when the run did not pause. */ - @JsonProperty("pauseInfo") Object pauseInfo -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryAgentResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteParams.java similarity index 82% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryAgentResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteParams.java index dcd31fd34a..67f7b64880 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryAgentResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Result of one factory-scoped subagent call. + * Working-tree path a git query applies to. * * @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 SessionFactoryAgentResult( - /** Agent result, omitted when the agent produced no result. */ - @JsonProperty("result") Object result +record GitCurrentBranchRemoteParams( + /** Absolute path to a directory inside the git working tree to query. */ + @JsonProperty("cwd") String cwd ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteResult.java new file mode 100644 index 0000000000..faa7e52b3d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteResult.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; + +/** + * The remote the checked-out branch tracks. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitCurrentBranchRemoteResult( + /** Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. */ + @JsonProperty("remote") String remote +) { +} 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/GitHubOwnerOption.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnerOption.java new file mode 100644 index 0000000000..43e3d2d01d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnerOption.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; + +/** + * A GitHub login the authenticated user may act as: their own account, or an organization they belong to. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubOwnerOption( + /** The owner's GitHub login. */ + @JsonProperty("login") String login, + /** Which kind of owner this is. The authenticated user's own account is always reported as `user`. */ + @JsonProperty("type") String type +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelParams.java new file mode 100644 index 0000000000..1ea9251095 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelParams.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; + +/** + * The owner listing to abandon. + * + * @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 GitHubOwnersCancelParams( + /** Request id the listing was started with. */ + @JsonProperty("requestId") Long requestId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelResult.java new file mode 100644 index 0000000000..d70a718261 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelResult.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 javax.annotation.processing.Generated; + +/** + * Whether the id named a running owner listing. + * + * @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) +record GitHubOwnersCancelResult( + /** True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. */ + @JsonProperty("canceled") Boolean canceled +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListParams.java new file mode 100644 index 0000000000..224b85f6dc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListParams.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; + +/** + * Credential to list owners under, and the request id that makes the listing cancellable. + * + * @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 GitHubOwnersListParams( + /** Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. */ + @JsonProperty("requestId") Long requestId, + /** The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. */ + @JsonProperty("authInfo") Object authInfo +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListResult.java new file mode 100644 index 0000000000..02f844bced --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListResult.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 javax.annotation.processing.Generated; + +/** + * Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. + * + * @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) +record GitHubOwnersListResult( + /** The owners, on success: the authenticated user first, then the organizations they belong to. */ + @JsonProperty("owners") List owners, + /** Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. */ + @JsonProperty("message") String message, + /** A line the caller should log. Present only alongside `message`, and only for failures worth recording. */ + @JsonProperty("warning") String warning, + /** A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. */ + @JsonProperty("throwError") String throwError +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersRequestIdResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersRequestIdResult.java new file mode 100644 index 0000000000..8b5d180b05 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersRequestIdResult.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 javax.annotation.processing.Generated; + +/** + * A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + * + * @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) +record GitHubOwnersRequestIdResult( + /** Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. */ + @JsonProperty("requestId") Long requestId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathParams.java new file mode 100644 index 0000000000..6a896bff33 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathParams.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; + +/** + * Working-tree path whose owning GitHub repository should be resolved. + * + * @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 GitHubRepositoryAtPathParams( + /** Absolute path to a directory inside the git working tree to resolve. */ + @JsonProperty("path") String path +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathResult.java new file mode 100644 index 0000000000..f32a3eafc0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathResult.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; + +/** + * The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubRepositoryAtPathResult( + /** Resolved repository identity, or null when the selected remote resolves to no GitHub host. */ + @JsonProperty("repository") GitHubRepositoryIdentity repository +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryIdentity.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryIdentity.java new file mode 100644 index 0000000000..a1d8c3acc3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryIdentity.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 javax.annotation.processing.Generated; + +/** + * Owner, name, and host of a GitHub repository, as resolved from a git remote URL. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubRepositoryIdentity( + /** Repository owner login (user or organization). */ + @JsonProperty("owner") String owner, + /** Repository name, without the owner prefix or the `.git` suffix. */ + @JsonProperty("name") String name, + /** Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. */ + @JsonProperty("host") String host +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitRemoteRepository.java similarity index 59% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitRemoteRepository.java index fd60b9643e..936e0f8159 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitRemoteRepository.java @@ -10,27 +10,24 @@ 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; /** - * Parameters for invoking a registered factory. + * A GitHub repository one of a working tree's remotes points at. * - * @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 SessionFactoryRunParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Registered factory name. */ +record GitRemoteRepository( + /** Account or organization owning the repository. */ + @JsonProperty("owner") String owner, + /** Repository name, without the owner. */ @JsonProperty("name") String name, - /** Factory input value. */ - @JsonProperty("args") Object args, - /** Factory invocation options. */ - @JsonProperty("options") RunOptions options + /** GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. */ + @JsonProperty("host") String host, + /** Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. */ + @JsonProperty("remoteName") String remoteName ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesParams.java new file mode 100644 index 0000000000..f9c351550f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesParams.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; + +/** + * Git working tree whose GitHub remotes should be listed. + * + * @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 GitReposFromRemotesParams( + /** Absolute path to the root of the git working tree. */ + @JsonProperty("gitRoot") String gitRoot +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesResult.java new file mode 100644 index 0000000000..cc62acf984 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * The GitHub repositories a working tree's remotes point at. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitReposFromRemotesResult( + /** One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. */ + @JsonProperty("repositories") List repositories +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitWorkingDirectoryContextParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitWorkingDirectoryContextParams.java new file mode 100644 index 0000000000..17acdb64c3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitWorkingDirectoryContextParams.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; + +/** + * Working-tree path a git query applies to. + * + * @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 GitWorkingDirectoryContextParams( + /** Absolute path to a directory inside the git working tree to query. */ + @JsonProperty("cwd") String cwd +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadForConfigDirParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadForConfigDirParams.java new file mode 100644 index 0000000000..a056698c92 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadForConfigDirParams.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; + +/** + * Selects the configuration directory whose machine-wide state to read. + * + * @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 GlobalStateLoadForConfigDirParams( + /** Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. */ + @JsonProperty("configDir") String configDir +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadResult.java new file mode 100644 index 0000000000..0b6dbb9d53 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadResult.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.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; + +/** + * The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GlobalStateLoadResult( + /** Plugins installed on this machine. */ + @JsonProperty("installedPlugins") List installedPlugins, + /** Models the user selected recently, most recent first. */ + @JsonProperty("recentModelIds") List recentModelIds, + /** When the host first ran on this machine. */ + @JsonProperty("firstLaunchAt") String firstLaunchAt, + /** Terminals the user has already been asked to set up, so the host does not ask twice. */ + @JsonProperty("askedSetupTerminals") List askedSetupTerminals, + /** Folders where the user declined the init prompt, so it stays hidden there. */ + @JsonProperty("suppressInitFolders") List suppressInitFolders, + /** Whether the sandbox onboarding has been shown. */ + @JsonProperty("sandboxOnboardingShown") Boolean sandboxOnboardingShown, + /** Whether the user declined to trust the sandbox credential proxy CA. */ + @JsonProperty("sandboxCredentialProxyCaDeclined") Boolean sandboxCredentialProxyCaDeclined, + /** Whether the app tip has been shown. */ + @JsonProperty("appTipShown") Boolean appTipShown, + /** Whether the one-off cleanup of stored reasoning summaries has run. */ + @JsonProperty("reasoningSummariesCleanupDone") Boolean reasoningSummariesCleanupDone, + /** Account used for the most recent sign-in. */ + @JsonProperty("lastLoggedInUser") LoggedInUser lastLoggedInUser, + /** Every account the host has signed in to on this machine. */ + @JsonProperty("loggedInUsers") List loggedInUsers, + /** Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. */ + @JsonProperty("staff") Boolean staff, + /** Whether the user was recognized as GitHub staff. */ + @JsonProperty("staffGithub") Boolean staffGitHub, + /** Whether the user was recognized as Microsoft staff. */ + @JsonProperty("staffMicrosoft") Boolean staffMicrosoft, + /** When the staff-only model reset last ran. */ + @JsonProperty("staffModelResetAt") String staffModelResetAt, + /** When the staff-only log level migration last ran. */ + @JsonProperty("staffLogLevelMigrationAt") String staffLogLevelMigrationAt, + /** When the staff-only update channel migration last ran. */ + @JsonProperty("staffUpdateChannelMigrationAt") String staffUpdateChannelMigrationAt, + /** Folders the user has marked as trusted. */ + @JsonProperty("trustedFolders") List trustedFolders, + /** Whether the user has answered the prompt suggesting they install the desktop app. */ + @JsonProperty("appInstallNudgeResponded") Boolean appInstallNudgeResponded, + /** When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. */ + @JsonProperty("autoFeedbackLastPromptedAt") String autoFeedbackLastPromptedAt +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateWriteKeyParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateWriteKeyParams.java new file mode 100644 index 0000000000..f6ee53a57b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateWriteKeyParams.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 single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. + * + * @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 GlobalStateWriteKeyParams( + /** Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. */ + @JsonProperty("configDir") String configDir, + /** Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. */ + @JsonProperty("key") String key, + /** Value to store for the key. Omit it, or pass null, to remove the key instead. */ + @JsonProperty("value") Object value +) { +} 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/HookInvokeResponse.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeResponse.java new file mode 100644 index 0000000000..7a799afcca --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeResponse.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Optional output returned by an SDK callback hook. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@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 new file mode 100644 index 0000000000..6c2233be78 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionParams.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.Map; +import javax.annotation.processing.Generated; + +/** + * One application-owned session handoff, requested by the supervised hosting participant. + * + * @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 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. */ + @JsonProperty("resume") Boolean resume, + /** Host-selected SDK creation or resume settings, without executable callbacks or tools. */ + @JsonProperty("config") Map config +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostDisposeParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostDisposeParams.java new file mode 100644 index 0000000000..4b08ad5340 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostDisposeParams.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; + +/** + * Stops a connection-owned listener and joins its teardown. + * + * @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 HostDisposeParams( + /** Listener UUID. Unknown or successfully stopped IDs are harmless. */ + @JsonProperty("hostId") String hostId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostEmptyResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostEmptyResult.java new file mode 100644 index 0000000000..658f68f3b2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostEmptyResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Empty acknowledgement for a completed host lifecycle operation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HostEmptyResult() { +} 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/HostExitReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostExitReason.java new file mode 100644 index 0000000000..28a5391b27 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostExitReason.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 javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HostExitReason { + /** The {@code disposed} variant. */ + DISPOSED("disposed"), + /** The {@code exited} variant. */ + EXITED("exited"), + /** The {@code ownerDisconnected} variant. */ + OWNERDISCONNECTED("ownerDisconnected"), + /** The {@code runtimeShutdown} variant. */ + RUNTIMESHUTDOWN("runtimeShutdown"); + + private final String value; + HostExitReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HostExitReason fromValue(String value) { + for (HostExitReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HostExitReason value: " + value); + } +} 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/HostExitedNotification.java similarity index 63% 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/HostExitedNotification.java index 65f8745baf..aa930eefe1 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/HostExitedNotification.java @@ -10,23 +10,24 @@ 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. + * Reports a supervised listener's hosting-task termination and cleanup outcome. * - * @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 HostExitedNotification( + /** Listener UUID. */ + @JsonProperty("hostId") String hostId, + /** Cause of termination. */ + @JsonProperty("reason") HostExitReason reason, + /** Process exit status when available; absent for in-process listener tasks. */ + @JsonProperty("exitCode") Long exitCode, + /** Explicit startup or teardown failure, when present. */ @JsonProperty("error") String error ) { } 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/HostGetConfigurationParams.java similarity index 87% 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/HostGetConfigurationParams.java index a111b7af4e..2185af620e 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/HostGetConfigurationParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Optional output returned by an SDK callback hook. + * Empty acknowledgement for a completed host lifecycle operation. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,7 +23,5 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HooksInvokeResult( - @JsonProperty("output") Object output -) { +record HostGetConfigurationParams() { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetEnvironmentCredentialsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetEnvironmentCredentialsParams.java new file mode 100644 index 0000000000..9b67713cba --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetEnvironmentCredentialsParams.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 com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Empty acknowledgement for a completed host lifecycle operation. + * + * @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 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/HostLocalServerConfiguration.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerConfiguration.java new file mode 100644 index 0000000000..86f544e4ff --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerConfiguration.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 local WebSocket listener settings. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record HostLocalServerConfiguration( + /** Hostname or IP address to bind. */ + @JsonProperty("hostname") String hostname, + /** Port to bind, with zero requesting OS allocation. */ + @JsonProperty("port") Long port, + /** Secret connection token, absent when authentication is disabled. */ + @JsonProperty("token") String token, + /** Whether the listener requires token authentication. */ + @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/HostPublishSessionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostPublishSessionParams.java new file mode 100644 index 0000000000..d9c8395ba4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostPublishSessionParams.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; + +/** + * Publishes a resident session attached to the listener's owning 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 HostPublishSessionParams( + /** Listener UUID returned by host.start. */ + @JsonProperty("hostId") String hostId, + /** Canonical runtime session ID attached to the listener's owning connection. */ + @JsonProperty("sessionId") String sessionId +) { +} 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 new file mode 100644 index 0000000000..3bad91e955 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostPublishSessionResult.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; + +/** + * The existing runtime identity and its resource on the listener. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HostPublishSessionResult( + /** Canonical runtime ID of the published session. */ + @JsonProperty("sessionId") String sessionId, + /** AHP resource URI for the session on this listener. */ + @JsonProperty("sessionUri") String sessionUri +) { +} 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 new file mode 100644 index 0000000000..8d1ed23bb6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReadyParams.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; + +/** + * Readiness reported by the supervised hosting participant on its own SDK 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) +record HostReadyParams( + /** Actual bound WebSocket URL. */ + @JsonProperty("address") String address, + /** Configured secret token, absent when authentication is disabled. */ + @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 new file mode 100644 index 0000000000..f431893e28 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionRequest.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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Listener-scoped registration, not a copy or durable adoption of a session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record HostRegisterSessionRequest( + /** Canonical ID of the existing resident runtime session. */ + @JsonProperty("sessionId") String sessionId, + /** Absolute working directory of the resident session. */ + @JsonProperty("workingDirectory") String workingDirectory, + /** Additional directories already granted to the resident session. */ + @JsonProperty("additionalDirectories") List additionalDirectories, + /** Current display title of the resident session, when available. */ + @JsonProperty("title") String title, + /** Session creation time in milliseconds since the Unix epoch, when available. */ + @JsonProperty("createdAtUnixMs") Long createdAtUnixMs, + /** Last session modification time in milliseconds since the Unix epoch, when available. */ + @JsonProperty("modifiedAtUnixMs") Long modifiedAtUnixMs +) { +} 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 new file mode 100644 index 0000000000..894687ca09 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReleaseSessionParams.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; + +/** + * Ends one participation, not the application's session lifetime. + * + * @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 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 new file mode 100644 index 0000000000..f35ea3d1a6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateCallback.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; + +/** + * Application callback routed over its existing SDK connection. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record HostSessionCreateCallback( + /** Listener UUID identifying the owning application's host. */ + @JsonProperty("hostId") String hostId, + /** Unique identity of the session participation being requested. */ + @JsonProperty("handoffId") String handoffId, + /** Resume an app-owned durable session instead of creating a new session. */ + @JsonProperty("resume") Boolean resume, + /** Host-selected SDK creation or resume settings, without executable callbacks or tools. */ + @JsonProperty("config") Map config +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateResult.java new file mode 100644 index 0000000000..96f0c87ec4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateResult.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; + +/** + * The resident session the application has materialized on its own connection. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +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 new file mode 100644 index 0000000000..6d06c6b9a6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionReleasedNotification.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; + +/** + * Releases the original application session object retained for one handoff. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record HostSessionReleasedNotification( + /** Listener UUID whose application session participation ended. */ + @JsonProperty("hostId") String hostId, + /** Identity of the handoff retaining the original application session object. */ + @JsonProperty("handoffId") String handoffId +) { +} 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 new file mode 100644 index 0000000000..b60a36451d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartParams.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 javax.annotation.processing.Generated; + +/** + * 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 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HostStartParams( + /** Caller-generated UUID identifying this connection-owned listener. */ + @JsonProperty("hostId") String hostId, + /** 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. */ + @JsonProperty("resumeFactory") Boolean resumeFactory +) { +} 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 new file mode 100644 index 0000000000..4a9b908ecc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartResult.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 javax.annotation.processing.Generated; + +/** + * Listener readiness, returned only after binding and the supervised participant's SDK handshake. + * + * @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 HostStartResult( + /** Caller-generated listener UUID. */ + @JsonProperty("hostId") String hostId, + /** Actual bound WebSocket URL, including the allocated port. */ + @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/InstallationCatalogueIdentity.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationCatalogueIdentity.java new file mode 100644 index 0000000000..34620ea725 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationCatalogueIdentity.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 javax.annotation.processing.Generated; + +/** + * Catalogue identity retained from a bound candidate or plan at installation time. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record InstallationCatalogueIdentity( + /** Authority resource identifier when supplied by the catalogue. */ + @JsonProperty("resourceId") String resourceId, + /** Catalogue item URL when supplied by the authority. */ + @JsonProperty("itemUrl") String itemUrl, + /** Human display name retained from the catalogue candidate. */ + @JsonProperty("displayName") String displayName, + /** Catalogue description retained at install planning time. */ + @JsonProperty("description") String description, + /** Catalogue publisher retained at install planning time. */ + @JsonProperty("publisher") String publisher, + /** Catalogue version retained at install planning time. */ + @JsonProperty("version") String version, + /** Catalogue authority/source string that supplied the candidate. */ + @JsonProperty("source") String source, + /** Catalogue trust observation retained at install planning time. */ + @JsonProperty("trustAtInstall") CatalogTrustSnapshot trustAtInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationConfirmationRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationConfirmationRequest.java new file mode 100644 index 0000000000..02cccb018a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationConfirmationRequest.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 javax.annotation.processing.Generated; + +/** + * One connection-owned, expiring request for a trusted host's explicit user decision. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record InstallationConfirmationRequest( + /** 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. */ + @JsonProperty("policySessionId") String policySessionId, + /** Opaque one-use challenge. Return unchanged; never log or persist. */ + @JsonProperty("confirmationId") String confirmationId, + /** Random identifier of this installation operation, not a plan handle. */ + @JsonProperty("operationId") String operationId, + /** Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. */ + @JsonProperty("expiresAt") String expiresAt, + /** Opaque commitment to the exact review and inputs. Return unchanged; never log. */ + @JsonProperty("reviewFingerprint") String reviewFingerprint, + /** Resource-specific review to present before collecting the user's decision. */ + @JsonProperty("review") InstallationReview review +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationConfirmationRequestReview.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationConfirmationRequestReview.java new file mode 100644 index 0000000000..8f15d740f5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationConfirmationRequestReview.java @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** Compatibility wrapper for installation confirmation review payloads. */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record InstallationConfirmationRequestReview( + /** Resource-specific review to present before collecting the user's decision. */ + @JsonProperty("review") InstallationReview review +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationDecision.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationDecision.java new file mode 100644 index 0000000000..3eec6a61a5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationDecision.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; + +/** + * Explicit user decisions, never inferred from a permission grant or model response. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum InstallationDecision { + /** The {@code confirm} variant. */ + CONFIRM("confirm"), + /** The {@code decline} variant. */ + DECLINE("decline"), + /** The {@code cancel} variant. */ + CANCEL("cancel"); + + private final String value; + InstallationDecision(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static InstallationDecision fromValue(String value) { + for (InstallationDecision v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown InstallationDecision value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReview.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReview.java new file mode 100644 index 0000000000..ccdc6eb9b3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReview.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Only resource kinds with an implemented installation engine have a review variant. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "resource", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = InstallationReviewMcp.class, name = "mcp"), + @JsonSubTypes.Type(value = InstallationReviewSkill.class, name = "skill") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class InstallationReview { + + /** + * Returns the discriminator value for this variant. + * + * @return the resource discriminator + */ + public abstract String getResource(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReviewMcp.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReviewMcp.java new file mode 100644 index 0000000000..e739591ebd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReviewMcp.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; + +/** + * Variant {@code mcp} of {@link InstallationReview}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class InstallationReviewMcp extends InstallationReview { + + @JsonProperty("resource") + private final String resource = "mcp"; + + @Override + public String getResource() { return resource; } + + /** The exact MCP action and its reviewed changes. */ + @JsonProperty("review") + private McpInstallationReview review; + + public McpInstallationReview getReview() { return review; } + public void setReview(McpInstallationReview review) { this.review = review; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReviewSkill.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReviewSkill.java new file mode 100644 index 0000000000..7476ec6272 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationReviewSkill.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; + +/** + * Variant {@code skill} of {@link InstallationReview}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class InstallationReviewSkill extends InstallationReview { + + @JsonProperty("resource") + private final String resource = "skill"; + + @Override + public String getResource() { return resource; } + + /** The exact verified Skill action and its reviewed files. */ + @JsonProperty("review") + private SkillInstallationReview review; + + public SkillInstallationReview getReview() { return review; } + public void setReview(SkillInstallationReview review) { this.review = review; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationsConfirmResult.java similarity index 65% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationsConfirmResult.java index b4cfcae116..9398c08deb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/InstallationsConfirmResult.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Resolved persisted factory identity and resumed run envelope. + * A response is meaningful only on the connection and request that issued its challenge. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,10 +23,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionFactoryResumeResult( - /** Persisted factory name resolved for the resumed run. */ - @JsonProperty("factoryName") String factoryName, - /** Terminal resumed run envelope. */ - @JsonProperty("run") FactoryRunResult run +public record InstallationsConfirmResult( + /** Exact challenge from the request. */ + @JsonProperty("confirmationId") String confirmationId, + /** Exact review commitment from the request. */ + @JsonProperty("reviewFingerprint") String reviewFingerprint, + /** Fresh explicit user decision. There is no default. */ + @JsonProperty("decision") InstallationDecision decision ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoggedInUser.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoggedInUser.java new file mode 100644 index 0000000000..ebd6526dc4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoggedInUser.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; + +/** + * An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record LoggedInUser( + /** Host the account belongs to, such as `github.com` or an Enterprise server. */ + @JsonProperty("host") String host, + /** Account login on that host. */ + @JsonProperty("login") String login, + /** Account kind, when the host recorded one. Consumers must tolerate new strings. */ + @JsonProperty("kind") String kind, + /** Source account this account was derived from, when one was recorded. */ + @JsonProperty("derivedFrom") String derivedFrom +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoginProviderKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoginProviderKind.java new file mode 100644 index 0000000000..75517e5892 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoginProviderKind.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.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A provider a consumer may interactively sign in with. + * + * @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 LoginProviderKind { + /** The {@code githubDotCom} variant. */ + GITHUBDOTCOM("githubDotCom"), + /** The {@code proxima} variant. */ + PROXIMA("proxima"), + /** The {@code entra} variant. */ + ENTRA("entra"); + + private final String value; + LoginProviderKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static LoginProviderKind fromValue(String value) { + for (LoginProviderKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown LoginProviderKind value: " + value); + } +} 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/ManagedSettingsChannel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsChannel.java new file mode 100644 index 0000000000..1ab915c122 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsChannel.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.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A channel accepted by managedSettings.compose. + * + * @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 ManagedSettingsChannel { + /** The {@code device} variant. */ + DEVICE("device"), + /** The {@code server} variant. */ + SERVER("server"), + /** The {@code policyHelper} variant. */ + POLICYHELPER("policyHelper"); + + private final String value; + ManagedSettingsChannel(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ManagedSettingsChannel fromValue(String value) { + for (ManagedSettingsChannel v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ManagedSettingsChannel value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeLayer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeLayer.java new file mode 100644 index 0000000000..b6267f167b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeLayer.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 candidate channel; absent settings represents a channel that delivered no 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 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/SessionFactoryGetRunProgressResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeParams.java similarity index 53% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunProgressResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeParams.java index 2a4cb78cb2..ca313ce283 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunProgressResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeParams.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * A bidirectional page of factory progress. + * 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 @@ -24,18 +24,8 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionFactoryGetRunProgressResult( - /** Progress records in sequence order. */ - @JsonProperty("records") List records, - /** Oldest sequence number in this page, or null when empty. */ - @JsonProperty("oldestSeq") Long oldestSeq, - /** Newest sequence number in this page, or null when empty. */ - @JsonProperty("newestSeq") Long newestSeq, - /** Whether progress records older than this page exist. */ - @JsonProperty("hasMoreOlder") Boolean hasMoreOlder, - /** Whether progress records newer than this page exist. */ - @JsonProperty("hasMoreNewer") Boolean hasMoreNewer, - /** Run revision reflected by this page. */ - @JsonProperty("revision") Long revision +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/McpApplyInstallParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpApplyInstallParams.java new file mode 100644 index 0000000000..7f17233221 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpApplyInstallParams.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; + +/** + * Applies exactly one previously prepared operation on its original 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 McpApplyInstallParams( + /** Capabilities required by the original prepared operation. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Runtime-issued ID already returned by prepareInstall, never reused or rebound. */ + @JsonProperty("operationId") String operationId, + /** Same existing attached or privately borrowed session as preparation. */ + @JsonProperty("policySessionId") String policySessionId +) { +} 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/McpApplyUninstallParams.java similarity index 66% 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/McpApplyUninstallParams.java index 7952fdc88b..77b361694d 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/McpApplyUninstallParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Result of registering or re-arming a scheduled prompt. + * One-use application of the exact retained removal plan. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,10 +23,12 @@ @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 McpApplyUninstallParams( + /** Required authenticated bound installation capabilities. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Opaque original removal plan, consumed once. */ + @JsonProperty("planHandle") String planHandle, + /** Same existing selected session as removal preparation. */ + @JsonProperty("policySessionId") String policySessionId ) { } 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/McpConfiguredServerState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java new file mode 100644 index 0000000000..0087f2546b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.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; + +/** + * Observational state for a matching already materialized MCP server. + * + * @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 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/McpInstallPlan.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallPlan.java index b9e1bf0d42..3b55b24666 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallPlan.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallPlan.java @@ -31,7 +31,7 @@ public record McpInstallPlan( /** Origin and semantic digest of the exact validated JSON MCP card content bound to this plan. */ @JsonProperty("provenance") McpPlanProvenance provenance, /** Every eligible transport, so a host can present an explicit choice. A completed plan always has at least one; when none is eligible, planning returns `CatalogUnavailableTransportError` instead. */ - @JsonProperty("transportChoices") List transportChoices, + @JsonProperty("transportChoices") List transportChoices, /** Identifier of the choice the runtime would pick by default. Omitted when there is no eligible transport, or when the runtime expresses no preference. */ @JsonProperty("recommendedTransportChoiceId") String recommendedTransportChoiceId, /** Configuration scope and key the plan would write to. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationFailureReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationFailureReason.java new file mode 100644 index 0000000000..99400f1098 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationFailureReason.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Bounded refusal categories, without echoing handles, credentials or configuration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpInstallationFailureReason { + /** The {@code invalid-request} variant. */ + INVALID_REQUEST("invalid-request"), + /** The {@code operation-limit} variant. */ + OPERATION_LIMIT("operation-limit"), + /** The {@code cancelled} variant. */ + CANCELLED("cancelled"), + /** The {@code capability-required} variant. */ + CAPABILITY_REQUIRED("capability-required"), + /** The {@code confirmation-unavailable} variant. */ + CONFIRMATION_UNAVAILABLE("confirmation-unavailable"), + /** The {@code confirmation-invalid} variant. */ + CONFIRMATION_INVALID("confirmation-invalid"), + /** The {@code policy-context-unavailable} variant. */ + POLICY_CONTEXT_UNAVAILABLE("policy-context-unavailable"), + /** The {@code policy-changed} variant. */ + POLICY_CHANGED("policy-changed"), + /** The {@code policy-denied} variant. */ + POLICY_DENIED("policy-denied"), + /** The {@code configuration-changed} variant. */ + CONFIGURATION_CHANGED("configuration-changed"), + /** The {@code configuration-modified} variant. */ + CONFIGURATION_MODIFIED("configuration-modified"), + /** The {@code resource-not-found} variant. */ + RESOURCE_NOT_FOUND("resource-not-found"), + /** The {@code plan-expired} variant. */ + PLAN_EXPIRED("plan-expired"), + /** The {@code plan-replayed} variant. */ + PLAN_REPLAYED("plan-replayed"), + /** The {@code foreign-runtime} variant. */ + FOREIGN_RUNTIME("foreign-runtime"), + /** The {@code replan-required} variant. */ + REPLAN_REQUIRED("replan-required"), + /** The {@code source-revalidation-unavailable} variant. */ + SOURCE_REVALIDATION_UNAVAILABLE("source-revalidation-unavailable"), + /** The {@code source-changed} variant. */ + SOURCE_CHANGED("source-changed"), + /** The {@code source-unavailable} variant. */ + SOURCE_UNAVAILABLE("source-unavailable"), + /** The {@code registry-unavailable} variant. */ + REGISTRY_UNAVAILABLE("registry-unavailable"), + /** The {@code secret-store-unavailable} variant. */ + SECRET_STORE_UNAVAILABLE("secret-store-unavailable"), + /** The {@code lifecycle-unavailable} variant. */ + LIFECYCLE_UNAVAILABLE("lifecycle-unavailable"), + /** The {@code write-failed} variant. */ + WRITE_FAILED("write-failed"); + + private final String value; + McpInstallationFailureReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpInstallationFailureReason fromValue(String value) { + for (McpInstallationFailureReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpInstallationFailureReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationInput.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationInput.java new file mode 100644 index 0000000000..779cb4c6a3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationInput.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 javax.annotation.processing.Generated; + +/** + * One Registry string-valued configuration entry for the selected transport. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpInstallationInput( + /** Exact category declared by the selected choice. */ + @JsonProperty("category") McpPlanValueCategory category, + /** Exact key declared by the selected choice. */ + @JsonProperty("key") String key, + /** Explicit non-secret value. Secret placeholders use a separate input channel. */ + @JsonProperty("value") String value +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcome.java new file mode 100644 index 0000000000..f7dd74df00 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcome.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.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Read-only or recovery management result, never permission to activate or replay. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = McpInstallationManagementOutcomeRecoveryRequired.class, name = "recovery-required"), + @JsonSubTypes.Type(value = McpInstallationManagementOutcomeInstallPrepared.class, name = "install-prepared"), + @JsonSubTypes.Type(value = McpInstallationManagementOutcomeListed.class, name = "listed"), + @JsonSubTypes.Type(value = McpInstallationManagementOutcomeRecovered.class, name = "recovered"), + @JsonSubTypes.Type(value = McpInstallationManagementOutcomeUninstallPlanned.class, name = "uninstall-planned"), + @JsonSubTypes.Type(value = McpInstallationManagementOutcomeOperation.class, name = "operation"), + @JsonSubTypes.Type(value = McpInstallationManagementOutcomeRefused.class, name = "refused") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public sealed interface McpInstallationManagementOutcome permits McpInstallationManagementOutcomeRecoveryRequired, McpInstallationManagementOutcomeInstallPrepared, McpInstallationManagementOutcomeListed, McpInstallationManagementOutcomeRecovered, McpInstallationManagementOutcomeUninstallPlanned, McpInstallationManagementOutcomeOperation, McpInstallationManagementOutcomeRefused { + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + @JsonProperty("kind") + String kind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeInstallPrepared.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeInstallPrepared.java new file mode 100644 index 0000000000..0ed4156019 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeInstallPrepared.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code install-prepared} of {@link McpInstallationManagementOutcome}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationManagementOutcomeInstallPrepared( + /** Known original operation identity, returned before callback or effects. */ + @JsonProperty("operation") McpPreparedInstall operation, + /** Installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements McpInstallationManagementOutcome { + public McpInstallationManagementOutcomeInstallPrepared { + kind = "install-prepared"; + } + + public McpInstallationManagementOutcomeInstallPrepared( + McpPreparedInstall operation + ) { + this(operation, "install-prepared"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeListed.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeListed.java new file mode 100644 index 0000000000..5bfd213182 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeListed.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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code listed} of {@link McpInstallationManagementOutcome}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationManagementOutcomeListed( + /** Owned receipts visible to the selected account and host. */ + @JsonProperty("installations") List installations, + /** Installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements McpInstallationManagementOutcome { + public McpInstallationManagementOutcomeListed { + kind = "listed"; + } + + public McpInstallationManagementOutcomeListed( + List installations + ) { + this(installations, "listed"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeOperation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeOperation.java new file mode 100644 index 0000000000..b23b8c3d8e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeOperation.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code operation} of {@link McpInstallationManagementOutcome}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationManagementOutcomeOperation( + /** Original-connection operation snapshot. */ + @JsonProperty("operation") McpInstallationOperationStatus operation, + /** Installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements McpInstallationManagementOutcome { + public McpInstallationManagementOutcomeOperation { + kind = "operation"; + } + + public McpInstallationManagementOutcomeOperation( + McpInstallationOperationStatus operation + ) { + this(operation, "operation"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRecovered.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRecovered.java new file mode 100644 index 0000000000..be1b5b3b1e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRecovered.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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code recovered} of {@link McpInstallationManagementOutcome}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationManagementOutcomeRecovered( + /** Freshly inspected receipts after successful durable reconciliation. */ + @JsonProperty("installations") List installations, + /** Installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements McpInstallationManagementOutcome { + public McpInstallationManagementOutcomeRecovered { + kind = "recovered"; + } + + public McpInstallationManagementOutcomeRecovered( + List installations + ) { + this(installations, "recovered"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRecoveryRequired.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRecoveryRequired.java new file mode 100644 index 0000000000..3f2f92c5f2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRecoveryRequired.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Already-confirmed durable work must be reconciled before new mutations or inventory. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationManagementOutcomeRecoveryRequired( + /** Installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements McpInstallationManagementOutcome { + public McpInstallationManagementOutcomeRecoveryRequired { + kind = "recovery-required"; + } + + public McpInstallationManagementOutcomeRecoveryRequired() { + this("recovery-required"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRefused.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRefused.java new file mode 100644 index 0000000000..b8ebdca145 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeRefused.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code refused} of {@link McpInstallationManagementOutcome}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationManagementOutcomeRefused( + /** Specific bounded refusal. */ + @JsonProperty("reason") McpInstallationFailureReason reason, + /** Installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements McpInstallationManagementOutcome { + public McpInstallationManagementOutcomeRefused { + kind = "refused"; + } + + public McpInstallationManagementOutcomeRefused( + McpInstallationFailureReason reason + ) { + this(reason, "refused"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeUninstallPlanned.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeUninstallPlanned.java new file mode 100644 index 0000000000..6ebda5f989 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementOutcomeUninstallPlanned.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code uninstall-planned} of {@link McpInstallationManagementOutcome}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationManagementOutcomeUninstallPlanned( + /** Original owned removal plan and operation. */ + @JsonProperty("plan") McpUninstallPlan plan, + /** Installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements McpInstallationManagementOutcome { + public McpInstallationManagementOutcomeUninstallPlanned { + kind = "uninstall-planned"; + } + + public McpInstallationManagementOutcomeUninstallPlanned( + McpUninstallPlan plan + ) { + this(plan, "uninstall-planned"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResult.java new file mode 100644 index 0000000000..30db4d1ec7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResult.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Management result with contract receipt, or a typed request/negotiation refusal. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = McpInstallationManagementResultOutcome.class, name = "outcome"), + @JsonSubTypes.Type(value = McpInstallationManagementResultCatalogNegotiationRefusedError.class, name = "negotiation-refused"), + @JsonSubTypes.Type(value = McpInstallationManagementResultCatalogInvalidRequestError.class, name = "invalid-request") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class McpInstallationManagementResult { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultCatalogInvalidRequestError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultCatalogInvalidRequestError.java new file mode 100644 index 0000000000..c230e50a24 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultCatalogInvalidRequestError.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; + +/** + * The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpInstallationManagementResultCatalogInvalidRequestError extends McpInstallationManagementResult { + + @JsonProperty("kind") + private final String kind = "invalid-request"; + + @Override + public String getKind() { return kind; } + + /** Which request field was rejected. */ + @JsonProperty("field") + private CatalogInvalidRequestField field; + + /** Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogInvalidRequestField getField() { return field; } + public void setField(CatalogInvalidRequestField field) { this.field = field; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultCatalogNegotiationRefusedError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultCatalogNegotiationRefusedError.java new file mode 100644 index 0000000000..f0068631c2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultCatalogNegotiationRefusedError.java @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpInstallationManagementResultCatalogNegotiationRefusedError extends McpInstallationManagementResult { + + @JsonProperty("kind") + private final String kind = "negotiation-refused"; + + @Override + public String getKind() { return kind; } + + /** Whether the version or the capability set was the problem. */ + @JsonProperty("reason") + private CatalogNegotiationRefusedReason reason; + + /** Protocol version of the runtime that refused the request. */ + @JsonProperty("runtimeProtocolVersion") + private Long runtimeProtocolVersion; + + /** Lowest caller protocol version this runtime will serve. */ + @JsonProperty("minimumSupportedProtocolVersion") + private Long minimumSupportedProtocolVersion; + + /** Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. */ + @JsonProperty("supportedCapabilities") + private List supportedCapabilities; + + /** The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. */ + @JsonProperty("unsupportedCapabilities") + private List unsupportedCapabilities; + + /** Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogNegotiationRefusedReason getReason() { return reason; } + public void setReason(CatalogNegotiationRefusedReason reason) { this.reason = reason; } + + public Long getRuntimeProtocolVersion() { return runtimeProtocolVersion; } + public void setRuntimeProtocolVersion(Long runtimeProtocolVersion) { this.runtimeProtocolVersion = runtimeProtocolVersion; } + + public Long getMinimumSupportedProtocolVersion() { return minimumSupportedProtocolVersion; } + public void setMinimumSupportedProtocolVersion(Long minimumSupportedProtocolVersion) { this.minimumSupportedProtocolVersion = minimumSupportedProtocolVersion; } + + public List getSupportedCapabilities() { return supportedCapabilities; } + public void setSupportedCapabilities(List supportedCapabilities) { this.supportedCapabilities = supportedCapabilities; } + + public List getUnsupportedCapabilities() { return unsupportedCapabilities; } + public void setUnsupportedCapabilities(List unsupportedCapabilities) { this.unsupportedCapabilities = unsupportedCapabilities; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultOutcome.java new file mode 100644 index 0000000000..aab2a0cd7e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationManagementResultOutcome.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; + +/** + * Variant {@code outcome} of {@link McpInstallationManagementResult}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpInstallationManagementResultOutcome extends McpInstallationManagementResult { + + @JsonProperty("kind") + private final String kind = "outcome"; + + @Override + public String getKind() { return kind; } + + /** Observed management outcome. */ + @JsonProperty("outcome") + private McpInstallationManagementOutcome outcome; + + /** Capabilities actually honoured for this request. */ + @JsonProperty("negotiated") + private CatalogNegotiatedContract negotiated; + + public McpInstallationManagementOutcome getOutcome() { return outcome; } + public void setOutcome(McpInstallationManagementOutcome outcome) { this.outcome = outcome; } + + public CatalogNegotiatedContract getNegotiated() { return negotiated; } + public void setNegotiated(CatalogNegotiatedContract negotiated) { this.negotiated = negotiated; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatus.java new file mode 100644 index 0000000000..913d90f67f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatus.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Status snapshot from the original connection, independent of new-work account availability. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "phase", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = McpInstallationOperationStatusPreparing.class, name = "preparing"), + @JsonSubTypes.Type(value = McpInstallationOperationStatusPrepared.class, name = "prepared"), + @JsonSubTypes.Type(value = McpInstallationOperationStatusAwaitingConfirmation.class, name = "awaiting-confirmation"), + @JsonSubTypes.Type(value = McpInstallationOperationStatusRevalidating.class, name = "revalidating"), + @JsonSubTypes.Type(value = McpInstallationOperationStatusApplying.class, name = "applying"), + @JsonSubTypes.Type(value = McpInstallationOperationStatusCompleted.class, name = "completed") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public sealed interface McpInstallationOperationStatus permits McpInstallationOperationStatusPreparing, McpInstallationOperationStatusPrepared, McpInstallationOperationStatusAwaitingConfirmation, McpInstallationOperationStatusRevalidating, McpInstallationOperationStatusApplying, McpInstallationOperationStatusCompleted { + /** + * Returns the discriminator value for this variant. + * + * @return the phase discriminator + */ + @JsonProperty("phase") + String phase(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusApplying.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusApplying.java new file mode 100644 index 0000000000..2ca07480ab --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusApplying.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code applying} of {@link McpInstallationOperationStatus}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationOperationStatusApplying( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether applying was asked to cancel; already-started effects retain their lease. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + /** Original operation progress discriminator. */ + @JsonProperty("phase") String phase +) implements McpInstallationOperationStatus { + public McpInstallationOperationStatusApplying { + phase = "applying"; + } + + public McpInstallationOperationStatusApplying( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "applying"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusAwaitingConfirmation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusAwaitingConfirmation.java new file mode 100644 index 0000000000..b00033150a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusAwaitingConfirmation.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code awaiting-confirmation} of {@link McpInstallationOperationStatus}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationOperationStatusAwaitingConfirmation( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether the pending human callback was asked to cancel. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + /** Original operation progress discriminator. */ + @JsonProperty("phase") String phase +) implements McpInstallationOperationStatus { + public McpInstallationOperationStatusAwaitingConfirmation { + phase = "awaiting-confirmation"; + } + + public McpInstallationOperationStatusAwaitingConfirmation( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "awaiting-confirmation"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusCompleted.java new file mode 100644 index 0000000000..c083944eed --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusCompleted.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 com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code completed} of {@link McpInstallationOperationStatus}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationOperationStatusCompleted( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation was requested before the terminal result. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + /** Immutable terminal receipt. */ + @JsonProperty("outcome") Object outcome, + /** Original operation progress discriminator. */ + @JsonProperty("phase") String phase +) implements McpInstallationOperationStatus { + public McpInstallationOperationStatusCompleted { + phase = "completed"; + } + + public McpInstallationOperationStatusCompleted( + String operationId, + Boolean cancellationRequested, + Object outcome + ) { + this(operationId, cancellationRequested, outcome, "completed"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusPrepared.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusPrepared.java new file mode 100644 index 0000000000..5a662bd48b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusPrepared.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code prepared} of {@link McpInstallationOperationStatus}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationOperationStatusPrepared( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether the inert prepared operation was asked to cancel. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + /** Original operation progress discriminator. */ + @JsonProperty("phase") String phase +) implements McpInstallationOperationStatus { + public McpInstallationOperationStatusPrepared { + phase = "prepared"; + } + + public McpInstallationOperationStatusPrepared( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "prepared"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusPreparing.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusPreparing.java new file mode 100644 index 0000000000..572dc16995 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusPreparing.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code preparing} of {@link McpInstallationOperationStatus}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationOperationStatusPreparing( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation has been requested, not proof that a write was undone. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + /** Original operation progress discriminator. */ + @JsonProperty("phase") String phase +) implements McpInstallationOperationStatus { + public McpInstallationOperationStatusPreparing { + phase = "preparing"; + } + + public McpInstallationOperationStatusPreparing( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "preparing"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusRevalidating.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusRevalidating.java new file mode 100644 index 0000000000..0f1d895185 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationOperationStatusRevalidating.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code revalidating} of {@link McpInstallationOperationStatus}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationOperationStatusRevalidating( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether source or authority revalidation was asked to cancel. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + /** Original operation progress discriminator. */ + @JsonProperty("phase") String phase +) implements McpInstallationOperationStatus { + public McpInstallationOperationStatusRevalidating { + phase = "revalidating"; + } + + public McpInstallationOperationStatusRevalidating( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "revalidating"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationRemoteConfiguration.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationRemoteConfiguration.java new file mode 100644 index 0000000000..32dcbceb69 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationRemoteConfiguration.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 java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Final remote configuration, not a template. The producer refuses external-value +expansion before presenting this review. Receipt-owned secrets appear only as +`${installation-secret:}` references whose `` matches a reviewed +`${secret:}` placeholder; values are never included. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpInstallationRemoteConfiguration( + /** Transport in the effective persisted remote configuration. */ + @JsonProperty("transport") McpPlanRemoteTransport transport, + /** Exact resolved endpoint, without templates or secret placeholders. */ + @JsonProperty("url") String url, + /** Configured headers, excluding separately authorised OAuth tokens. Values may +contain owned secret references, never secret values. */ + @JsonProperty("headers") Map headers, + /** Configured tool selection, not permission to invoke those tools. */ + @JsonProperty("tools") List tools +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResult.java new file mode 100644 index 0000000000..439ff3d0d7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResult.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * An installation result together with the exact honoured contract, or a negotiation refusal. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = McpInstallationResultOutcome.class, name = "outcome"), + @JsonSubTypes.Type(value = McpInstallationResultCatalogNegotiationRefusedError.class, name = "negotiation-refused"), + @JsonSubTypes.Type(value = McpInstallationResultCatalogInvalidRequestError.class, name = "invalid-request") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class McpInstallationResult { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultCatalogInvalidRequestError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultCatalogInvalidRequestError.java new file mode 100644 index 0000000000..9b79aeea42 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultCatalogInvalidRequestError.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; + +/** + * The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpInstallationResultCatalogInvalidRequestError extends McpInstallationResult { + + @JsonProperty("kind") + private final String kind = "invalid-request"; + + @Override + public String getKind() { return kind; } + + /** Which request field was rejected. */ + @JsonProperty("field") + private CatalogInvalidRequestField field; + + /** Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogInvalidRequestField getField() { return field; } + public void setField(CatalogInvalidRequestField field) { this.field = field; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultCatalogNegotiationRefusedError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultCatalogNegotiationRefusedError.java new file mode 100644 index 0000000000..c64e2bd63f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultCatalogNegotiationRefusedError.java @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpInstallationResultCatalogNegotiationRefusedError extends McpInstallationResult { + + @JsonProperty("kind") + private final String kind = "negotiation-refused"; + + @Override + public String getKind() { return kind; } + + /** Whether the version or the capability set was the problem. */ + @JsonProperty("reason") + private CatalogNegotiationRefusedReason reason; + + /** Protocol version of the runtime that refused the request. */ + @JsonProperty("runtimeProtocolVersion") + private Long runtimeProtocolVersion; + + /** Lowest caller protocol version this runtime will serve. */ + @JsonProperty("minimumSupportedProtocolVersion") + private Long minimumSupportedProtocolVersion; + + /** Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. */ + @JsonProperty("supportedCapabilities") + private List supportedCapabilities; + + /** The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. */ + @JsonProperty("unsupportedCapabilities") + private List unsupportedCapabilities; + + /** Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogNegotiationRefusedReason getReason() { return reason; } + public void setReason(CatalogNegotiationRefusedReason reason) { this.reason = reason; } + + public Long getRuntimeProtocolVersion() { return runtimeProtocolVersion; } + public void setRuntimeProtocolVersion(Long runtimeProtocolVersion) { this.runtimeProtocolVersion = runtimeProtocolVersion; } + + public Long getMinimumSupportedProtocolVersion() { return minimumSupportedProtocolVersion; } + public void setMinimumSupportedProtocolVersion(Long minimumSupportedProtocolVersion) { this.minimumSupportedProtocolVersion = minimumSupportedProtocolVersion; } + + public List getSupportedCapabilities() { return supportedCapabilities; } + public void setSupportedCapabilities(List supportedCapabilities) { this.supportedCapabilities = supportedCapabilities; } + + public List getUnsupportedCapabilities() { return unsupportedCapabilities; } + public void setUnsupportedCapabilities(List unsupportedCapabilities) { this.unsupportedCapabilities = unsupportedCapabilities; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultOutcome.java new file mode 100644 index 0000000000..0e83a6cd45 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationResultOutcome.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; + +/** + * Variant {@code outcome} of {@link McpInstallationResult}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpInstallationResultOutcome extends McpInstallationResult { + + @JsonProperty("kind") + private final String kind = "outcome"; + + @Override + public String getKind() { return kind; } + + /** Terminal result of the original operation. */ + @JsonProperty("outcome") + private Object outcome; + + /** Capabilities actually honoured for this request. */ + @JsonProperty("negotiated") + private CatalogNegotiatedContract negotiated; + + public Object getOutcome() { return outcome; } + public void setOutcome(Object outcome) { this.outcome = outcome; } + + public CatalogNegotiatedContract getNegotiated() { return negotiated; } + public void setNegotiated(CatalogNegotiatedContract negotiated) { this.negotiated = negotiated; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReview.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReview.java new file mode 100644 index 0000000000..4314e82025 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReview.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.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "action", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = McpInstallationReviewInstall.class, name = "install"), + @JsonSubTypes.Type(value = McpInstallationReviewUninstall.class, name = "uninstall") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public sealed interface McpInstallationReview permits McpInstallationReviewInstall, McpInstallationReviewUninstall { + /** + * Returns the discriminator value for this variant. + * + * @return the action discriminator + */ + @JsonProperty("action") + String action(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReviewInstall.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReviewInstall.java new file mode 100644 index 0000000000..b6aec56640 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReviewInstall.java @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code install} of {@link McpInstallationReview}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationReviewInstall( + /** Identity from the retained plan, not caller display text. */ + @JsonProperty("identity") McpPlanResourceIdentity identity, + /** Original source identity and content commitment. */ + @JsonProperty("provenance") McpPlanProvenance provenance, + /** Original catalogue trust metadata, not a verification claim. */ + @JsonProperty("catalogueTrust") CatalogTrustSnapshot catalogueTrust, + /** Catalogue identity retained from the bound candidate when available. */ + @JsonProperty("catalogue") InstallationCatalogueIdentity catalogue, + /** Exact reviewed user-scope destination. */ + @JsonProperty("target") McpPlanTarget target, + /** Policy decision bound to this plan. */ + @JsonProperty("policy") McpPlanPolicyResult policy, + /** Only the selected alternative is applied. */ + @JsonProperty("selectedChoice") McpPlanTransportChoice selectedChoice, + /** The configuration change for the selected alternative only. */ + @JsonProperty("configurationChange") McpPlanConfigurationChange configurationChange, + /** Non-secret values supplied for this selected alternative. */ + @JsonProperty("inputs") List inputs, + /** Exact reviewed placeholders supplied separately. Never secret values. */ + @JsonProperty("suppliedSecrets") List suppliedSecrets, + /** Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. */ + @JsonProperty("secretStorage") McpInstallationSecretStorage secretStorage, + /** 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. */ + @JsonProperty("effectiveConfiguration") McpInstallationRemoteConfiguration effectiveConfiguration, + /** Exact reviewed installation action. */ + @JsonProperty("action") String action +) implements McpInstallationReview { + public McpInstallationReviewInstall { + action = "install"; + } + + public McpInstallationReviewInstall( + McpPlanResourceIdentity identity, + McpPlanProvenance provenance, + CatalogTrustSnapshot catalogueTrust, + InstallationCatalogueIdentity catalogue, + McpPlanTarget target, + McpPlanPolicyResult policy, + McpPlanTransportChoice selectedChoice, + McpPlanConfigurationChange configurationChange, + List inputs, + List suppliedSecrets, + McpInstallationSecretStorage secretStorage, + McpInstallationRemoteConfiguration effectiveConfiguration + ) { + this(identity, provenance, catalogueTrust, catalogue, target, policy, selectedChoice, configurationChange, inputs, suppliedSecrets, secretStorage, effectiveConfiguration, "install"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReviewUninstall.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReviewUninstall.java new file mode 100644 index 0000000000..0a2c2c8f9f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationReviewUninstall.java @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Variant {@code uninstall} of {@link McpInstallationReview}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpInstallationReviewUninstall( + /** Receipt-owned installation being removed. */ + @JsonProperty("installationId") String installationId, + /** Identity from the installed receipt. */ + @JsonProperty("identity") McpPlanResourceIdentity identity, + /** Source identity and content commitment retained by the installed receipt. */ + @JsonProperty("provenance") McpPlanProvenance provenance, + /** Exact destination, checked for intervening changes before mutation. */ + @JsonProperty("target") McpPlanTarget target, + /** Current removal policy, independent of permission to activate the server. */ + @JsonProperty("policy") McpPlanPolicyResult policy, + /** Whether uninstall restores a protected pre-install configuration. */ + @JsonProperty("restoresPreviousConfiguration") Boolean restoresPreviousConfiguration, + /** Exact planner-owned secret slots to remove, excluding shared OAuth grants. */ + @JsonProperty("ownedSecretCount") Long ownedSecretCount, + /** Shared profile authentication is deliberately retained, not pending cleanup. */ + @JsonProperty("preservesSharedAuthentication") Boolean preservesSharedAuthentication, + /** Exact reviewed installation action. */ + @JsonProperty("action") String action +) implements McpInstallationReview { + public McpInstallationReviewUninstall { + action = "uninstall"; + } + + public McpInstallationReviewUninstall( + String installationId, + McpPlanResourceIdentity identity, + McpPlanProvenance provenance, + McpPlanTarget target, + McpPlanPolicyResult policy, + Boolean restoresPreviousConfiguration, + Long ownedSecretCount, + Boolean preservesSharedAuthentication + ) { + this(installationId, identity, provenance, target, policy, restoresPreviousConfiguration, ownedSecretCount, preservesSharedAuthentication, "uninstall"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSecret.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSecret.java new file mode 100644 index 0000000000..21c9a77569 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSecret.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; + +/** + * A request-local value for one exact reviewed placeholder. Never logged or persisted in a plan. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpInstallationSecret( + /** Exact placeholder from the selected choice, not a caller-chosen backend identifier. */ + @JsonProperty("placeholder") String placeholder, + /** Fresh explicit secret value. It is omitted from confirmation reviews and telemetry. */ + @JsonProperty("value") String value +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSecretStorage.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSecretStorage.java new file mode 100644 index 0000000000..ab73f14a71 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSecretStorage.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; + +/** + * Explicit backend selection is part of the final review; failures never switch backends. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpInstallationSecretStorage { + /** The {@code keychain} variant. */ + KEYCHAIN("keychain"), + /** The {@code private-file} variant. */ + PRIVATE_FILE("private-file"); + + private final String value; + McpInstallationSecretStorage(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpInstallationSecretStorage fromValue(String value) { + for (McpInstallationSecretStorage v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpInstallationSecretStorage value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationState.java new file mode 100644 index 0000000000..b6c7fcae6e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationState.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 javax.annotation.processing.Generated; + +/** + * Configuration ownership and setup observations, distinct from tool permissions. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpInstallationState { + /** The {@code needs-setup} variant. */ + NEEDS_SETUP("needs-setup"), + /** The {@code active} variant. */ + ACTIVE("active"), + /** The {@code authentication-required} variant. */ + AUTHENTICATION_REQUIRED("authentication-required"), + /** The {@code activation-failed} variant. */ + ACTIVATION_FAILED("activation-failed"), + /** The {@code configuration-modified} variant. */ + CONFIGURATION_MODIFIED("configuration-modified"), + /** The {@code recovery-required} variant. */ + RECOVERY_REQUIRED("recovery-required"); + + private final String value; + McpInstallationState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpInstallationState fromValue(String value) { + for (McpInstallationState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpInstallationState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSummary.java new file mode 100644 index 0000000000..b16180583b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationSummary.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 javax.annotation.processing.Generated; + +/** + * Durable configuration ownership is distinct from session-specific usability. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpInstallationSummary( + /** Exact durable installation receipt identity. */ + @JsonProperty("installationId") String installationId, + /** Original installing operation, not a fresh management operation. */ + @JsonProperty("operationId") String operationId, + /** Identity retained from the validated original plan. */ + @JsonProperty("identity") McpPlanResourceIdentity identity, + /** Exact alternative retained in the installing receipt. */ + @JsonProperty("choiceId") String choiceId, + /** Ownership or setup state, never inferred proof of tool usability. */ + @JsonProperty("state") McpInstallationState state, + /** Catalogue identity retained from the installed plan when available. */ + @JsonProperty("catalogue") InstallationCatalogueIdentity catalogue, + /** ISO 8601 wall-clock installation time when available. */ + @JsonProperty("installedAt") String installedAt +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsCancelParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsCancelParams.java new file mode 100644 index 0000000000..abc3dc69dd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsCancelParams.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; + +/** + * Existing-operation control. A new session selector is deliberately not accepted. + * + * @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 McpInstallationsCancelParams( + /** Original installation wire capability; new-work authentication is not reacquired. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Exact runtime-issued operation ID on the original connection. */ + @JsonProperty("operationId") String operationId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsListParams.java new file mode 100644 index 0000000000..6710f40429 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsListParams.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; + +/** + * New-work inventory or recovery request under an explicitly selected existing 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 McpInstallationsListParams( + /** Required authenticated bound installation contract. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Existing selected local session on this connection. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsRecoverParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsRecoverParams.java new file mode 100644 index 0000000000..e4aa30ec5c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsRecoverParams.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; + +/** + * New-work inventory or recovery request under an explicitly selected existing 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 McpInstallationsRecoverParams( + /** Required authenticated bound installation contract. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Existing selected local session on this connection. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsStatusParams.java new file mode 100644 index 0000000000..b23847caa4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpInstallationsStatusParams.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; + +/** + * Existing-operation control. A new session selector is deliberately not accepted. + * + * @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 McpInstallationsStatusParams( + /** Original installation wire capability; new-work authentication is not reacquired. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Exact runtime-issued operation ID on the original connection. */ + @JsonProperty("operationId") String operationId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpOwnedOauthLoginStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpOwnedOauthLoginStatus.java new file mode 100644 index 0000000000..0179b41424 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpOwnedOauthLoginStatus.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; + +/** + * Outcome of starting the original prepared owned login. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpOwnedOauthLoginStatus { + /** The {@code awaiting-browser} variant. */ + AWAITING_BROWSER("awaiting-browser"), + /** The {@code connected} variant. */ + CONNECTED("connected"); + + private final String value; + McpOwnedOauthLoginStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpOwnedOauthLoginStatus fromValue(String value) { + for (McpOwnedOauthLoginStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpOwnedOauthLoginStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanInstallRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanInstallRequest.java new file mode 100644 index 0000000000..cdf5762dec --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanInstallRequest.java @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + *

+ * 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 McpPlanInstallRequest { + + /** Protocol version and capabilities the caller requires. */ + @JsonProperty("contract") + private final CatalogClientContract contract; + + /** What to plan: either a candidate handle from a previous search, or a card supplied directly. */ + @JsonProperty("source") + private final Object source; + + /** Configuration scope the plan targets. Defaults to user scope when omitted. */ + @JsonProperty("scope") + private McpPlanScope scope; + + /** The same existing attached session that owns the original catalogue candidate. */ + @JsonProperty("policySessionId") + private String policySessionId; + + /** + * Creates a request with its required inputs. + * + * @param contract Protocol version and capabilities the caller requires. + * @param source What to plan: either a candidate handle from a previous search, or a card supplied directly. + */ + public McpPlanInstallRequest(CatalogClientContract contract, Object source) { + this.contract = Objects.requireNonNull(contract, "contract"); + this.source = Objects.requireNonNull(source, "source"); + } + + /** + * Returns the {@code contract} property. + * + * @return Protocol version and capabilities the caller requires. + */ + public CatalogClientContract getContract() { + return contract; + } + + /** + * Returns the {@code source} property. + * + * @return What to plan: either a candidate handle from a previous search, or a card supplied directly. + */ + public Object getSource() { + return source; + } + + /** + * Returns the {@code scope} property. + * + * @return Configuration scope the plan targets. Defaults to user scope when omitted. + */ + public McpPlanScope getScope() { + return scope; + } + + /** + * Returns the {@code policySessionId} property. + * + * @return The same existing attached session that owns the original catalogue candidate. + */ + public String getPolicySessionId() { + return policySessionId; + } + + /** + * Sets the {@code scope} property. + * + * @param value Configuration scope the plan targets. Defaults to user scope when omitted. + * @return this request + */ + public McpPlanInstallRequest setScope(McpPlanScope value) { + this.scope = value; + return this; + } + + /** + * Sets the {@code policySessionId} property. + * + * @param value The same existing attached session that owns the original catalogue candidate. + * @return this request + */ + public McpPlanInstallRequest setPolicySessionId(String value) { + this.policySessionId = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLineKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java similarity index 63% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLineKind.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java index 1064f1691d..cae9f57b89 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLineKind.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanPackageTransport.java @@ -10,26 +10,24 @@ import javax.annotation.processing.Generated; /** - * Kind of factory progress line. + * Transport exposed by a locally launched package * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum FactoryLogLineKind { - /** The {@code log} variant. */ - LOG("log"), - /** The {@code phase} variant. */ - PHASE("phase"); +public enum McpPlanPackageTransport { + /** The {@code stdio} variant. */ + STDIO("stdio"); private final String value; - FactoryLogLineKind(String value) { this.value = value; } + McpPlanPackageTransport(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator - public static FactoryLogLineKind fromValue(String value) { - for (FactoryLogLineKind v : values()) { + public static McpPlanPackageTransport fromValue(String value) { + for (McpPlanPackageTransport v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown FactoryLogLineKind value: " + value); + throw new IllegalArgumentException("Unknown McpPlanPackageTransport value: " + value); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRemoteTransport.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRemoteTransport.java new file mode 100644 index 0000000000..40366197df --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanRemoteTransport.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; + +/** + * Transport exposed by a remote endpoint + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpPlanRemoteTransport { + /** The {@code http} variant. */ + HTTP("http"), + /** The {@code streamable-http} variant. */ + STREAMABLE_HTTP("streamable-http"), + /** The {@code sse} variant. */ + SSE("sse"); + + private final String value; + McpPlanRemoteTransport(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpPlanRemoteTransport fromValue(String value) { + for (McpPlanRemoteTransport v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpPlanRemoteTransport value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanSecretPlaceholder.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanSecretPlaceholder.java new file mode 100644 index 0000000000..c54696caae --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanSecretPlaceholder.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 javax.annotation.processing.Generated; + +/** + * A secret a transport choice needs, referenced by placeholder. No secret value ever appears in a plan, and the placeholder resolves against the keychain only when a plan is applied. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpPlanSecretPlaceholder( + /** Key the secret is supplied under. Inert untrusted data. */ + @JsonProperty("key") String key, + /** The runtime-assigned `${secret:}` placeholder written into configuration in place of the value. */ + @JsonProperty("placeholder") String placeholder, + /** Human-readable label from the card. Inert untrusted text. */ + @JsonProperty("title") String title +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoice.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoice.java new file mode 100644 index 0000000000..2d9ce8e129 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoice.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.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * One eligible way to run the server, represented as a tagged package or remote variant so package identity and endpoint states cannot contradict the install method. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "installMethod", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = McpPlanTransportChoicePackage.class, name = "package"), + @JsonSubTypes.Type(value = McpPlanTransportChoiceRemote.class, name = "remote") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public sealed interface McpPlanTransportChoice permits McpPlanTransportChoicePackage, McpPlanTransportChoiceRemote { + /** + * Returns the discriminator value for this variant. + * + * @return the installMethod discriminator + */ + @JsonProperty("installMethod") + String installMethod(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoicePackage.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoicePackage.java new file mode 100644 index 0000000000..e53329ebc3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoicePackage.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * An eligible local-package transport choice. Package identity is required and a remote endpoint cannot be represented. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpPlanTransportChoicePackage( + /** Stable identifier for this choice within the plan, used to select it when the plan is applied. */ + @JsonProperty("choiceId") String choiceId, + /** Local process transport this package choice would use. */ + @JsonProperty("transport") McpPlanPackageTransport transport, + /** Discriminator: this choice runs a local package */ + @JsonProperty("installMethod") String installMethod, + /** Packaging ecosystem, for example `oci` or `npm`. */ + @JsonProperty("packageType") String packageType, + /** Package identifier. Inert untrusted data. */ + @JsonProperty("packageIdentifier") String packageIdentifier, + /** Typed values this choice requires, excluding secrets. */ + @JsonProperty("requiredValues") List requiredValues, + /** Secrets this choice requires, referenced by placeholder only. */ + @JsonProperty("secretPlaceholders") List secretPlaceholders +) implements McpPlanTransportChoice { + public McpPlanTransportChoicePackage { + installMethod = "package"; + } + + public McpPlanTransportChoicePackage( + String choiceId, + McpPlanPackageTransport transport, + String packageType, + String packageIdentifier, + List requiredValues, + List secretPlaceholders + ) { + this(choiceId, transport, "package", packageType, packageIdentifier, requiredValues, secretPlaceholders); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoiceRemote.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoiceRemote.java new file mode 100644 index 0000000000..0e1c04f168 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanTransportChoiceRemote.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * An eligible remote-endpoint transport choice. The endpoint is required and package identity cannot be represented. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record McpPlanTransportChoiceRemote( + /** Stable identifier for this choice within the plan, used to select it when the plan is applied. */ + @JsonProperty("choiceId") String choiceId, + /** Endpoint transport this remote choice would use. */ + @JsonProperty("transport") McpPlanRemoteTransport transport, + /** Discriminator: this choice connects to a remote endpoint */ + @JsonProperty("installMethod") String installMethod, + /** Endpoint URL. Inert untrusted data. */ + @JsonProperty("endpoint") String endpoint, + /** Typed values this choice requires, excluding secrets. */ + @JsonProperty("requiredValues") List requiredValues, + /** Secrets this choice requires, referenced by placeholder only. */ + @JsonProperty("secretPlaceholders") List secretPlaceholders +) implements McpPlanTransportChoice { + public McpPlanTransportChoiceRemote { + installMethod = "remote"; + } + + public McpPlanTransportChoiceRemote( + String choiceId, + McpPlanRemoteTransport transport, + String endpoint, + List requiredValues, + List secretPlaceholders + ) { + this(choiceId, transport, "remote", endpoint, requiredValues, secretPlaceholders); + } +} 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/McpPlanUninstallParams.java similarity index 64% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanUninstallParams.java index 193dea1b4c..e5dec43490 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanUninstallParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Result of registering or re-arming a scheduled prompt. + * Read-only preparation of one owned removal under fresh selected-session authority. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,10 +23,12 @@ @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 +public record McpPlanUninstallParams( + /** Required authenticated bound installation capabilities. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Exact receipt to inspect, not a server-name guess. */ + @JsonProperty("installationId") String installationId, + /** Existing selected session on the original connection. */ + @JsonProperty("policySessionId") String policySessionId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanValueCategory.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanValueCategory.java new file mode 100644 index 0000000000..e94008ef7a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPlanValueCategory.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; + +/** + * Where a required value is applied when the planned server is launched + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpPlanValueCategory { + /** The {@code environment-variable} variant. */ + ENVIRONMENT_VARIABLE("environment-variable"), + /** The {@code runtime-argument} variant. */ + RUNTIME_ARGUMENT("runtime-argument"), + /** The {@code package-argument} variant. */ + PACKAGE_ARGUMENT("package-argument"), + /** The {@code header} variant. */ + HEADER("header"), + /** The {@code url-variable} variant. */ + URL_VARIABLE("url-variable"); + + private final String value; + McpPlanValueCategory(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpPlanValueCategory fromValue(String value) { + for (McpPlanValueCategory v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpPlanValueCategory value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPrepareInstallParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPrepareInstallParams.java new file mode 100644 index 0000000000..998cbe7d73 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPrepareInstallParams.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Side-effect-free preparation of one original bound remote MCP choice. + * + * @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 McpPrepareInstallParams( + /** Required bound catalogue and confirmed remote installation capabilities. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Original single-use bound plan, never a client-authored configuration. */ + @JsonProperty("planHandle") String planHandle, + /** Exact selected alternative from that plan. */ + @JsonProperty("choiceId") String choiceId, + /** An existing local session attached to this connection, not permission to attach one. */ + @JsonProperty("policySessionId") String policySessionId, + /** Declared non-secret values. Non-empty only when the caller requires +`mcp-configured-remote-installation`; omitted values use the card default. */ + @JsonProperty("inputs") List inputs, + /** One entry per declared secret placeholder of the selected choice. Non-empty +only when the caller requires `mcp-configured-remote-installation`. */ + @JsonProperty("secrets") List secrets, + /** The exact original source, used transiently only after confirmation. */ + @JsonProperty("source") Object source, + /** The trusted host presents this choice alongside the exact secret placeholders. */ + @JsonProperty("secretStorage") McpInstallationSecretStorage secretStorage +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPreparedInstall.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPreparedInstall.java new file mode 100644 index 0000000000..7c50266b1d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPreparedInstall.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; + +/** + * Inert, runtime-owned admission. The operation ID is known before confirmation or effects. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpPreparedInstall( + /** Original connection-owned operation, known before the first confirmation callback. */ + @JsonProperty("operationId") String operationId, + /** Original plan expiry in Unix epoch milliseconds; preparation does not extend it. */ + @JsonProperty("expiresAtEpochMs") Long expiresAtEpochMs +) { +} 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/FactoryPauseCheckpointAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptRole.java similarity index 58% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPauseCheckpointAction.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptRole.java index a07aa8a677..461ebb3b73 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPauseCheckpointAction.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptRole.java @@ -10,26 +10,26 @@ import javax.annotation.processing.Generated; /** - * Action the runtime selected for a durable factory pause checkpoint. + * The sender role of an MCP prompt message. * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum FactoryPauseCheckpointAction { - /** The {@code continue} variant. */ - CONTINUE("continue"), - /** The {@code pause} variant. */ - PAUSE("pause"); +public enum McpPromptRole { + /** The {@code user} variant. */ + USER("user"), + /** The {@code assistant} variant. */ + ASSISTANT("assistant"); private final String value; - FactoryPauseCheckpointAction(String value) { this.value = value; } + McpPromptRole(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator - public static FactoryPauseCheckpointAction fromValue(String value) { - for (FactoryPauseCheckpointAction v : values()) { + public static McpPromptRole fromValue(String value) { + for (McpPromptRole v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown FactoryPauseCheckpointAction value: " + value); + throw new IllegalArgumentException("Unknown McpPromptRole value: " + value); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java index d180998c54..9b93d39830 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java @@ -27,6 +27,8 @@ public record McpServer( @JsonProperty("status") McpServerStatus status, /** Configuration source: user, workspace, plugin, builtin, or managed */ @JsonProperty("source") McpServerSource source, + /** Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. */ + @JsonProperty("url") String url, /** Plugin name that provided this server, when source is plugin. */ @JsonProperty("sourcePlugin") String sourcePlugin, /** Plugin version that provided this server, when source is plugin. */ @@ -36,6 +38,33 @@ public record McpServer( /** Error message if the server failed to connect */ @JsonProperty("error") String error, /** 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. */ - @JsonProperty("serverMetadata") McpServerMetadata serverMetadata + @JsonProperty("serverMetadata") McpServerMetadata serverMetadata, + /** 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. */ + @JsonProperty("owned") McpServerOwnership owned ) { + + /** + * Creates a record with the components it had before later optional fields were added. + * + * @param name Server name (config key) + * @param status Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured + * @param source Configuration source: user, workspace, plugin, builtin, or managed + * @param sourcePlugin Plugin name that provided this server, when source is plugin. + * @param sourcePluginVersion Plugin version that provided this server, when source is plugin. + * @param displayName Human-readable display name supplied by a managed server catalog. + * @param error Error message if the server failed to connect + * @param serverMetadata 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. + */ + public McpServer( + String name, + McpServerStatus status, + McpServerSource source, + String sourcePlugin, + String sourcePluginVersion, + String displayName, + String error, + McpServerMetadata serverMetadata + ) { + this(name, status, source, null, sourcePlugin, sourcePluginVersion, displayName, error, serverMetadata, null); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerOwnership.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerOwnership.java new file mode 100644 index 0000000000..1daefe117c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerOwnership.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; + +/** + * Owned installation that a listed MCP server's live configuration came from. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpServerOwnership( + /** Stable installation identifier from the owned installation receipt. */ + @JsonProperty("installationId") String installationId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java index 72b1503d59..0902ef8812 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, builtin, or managed + * Configuration source: user, workspace, plugin, builtin, managed, or account * * @since 1.0.0 */ @@ -25,7 +25,9 @@ public enum McpServerSource { /** The {@code builtin} variant. */ BUILTIN("builtin"), /** The {@code managed} variant. */ - MANAGED("managed"); + MANAGED("managed"), + /** The {@code account} variant. */ + ACCOUNT("account"); private final String value; McpServerSource(String value) { this.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/McpUninstallPlan.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpUninstallPlan.java new file mode 100644 index 0000000000..150c603d89 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpUninstallPlan.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 javax.annotation.processing.Generated; + +/** + * Exact inert removal plan. No configuration or credentials have changed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpUninstallPlan( + /** One-use original connection and authority-bound plan handle. */ + @JsonProperty("planHandle") String planHandle, + /** The original operation, inspectable and cancellable on this same connection. */ + @JsonProperty("operationId") String operationId, + /** Original wall-clock expiry in milliseconds. Applying never renews it. */ + @JsonProperty("expiresAtEpochMs") Long expiresAtEpochMs, + /** Original owned receipt being removed. */ + @JsonProperty("installation") McpInstallationSummary installation, + /** Whether removal restores a protected earlier configuration. */ + @JsonProperty("restoresPreviousConfiguration") Boolean restoresPreviousConfiguration, + /** Exact configured input slots owned by this installation, never shared OAuth tokens. */ + @JsonProperty("ownedSecretCount") Long ownedSecretCount, + /** Shared authentication is deliberately retained; revocation is a separate action. */ + @JsonProperty("preservesSharedAuthentication") Boolean preservesSharedAuthentication +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java index 3e9f0b6b87..63fbd2937a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java @@ -31,6 +31,8 @@ public record Model( @JsonProperty("capabilities") ModelCapabilities capabilities, /** Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. */ @JsonProperty("metadata") Map metadata, + /** Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. */ + @JsonProperty("vendor") String vendor, /** Policy state (if applicable) */ @JsonProperty("policy") ModelPolicy policy, /** Billing information */ @@ -50,6 +52,8 @@ public record Model( /** Informational notices the service published for this model, such as an upcoming change or a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model. */ @JsonProperty("infoMessages") List infoMessages, /** Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. */ - @JsonProperty("warningMessages") List warningMessages + @JsonProperty("warningMessages") List warningMessages, + /** The model provider that produced this model, as a neutral reference (opaque id, human-readable label, and provider kind). Present on models returned by `session.model.list`, which resolves provider attribution from the session's account roster; absent on the flat `server.models.list`, which does not resolve a session roster. The model picker groups by this reference. */ + @JsonProperty("provider") ModelProviderRef provider ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressPage.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java similarity index 55% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressPage.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java index 935140612e..cef8cb97cc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressPage.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * A bidirectional page of factory progress. + * Provider-reported model artifact metadata. * * @apiNote This type is experimental and may change in a future version. * @@ -25,18 +25,20 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryProgressPage( - /** Progress records in sequence order. */ - @JsonProperty("records") List records, - /** Oldest sequence number in this page, or null when empty. */ - @JsonProperty("oldestSeq") Long oldestSeq, - /** Newest sequence number in this page, or null when empty. */ - @JsonProperty("newestSeq") Long newestSeq, - /** Whether progress records older than this page exist. */ - @JsonProperty("hasMoreOlder") Boolean hasMoreOlder, - /** Whether progress records newer than this page exist. */ - @JsonProperty("hasMoreNewer") Boolean hasMoreNewer, - /** Run revision reflected by this page. */ - @JsonProperty("revision") Long revision +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/ModelChangeSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelChangeSource.java index 6f0687414c..c7505885fa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelChangeSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelChangeSource.java @@ -39,7 +39,9 @@ public enum ModelChangeSource { /** The {@code changeboarding_shortcut} variant. */ CHANGEBOARDING_SHORTCUT("changeboarding_shortcut"), /** The {@code sdk} variant. */ - SDK("sdk"); + SDK("sdk"), + /** The {@code auto_tier_recommendation} variant. */ + AUTO_TIER_RECOMMENDATION("auto_tier_recommendation"); private final String value; ModelChangeSource(String value) { this.value = value; } 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/ModelProviderDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDescriptor.java new file mode 100644 index 0000000000..fad2ec49ae --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDescriptor.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; + +/** + * 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. + * + * @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 ModelProviderDescriptor( + /** The neutral provider kind. */ + @JsonProperty("kind") ModelProviderKind kind, + /** Opaque, stable provider id, stamped onto every model this provider returns. */ + @JsonProperty("id") String id, + /** Human-readable menu label, owned by the runtime so every consumer renders identical text. */ + @JsonProperty("label") String label, + /** Stable ordering key for presenting providers in a deterministic sequence. */ + @JsonProperty("ordering") Long ordering +) { +} 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/ModelProviderKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderKind.java new file mode 100644 index 0000000000..7fed880421 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderKind.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; + +/** + * 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. + * + * @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 ModelProviderKind { + /** The {@code copilot} variant. */ + COPILOT("copilot"), + /** The {@code loki} variant. */ + LOKI("loki"); + + private final String value; + ModelProviderKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderKind fromValue(String value) { + for (ModelProviderKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderKind value: " + value); + } +} 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/FactoryProgressLine.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderRef.java similarity index 57% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressLine.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderRef.java index c9ed149610..d539c94b10 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressLine.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderRef.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * One durable factory progress record. + * A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. * * @apiNote This type is experimental and may change in a future version. * @@ -24,18 +24,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryProgressLine( - /** Global monotonic sequence number within the run. */ - @JsonProperty("seq") Long seq, - /** Resume attempt that emitted this record. */ - @JsonProperty("attempt") Long attempt, - /** Phase active when the record was emitted, or null before any phase. */ - @JsonProperty("phaseId") String phaseId, - /** Epoch milliseconds when the record was persisted. */ - @JsonProperty("recordedAt") Long recordedAt, - /** Progress record kind. */ - @JsonProperty("kind") FactoryLogLineKind kind, - /** Prompt-safe progress text. */ - @JsonProperty("text") String text +public record ModelProviderRef( + /** Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. */ + @JsonProperty("id") String id, + /** Human-readable provider label, owned by the runtime so every consumer renders identical text. */ + @JsonProperty("label") String label, + /** The provider kind. */ + @JsonProperty("kind") ModelProviderKind kind ) { } 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/PermissionModeSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionModeSource.java index 52f3e6cea3..c0f24f1157 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionModeSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionModeSource.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. + * 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. * * @apiNote This type is experimental and may change in a future version. * 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/ProviderDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderDescriptor.java new file mode 100644 index 0000000000..f56810a7f9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderDescriptor.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; + +/** + * A provider offered for interactive login. + * + * @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 ProviderDescriptor( + /** The neutral provider kind. */ + @JsonProperty("kind") LoginProviderKind kind, + /** Human-readable menu label, owned by the runtime so every consumer renders identical text. */ + @JsonProperty("label") String label, + /** Whether this provider is currently available to sign in with. */ + @JsonProperty("available") Boolean available +) { +} 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/RunOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RunOptions.java deleted file mode 100644 index ae535af8cf..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RunOptions.java +++ /dev/null @@ -1,37 +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; - -/** - * Options controlling factory invocation. - * - * @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 RunOptions( - /** Per-invocation resource ceiling overrides. */ - @JsonProperty("limits") FactoryRunLimits limits, - /** Whether to notify the originating session when the factory completes. */ - @JsonProperty("notifyOnComplete") Boolean notifyOnComplete, - /** Whether to emit factory phase names to the session transcript. */ - @JsonProperty("logPhaseNames") Boolean logPhaseNames, - /** Run identifier whose journal and progress should seed this resumed run. */ - @JsonProperty("resumeFromRunId") String resumeFromRunId -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java index 0b02851782..221d05b5b5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java @@ -43,6 +43,8 @@ public record SandboxConfig( @JsonProperty("managedLspRoutingLocked") Boolean managedLspRoutingLocked, /** Credential-injection capability flags. */ @JsonProperty("auth") SandboxConfigAuth auth, + /** 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. */ + @JsonProperty("credentials") SandboxCredentialsConfig credentials, /** 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). */ @JsonProperty("allowDevToolAccess") Boolean allowDevToolAccess ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigAuth.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigAuth.java index cb4efc78e7..0ea700ef7d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigAuth.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigAuth.java @@ -21,9 +21,9 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SandboxConfigAuth( - /** Whether to inject git credentials as an `http..extraheader` so authenticated HTTPS git works inside the sandbox without the shell-based credential helper the sandbox blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's own helper before the sandbox is applied. Default: false (opt-in). */ + /** Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http..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). */ @JsonProperty("git") Boolean git, - /** Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the OS keyring the sandbox blocks. Default: false (opt-in). */ + /** 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). */ @JsonProperty("gh") Boolean gh ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java index 73760b08a2..f7d55e6c71 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** - * Origin of the sandbox choice supplied by an internal client. + * Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. + * + * @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 SandboxConfigSource { /** The {@code never_configured} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxCredentialsConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxCredentialsConfig.java new file mode 100644 index 0000000000..840e1034e4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxCredentialsConfig.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Whole-value environment credential masking for sandboxed children. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxCredentialsConfig( + /** Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. */ + @JsonProperty("envVars") Map envVars +) { +} 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/SandboxMaskedEnvVar.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxMaskedEnvVar.java new file mode 100644 index 0000000000..7eb42463b0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxMaskedEnvVar.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Destinations authorized to receive one masked environment credential. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record 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. */ + @JsonProperty("injectHosts") List injectHosts +) { +} 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/FactoryRunStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java similarity index 51% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunStatus.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java index d5c87e7304..4a4dc15662 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java @@ -7,39 +7,37 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** - * Current or terminal state of a factory run. + * 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 FactoryRunStatus { - /** The {@code pending} variant. */ - PENDING("pending"), - /** The {@code running} variant. */ - RUNNING("running"), - /** The {@code completed} variant. */ - COMPLETED("completed"), - /** The {@code halted} variant. */ - HALTED("halted"), - /** The {@code paused} variant. */ - PAUSED("paused"), - /** The {@code cancelled} variant. */ - CANCELLED("cancelled"), +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; - FactoryRunStatus(String value) { this.value = value; } + SandboxProxyCaState(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator - public static FactoryRunStatus fromValue(String value) { - for (FactoryRunStatus v : values()) { + public static SandboxProxyCaState fromValue(String value) { + for (SandboxProxyCaState v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown FactoryRunStatus value: " + value); + 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/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..ddff436555 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 @@ -48,4 +48,59 @@ public CompletableFuture getDiscoveryPaths(Agents return caller.invoke("agents.getDiscoveryPaths", params, AgentsGetDiscoveryPathsResult.class); } + /** + * Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getBuiltins() { + return caller.invoke("agents.getBuiltins", java.util.Map.of(), AgentsGetBuiltinsResult.class); + } + + /** + * Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getAvailableBuiltins(AgentsGetAvailableBuiltinsParams params) { + return caller.invoke("agents.getAvailableBuiltins", params, AgentsGetAvailableBuiltinsResult.class); + } + + /** + * Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getBuiltinDefinition(AgentsGetBuiltinDefinitionParams params) { + return caller.invoke("agents.getBuiltinDefinition", params, AgentsGetBuiltinDefinitionResult.class); + } + + /** + * Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getBuiltinListingDefinition(AgentsGetBuiltinListingDefinitionParams params) { + return caller.invoke("agents.getBuiltinListingDefinition", params, AgentsGetBuiltinListingDefinitionResult.class); + } + + /** + * Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture customAgentInitialModelDecision(AgentsCustomAgentInitialModelDecisionParams params) { + return caller.invoke("agents.customAgentInitialModelDecision", params, AgentsCustomAgentInitialModelDecisionResult.class); + } + } 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 992faada12..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 @@ -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; @@ -27,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 @@ -38,7 +39,20 @@ public CompletableFuture search(CatalogSearchParams params) } /** - * Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture search(CatalogSearchRequest request) { + return caller.invoke("catalog.search", Objects.requireNonNull(request, "request"), CatalogSearchResult.class); + } + + /** + * 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/ServerConnectorsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerConnectorsApi.java new file mode 100644 index 0000000000..5b74d83721 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerConnectorsApi.java @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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 connectors} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerConnectorsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerConnectorsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Returns feature availability. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getCapabilities() { + return caller.invoke("connectors.getCapabilities", java.util.Map.of(), ConnectorsGetCapabilitiesResult.class); + } + + /** + * Returns eligible accounts. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getAccounts() { + return caller.invoke("connectors.getAccounts", java.util.Map.of(), ConnectorsGetAccountsResult.class); + } + + /** + * Lists entries for the selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list(ConnectorsListParams params) { + return caller.invoke("connectors.list", params, ConnectorsListResult.class); + } + + /** + * Refreshes entries for the selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture refresh(ConnectorsRefreshParams params) { + return caller.invoke("connectors.refresh", params, ConnectorsRefreshResult.class); + } + +} 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/ServerGitApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitApi.java new file mode 100644 index 0000000000..da512566d0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitApi.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 git} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGitApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGitApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Reads the remote that the branch checked out in a working tree tracks, as `branch..remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture currentBranchRemote(GitCurrentBranchRemoteParams params) { + return caller.invoke("git.currentBranchRemote", params, GitCurrentBranchRemoteResult.class); + } + + /** + * Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture workingDirectoryContext(GitWorkingDirectoryContextParams params) { + return caller.invoke("git.workingDirectoryContext", params, SessionWorkingDirectoryContext.class); + } + + /** + * Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture reposFromRemotes(GitReposFromRemotesParams params) { + return caller.invoke("git.reposFromRemotes", params, GitReposFromRemotesResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubOwnersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubOwnersApi.java new file mode 100644 index 0000000000..175c41400a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubOwnersApi.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 gitHubOwners} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGitHubOwnersApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGitHubOwnersApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture nextRequestId() { + return caller.invoke("gitHubOwners.nextRequestId", java.util.Map.of(), GitHubOwnersRequestIdResult.class); + } + + /** + * Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture list(GitHubOwnersListParams params) { + return caller.invoke("gitHubOwners.list", params, GitHubOwnersListResult.class); + } + + /** + * Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture cancel(GitHubOwnersCancelParams params) { + return caller.invoke("gitHubOwners.cancel", params, GitHubOwnersCancelResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubRepositoryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubRepositoryApi.java new file mode 100644 index 0000000000..d3eb8f0bd0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubRepositoryApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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 gitHubRepository} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGitHubRepositoryApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGitHubRepositoryApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture atPath(GitHubRepositoryAtPathParams params) { + return caller.invoke("gitHubRepository.atPath", params, GitHubRepositoryAtPathResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGlobalStateApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGlobalStateApi.java new file mode 100644 index 0000000000..3c215395b8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGlobalStateApi.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 globalState} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGlobalStateApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGlobalStateApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture load() { + return caller.invoke("globalState.load", java.util.Map.of(), GlobalStateLoadResult.class); + } + + /** + * Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture loadForConfigDir(GlobalStateLoadForConfigDirParams params) { + return caller.invoke("globalState.loadForConfigDir", params, GlobalStateLoadResult.class); + } + + /** + * Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture writeKey(GlobalStateWriteKeyParams params) { + return caller.invoke("globalState.writeKey", params, Void.class); + } + +} 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 new file mode 100644 index 0000000000..a921cdf945 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * 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 host} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerHostApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerHostApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture publishSession(HostPublishSessionParams params) { + return caller.invoke("host.publishSession", params, HostPublishSessionResult.class); + } + + /** + * 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, HostSessionCreateResult.class); + } + + /** + * Releases app ownership retention after AHP detaches. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture releaseSession(HostReleaseSessionParams params) { + return caller.invoke("host.releaseSession", params, Void.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture start(HostStartParams params) { + return caller.invoke("host.start", params, HostStartResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture dispose(HostDisposeParams params) { + return caller.invoke("host.dispose", params, Void.class); + } + + /** + * 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 getEnvironmentCredentials() { + return caller.invoke("host.getEnvironmentCredentials", java.util.Map.of(), HostEnvironmentCredentials.class); + } + + /** + * 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 + */ + @CopilotExperimental + CompletableFuture ready(HostReadyParams params) { + return caller.invoke("host.ready", params, Void.class); + } + +} 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 88dd7621fd..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 @@ -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; @@ -23,15 +24,18 @@ public final class ServerMcpApi { /** API methods for the {@code mcp.config} sub-namespace. */ public final ServerMcpConfigApi config; + /** API methods for the {@code mcp.installations} sub-namespace. */ + public final ServerMcpInstallationsApi installations; /** @param caller the RPC transport function */ ServerMcpApi(RpcCaller caller) { this.caller = caller; this.config = new ServerMcpConfigApi(caller); + this.installations = new ServerMcpInstallationsApi(caller); } /** - * 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 @@ -42,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 @@ -52,4 +56,61 @@ public CompletableFuture planInstall(McpPlanInstallParams return caller.invoke("mcp.planInstall", params, McpPlanInstallResult.class); } + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture planInstall(McpPlanInstallRequest request) { + return caller.invoke("mcp.planInstall", Objects.requireNonNull(request, "request"), McpPlanInstallResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture prepareInstall(McpPrepareInstallParams params) { + return caller.invoke("mcp.prepareInstall", params, McpInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture applyInstall(McpApplyInstallParams params) { + return caller.invoke("mcp.applyInstall", params, McpInstallationResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture planUninstall(McpPlanUninstallParams params) { + return caller.invoke("mcp.planUninstall", params, McpInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture applyUninstall(McpApplyUninstallParams params) { + return caller.invoke("mcp.applyUninstall", params, McpInstallationResult.class); + } + } 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 new file mode 100644 index 0000000000..6055d0fd3f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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.installations} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerMcpInstallationsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerMcpInstallationsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture list(McpInstallationsListParams params) { + return caller.invoke("mcp.installations.list", params, McpInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture recover(McpInstallationsRecoverParams params) { + return caller.invoke("mcp.installations.recover", params, McpInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture status(McpInstallationsStatusParams params) { + return caller.invoke("mcp.installations.status", params, McpInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture cancel(McpInstallationsCancelParams params) { + return caller.invoke("mcp.installations.cancel", params, McpInstallationManagementResult.class); + } + +} 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 05e6347107..99f65a5f89 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,10 @@ 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. */ public final ServerHooksApi hooks; /** API methods for the {@code models} namespace. */ @@ -41,20 +45,28 @@ public final class ServerRpc { public final ServerMcpApi mcp; /** API methods for the {@code extensions} namespace. */ public final ServerExtensionsApi extensions; + /** API methods for the {@code skills} namespace. */ + public final ServerSkillsApi skills; /** API methods for the {@code catalog} namespace. */ public final ServerCatalogApi catalog; /** API methods for the {@code plugins} namespace. */ public final ServerPluginsApi plugins; - /** API methods for the {@code skills} namespace. */ - public final ServerSkillsApi skills; /** API methods for the {@code agents} namespace. */ public final ServerAgentsApi agents; /** API methods for the {@code instructions} namespace. */ public final ServerInstructionsApi instructions; + /** API methods for the {@code globalState} namespace. */ + final ServerGlobalStateApi globalState; /** API methods for the {@code commands} namespace. */ public final ServerCommandsApi commands; /** API methods for the {@code user} namespace. */ public final ServerUserApi user; + /** API methods for the {@code gitHubRepository} namespace. */ + final ServerGitHubRepositoryApi gitHubRepository; + /** API methods for the {@code gitHubOwners} namespace. */ + final ServerGitHubOwnersApi gitHubOwners; + /** API methods for the {@code git} namespace. */ + final ServerGitApi git; /** API methods for the {@code managedSettings} namespace. */ public final ServerManagedSettingsApi managedSettings; /** API methods for the {@code runtime} namespace. */ @@ -67,6 +79,8 @@ public final class ServerRpc { public final ServerSessionsApi sessions; /** API methods for the {@code agentRegistry} namespace. */ public final ServerAgentRegistryApi agentRegistry; + /** API methods for the {@code connectors} namespace. */ + public final ServerConnectorsApi connectors; /** * Creates a new server RPC client. @@ -75,6 +89,8 @@ 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); this.sandbox = new ServerSandboxApi(caller); @@ -83,23 +99,28 @@ public ServerRpc(RpcCaller caller) { this.secrets = new ServerSecretsApi(caller); this.mcp = new ServerMcpApi(caller); this.extensions = new ServerExtensionsApi(caller); + this.skills = new ServerSkillsApi(caller); this.catalog = new ServerCatalogApi(caller); this.plugins = new ServerPluginsApi(caller); - this.skills = new ServerSkillsApi(caller); this.agents = new ServerAgentsApi(caller); this.instructions = new ServerInstructionsApi(caller); + this.globalState = new ServerGlobalStateApi(caller); this.commands = new ServerCommandsApi(caller); this.user = new ServerUserApi(caller); + this.gitHubRepository = new ServerGitHubRepositoryApi(caller); + this.gitHubOwners = new ServerGitHubOwnersApi(caller); + this.git = new ServerGitApi(caller); this.managedSettings = new ServerManagedSettingsApi(caller); this.runtime = new ServerRuntimeApi(caller); this.sessionFs = new ServerSessionFsApi(caller); this.llmInference = new ServerLlmInferenceApi(caller); this.sessions = new ServerSessionsApi(caller); this.agentRegistry = new ServerAgentRegistryApi(caller); + this.connectors = new ServerConnectorsApi(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 @@ -110,18 +131,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..fd963c0cbd 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,40 @@ public CompletableFuture enrichMetadata(SessionsEn } /** - * Active session ID and an optional flag for deferring repo-level hooks until folder trust. + * Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture createWorkspace(SessionsCreateWorkspaceParams params) { + return caller.invoke("sessions.createWorkspace", params, SessionsCreateWorkspaceResult.class); + } + + /** + * Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture loadWorkspace(SessionsLoadWorkspaceParams params) { + return caller.invoke("sessions.loadWorkspace", params, SessionsLoadWorkspaceResult.class); + } + + /** + * Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture updateWorkspaceFields(SessionsUpdateWorkspaceFieldsParams params) { + return caller.invoke("sessions.updateWorkspaceFields", params, Void.class); + } + + /** + * 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 +327,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 +338,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 +349,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 +371,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 +382,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 +393,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 +406,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 +417,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 +428,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 a7328dd567..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 @@ -21,17 +21,64 @@ public final class ServerSkillsApi { private final RpcCaller caller; + /** API methods for the {@code skills.installations} sub-namespace. */ + public final ServerSkillsInstallationsApi installations; /** API methods for the {@code skills.config} sub-namespace. */ public final ServerSkillsConfigApi config; /** @param caller the RPC transport function */ ServerSkillsApi(RpcCaller caller) { this.caller = caller; + this.installations = new ServerSkillsInstallationsApi(caller); this.config = new ServerSkillsConfigApi(caller); } /** - * Optional project paths and additional skill directories to include in discovery. + * 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 + */ + @CopilotExperimental + public CompletableFuture planInstall(SkillsPlanInstallParams params) { + return caller.invoke("skills.planInstall", params, SkillInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture applyInstall(SkillsApplyInstallParams params) { + return caller.invoke("skills.applyInstall", params, SkillInstallationResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture planUninstall(SkillsPlanUninstallParams params) { + return caller.invoke("skills.planUninstall", params, SkillInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture applyUninstall(SkillsApplyUninstallParams params) { + return caller.invoke("skills.applyUninstall", params, SkillInstallationResult.class); + } + + /** + * Discovers skills across global and project sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,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 new file mode 100644 index 0000000000..51029bc363 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.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 skills.installations} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSkillsInstallationsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerSkillsInstallationsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture list(SkillsInstallationsListParams params) { + return caller.invoke("skills.installations.list", params, SkillInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture recover(SkillsInstallationsRecoverParams params) { + return caller.invoke("skills.installations.recover", params, SkillInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture status(SkillsInstallationsStatusParams params) { + return caller.invoke("skills.installations.status", params, SkillInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture cancel(SkillsInstallationsCancelParams params) { + return caller.invoke("skills.installations.cancel", params, SkillInstallationManagementResult.class); + } + + /** + * 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 + */ + @CopilotExperimental + public CompletableFuture setEnabled(SkillsInstallationsSetEnabledParams params) { + return caller.invoke("skills.installations.setEnabled", params, SkillInstallationManagementResult.class); + } + +} 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..9c5656ff14 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,18 +27,7 @@ public final class ServerUserSettingsApi { } /** - * Invokes {@code user.settings.reload}. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ - @CopilotExperimental - public CompletableFuture reload() { - return caller.invoke("user.settings.reload", java.util.Map.of(), Void.class); - } - - /** - * 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 from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,14 +38,14 @@ 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture set(UserSettingsSetParams params) { - return caller.invoke("user.settings.set", params, UserSettingsSetResult.class); + public CompletableFuture set(UserSettingsSetParams params) { + return caller.invoke("user.settings.set", params, Void.class); } } 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 new file mode 100644 index 0000000000..2b4f752fdf --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * 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 SessionAccountsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** API methods for the {@code accounts.login} sub-namespace. */ + public final SessionAccountsLoginApi login; + + /** @param caller the RPC transport function */ + SessionAccountsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + this.login = new SessionAccountsLoginApi(caller, sessionId); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture enumerate(SessionAccountsEnumerateParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.accounts.enumerate", _p, AuthEnumerateValue.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture get(SessionAccountsGetParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.accounts.get", _p, AuthReadValue.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture set(SessionAccountsSetParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.accounts.set", _p, SessionAccountsSetResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsEnumerateParams.java similarity index 83% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsEnumerateParams.java index 00a1b4d660..480ba804b5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsEnumerateParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Parameters for pausing a running factory. + * Enumerate request carrying the typed collection query. * * @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 SessionFactoryPauseParams( +public record SessionAccountsEnumerateParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId + /** Which typed accounts collection to enumerate. */ + @JsonProperty("query") Object query ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunDetailParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsGetParams.java similarity index 85% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunDetailParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsGetParams.java index b4563d4b35..d75a1bc76a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunDetailParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsGetParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Parameters for retrieving a factory run. + * Read request carrying the typed datum query. * * @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 SessionFactoryGetRunDetailParams( +public record SessionAccountsGetParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId + /** Which typed accounts datum to read. */ + @JsonProperty("query") Object query ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginAdvanceParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginAdvanceParams.java new file mode 100644 index 0000000000..e4a8e34b3a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginAdvanceParams.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; + +/** + * Advance an in-flight login flow, optionally fulfilling an input-required step. + * + * @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 SessionAccountsLoginAdvanceParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque flow id from begin. */ + @JsonProperty("flowId") String flowId, + /** Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. */ + @JsonProperty("input") String input +) { +} 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 new file mode 100644 index 0000000000..24e3c6a642 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * 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.login} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAccountsLoginApi { + + 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 */ + SessionAccountsLoginApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture begin(SessionAccountsLoginBeginParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.accounts.login.begin", _p, SessionAccountsLoginBeginResult.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture advance(SessionAccountsLoginAdvanceParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.accounts.login.advance", _p, AuthLoginStep.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture cancel(SessionAccountsLoginCancelParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.accounts.login.cancel", _p, Void.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginBeginParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginBeginParams.java new file mode 100644 index 0000000000..2f2fdc473e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginBeginParams.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; + +/** + * Begin an interactive login flow for a provider kind. Dispatch is kind-only. + * + * @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 SessionAccountsLoginBeginParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The provider kind to sign in with. */ + @JsonProperty("kind") LoginProviderKind kind +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginBeginResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginBeginResult.java new file mode 100644 index 0000000000..451890e8c0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginBeginResult.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 started login flow: its opaque id and first step. + * + * @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 SessionAccountsLoginBeginResult( + /** Opaque flow id used to advance or cancel this login. */ + @JsonProperty("flowId") String flowId, + /** The first step of the flow. */ + @JsonProperty("step") Object step +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginCancelParams.java similarity index 85% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginCancelParams.java index f98e1f0d74..4426b02941 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginCancelParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Parameters for retrieving a factory run. + * Cancel an in-flight login flow. * * @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 SessionFactoryGetRunParams( +public record SessionAccountsLoginCancelParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId + /** Opaque flow id from begin. */ + @JsonProperty("flowId") String flowId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsSetParams.java similarity index 83% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsSetParams.java index 8ed7e4aa37..b7c16327d4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsSetParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Parameters for cancelling a factory run. + * Mutation request carrying the typed write command. * * @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 SessionFactoryCancelParams( +public record SessionAccountsSetParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId + /** The non-interactive mutation command to apply. */ + @JsonProperty("command") Object command ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsSetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsSetResult.java new file mode 100644 index 0000000000..7c6c42c029 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsSetResult.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; + +/** + * Result of a non-interactive accounts mutation. + * + * @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 SessionAccountsSetResult( + /** Whether the mutation was applied. */ + @JsonProperty("ok") Boolean ok, + /** For a logout, whether other signed-in accounts remain. */ + @JsonProperty("moreUsers") Boolean moreUsers +) { +} 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/SessionConnectedIdeInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectedIdeInfo.java new file mode 100644 index 0000000000..9a2f0bce74 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectedIdeInfo.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; + +/** + * The IDE a host is connected to, as reported to the session. + * + * @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) +record SessionConnectedIdeInfo( + /** Display name of the connected IDE, for example `VS Code`. */ + @JsonProperty("ideName") String ideName, + /** Absolute path of the workspace folder the IDE has open. */ + @JsonProperty("workspaceFolder") String workspaceFolder +) { +} 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/SessionConnectorsGetAccountParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetAccountParams.java new file mode 100644 index 0000000000..30288a8081 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetAccountParams.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 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/SessionFactoryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryApi.java deleted file mode 100644 index 12fa3cf6fd..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryApi.java +++ /dev/null @@ -1,245 +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 factory} namespace. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class SessionFactoryApi { - - private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; - - private final RpcCaller caller; - private final String sessionId; - - /** API methods for the {@code factory.journal} sub-namespace. */ - public final SessionFactoryJournalApi journal; - - /** @param caller the RPC transport function */ - SessionFactoryApi(RpcCaller caller, String sessionId) { - this.caller = caller; - this.sessionId = sessionId; - this.journal = new SessionFactoryJournalApi(caller, sessionId); - } - - /** - * Parameters for invoking a registered factory. - *

- * 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 run(SessionFactoryRunParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.run", _p, SessionFactoryRunResult.class); - } - - /** - * Parameters for resuming a factory run from its persisted identity. - *

- * 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 resume(SessionFactoryResumeParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.resume", _p, SessionFactoryResumeResult.class); - } - - /** - * Internal parameters for invoking a registered factory from a tool. - *

- * 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 runFromTool(SessionFactoryRunFromToolParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.runFromTool", _p, SessionFactoryRunFromToolResult.class); - } - - /** - * Internal parameters for resuming a factory run from a tool. - *

- * 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 resumeFromTool(SessionFactoryResumeFromToolParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.resumeFromTool", _p, SessionFactoryResumeFromToolResult.class); - } - - /** - * Parameters for retrieving a factory run. - *

- * 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 getRun(SessionFactoryGetRunParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.getRun", _p, SessionFactoryGetRunResult.class); - } - - /** - * Parameters for paging factory runs. - *

- * 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 listRuns(SessionFactoryListRunsParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.listRuns", _p, SessionFactoryListRunsResult.class); - } - - /** - * Parameters for retrieving a factory run. - *

- * 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 getRunDetail(SessionFactoryGetRunDetailParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.getRunDetail", _p, SessionFactoryGetRunDetailResult.class); - } - - /** - * Parameters for paging factory progress. - *

- * 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 getRunProgress(SessionFactoryGetRunProgressParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.getRunProgress", _p, SessionFactoryGetRunProgressResult.class); - } - - /** - * Parameters for cancelling a factory run. - *

- * 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 cancel(SessionFactoryCancelParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.cancel", _p, SessionFactoryCancelResult.class); - } - - /** - * Parameters for pausing a running factory. - *

- * 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 pause(SessionFactoryPauseParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.pause", _p, SessionFactoryPauseResult.class); - } - - /** - * Parameters for an owned durable pause checkpoint. - *

- * 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 pauseAtCheckpoint(SessionFactoryPauseAtCheckpointParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.pauseAtCheckpoint", _p, SessionFactoryPauseAtCheckpointResult.class); - } - - /** - * Parameters for recording factory progress. - *

- * 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 log(SessionFactoryLogParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.log", _p, Void.class); - } - - /** - * Parameters for one factory-scoped subagent call. - *

- * 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 agent(SessionFactoryAgentParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.agent", _p, SessionFactoryAgentResult.class); - } - -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java deleted file mode 100644 index 42e1b38811..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.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 factory 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 SessionFactoryCancelResult( - /** Factory 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 factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Completed factory 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/SessionFactoryGetRunDetailResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunDetailResult.java deleted file mode 100644 index 0a3fb7f83a..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunDetailResult.java +++ /dev/null @@ -1,75 +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; - -/** - * Full factory run observability detail. - * - * @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 SessionFactoryGetRunDetailResult( - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Registered factory name. */ - @JsonProperty("factoryName") String factoryName, - /** Human-readable factory description. */ - @JsonProperty("description") String description, - /** Current factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Monotonic durable run revision. */ - @JsonProperty("revision") Long revision, - /** Epoch milliseconds when the run was created. */ - @JsonProperty("createdAt") Long createdAt, - /** Epoch milliseconds when execution first started, or null before start. */ - @JsonProperty("startedAt") Long startedAt, - /** Epoch milliseconds when the durable run was last updated. */ - @JsonProperty("updatedAt") Long updatedAt, - /** Epoch milliseconds when the run completed, or null while nonterminal. */ - @JsonProperty("completedAt") Long completedAt, - /** Current phase identity, or null before any phase is entered. */ - @JsonProperty("currentPhase") FactoryCurrentPhase currentPhase, - /** Number of phases declared by the factory. */ - @JsonProperty("declaredPhaseCount") Long declaredPhaseCount, - /** Number of direct factory agents currently live. */ - @JsonProperty("liveAgentCount") Long liveAgentCount, - /** Total direct factory agents spawned across all attempts. */ - @JsonProperty("totalSpawnedAgentCount") Long totalSpawnedAgentCount, - /** Durable resource consumption. */ - @JsonProperty("consumed") FactoryRunConsumed consumed, - /** Resource ceilings declared by the factory. */ - @JsonProperty("declaredLimits") FactoryDeclaredLimits declaredLimits, - /** Approved effective resource ceilings, or null until approved. */ - @JsonProperty("approved") FactoryDeclaredLimits approved, - /** Epoch milliseconds when this live-overlay snapshot was observed. */ - @JsonProperty("observedAt") Long observedAt, - /** Epoch milliseconds when the current active segment started, or null while inactive. */ - @JsonProperty("activeSegmentStartedAt") Long activeSegmentStartedAt, - /** Terminal run outcome, or null while nonterminal. */ - @JsonProperty("terminal") FactoryRunTerminal terminal, - /** Whether the durable run state currently passes runtime resume eligibility checks. */ - @JsonProperty("canResume") Boolean canResume, - /** Lifecycle and timing observations for each factory phase. */ - @JsonProperty("phases") List phases, - /** Durable identities and live statuses for direct factory agents. */ - @JsonProperty("agents") List agents, - /** Bidirectional page of durable factory progress. */ - @JsonProperty("progress") FactoryProgressPage progress -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunProgressParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunProgressParams.java deleted file mode 100644 index 8445943cba..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunProgressParams.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.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; - -/** - * Parameters for paging factory progress. - * - * @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 SessionFactoryGetRunProgressParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Optional phase identifier used to scope records and cursors. */ - @JsonProperty("phaseId") String phaseId, - /** Exclusive forward cursor. */ - @JsonProperty("afterSeq") Long afterSeq, - /** Exclusive backward cursor. */ - @JsonProperty("beforeSeq") Long beforeSeq, - /** Maximum records to return. Defaults to 200 and is capped at 500. */ - @JsonProperty("limit") Long limit -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java deleted file mode 100644 index 9c141b28d4..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.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 factory 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 SessionFactoryGetRunResult( - /** Factory 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 factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Completed factory 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/SessionFactoryJournalGetParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalGetParams.java deleted file mode 100644 index 251ca946cb..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalGetParams.java +++ /dev/null @@ -1,36 +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; - -/** - * Parameters for reading a factory journal entry. - * - * @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 SessionFactoryJournalGetParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Opaque token identifying the current factory execution attempt. */ - @JsonProperty("executionToken") String executionToken, - /** Namespaced journal key. */ - @JsonProperty("key") String key -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalGetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalGetResult.java deleted file mode 100644 index 4b97e10296..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalGetResult.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 reading a factory journal entry. - * - * @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 SessionFactoryJournalGetResult( - /** Whether the journal contained the requested key. */ - @JsonProperty("hit") Boolean hit, - /** Cached JSON result. The hit field distinguishes a cached JSON null from a miss. */ - @JsonProperty("resultJson") Object resultJson -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalPutParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalPutParams.java deleted file mode 100644 index 06467b2657..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalPutParams.java +++ /dev/null @@ -1,38 +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; - -/** - * Parameters for storing a factory journal entry. - * - * @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 SessionFactoryJournalPutParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Opaque token identifying the current factory execution attempt. */ - @JsonProperty("executionToken") String executionToken, - /** Namespaced journal key. */ - @JsonProperty("key") String key, - /** JSON result to memoize. */ - @JsonProperty("resultJson") Object resultJson -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryListRunsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryListRunsParams.java deleted file mode 100644 index 41de4ae4a6..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryListRunsParams.java +++ /dev/null @@ -1,36 +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; - -/** - * Parameters for paging factory runs. - * - * @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 SessionFactoryListRunsParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Exclusive forward cursor. */ - @JsonProperty("afterSeq") Long afterSeq, - /** Exclusive backward cursor. */ - @JsonProperty("beforeSeq") Long beforeSeq, - /** Maximum terminal runs to return. Defaults to 200 and is capped at 500. */ - @JsonProperty("limit") Long limit -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseAtCheckpointParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseAtCheckpointParams.java deleted file mode 100644 index a54c089e20..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseAtCheckpointParams.java +++ /dev/null @@ -1,36 +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; - -/** - * Parameters for an owned durable pause checkpoint. - * - * @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 SessionFactoryPauseAtCheckpointParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Opaque token identifying the execution attempt that reached the checkpoint. */ - @JsonProperty("executionToken") String executionToken, - /** Stable author-defined checkpoint key. */ - @JsonProperty("key") String key -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseAtCheckpointResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseAtCheckpointResult.java deleted file mode 100644 index 4775fa0c6b..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseAtCheckpointResult.java +++ /dev/null @@ -1,30 +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 for the {@code session.factory.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 SessionFactoryPauseAtCheckpointResult( - /** Whether this execution attempt must pause or may continue. */ - @JsonProperty("action") FactoryPauseCheckpointAction action -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseResult.java deleted file mode 100644 index aa93afbea2..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryPauseResult.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 factory 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 SessionFactoryPauseResult( - /** Factory 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 factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Completed factory 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/SessionFactoryResumeFromToolParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeFromToolParams.java deleted file mode 100644 index 3a54179329..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeFromToolParams.java +++ /dev/null @@ -1,36 +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; - -/** - * Internal parameters for resuming a factory run from a tool. - * - * @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 SessionFactoryResumeFromToolParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Optional per-invocation resource ceiling overrides. */ - @JsonProperty("limits") FactoryRunLimits limits, - /** Opaque identifier of the originating tool call. */ - @JsonProperty("toolCallId") String toolCallId -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeParams.java deleted file mode 100644 index edb39e0811..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeParams.java +++ /dev/null @@ -1,38 +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; - -/** - * Parameters for resuming a factory run from its persisted identity. - * - * @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 SessionFactoryResumeParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Optional per-invocation resource ceiling overrides. */ - @JsonProperty("limits") FactoryRunLimits limits, - /** Whether to notify the originating session when the factory completes. */ - @JsonProperty("notifyOnComplete") Boolean notifyOnComplete, - /** Whether to emit factory phase names to the session transcript. */ - @JsonProperty("logPhaseNames") Boolean logPhaseNames -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolParams.java deleted file mode 100644 index dab8ae120d..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolParams.java +++ /dev/null @@ -1,38 +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; - -/** - * Internal parameters for invoking a registered factory from a tool. - * - * @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 SessionFactoryRunFromToolParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Registered factory name. */ - @JsonProperty("name") String name, - /** Factory input value. */ - @JsonProperty("args") Object args, - /** Tool-originated factory invocation options. */ - @JsonProperty("options") FactoryToolRunOptions options, - /** Opaque identifier of the originating tool call. */ - @JsonProperty("toolCallId") String toolCallId -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java deleted file mode 100644 index b76daece8d..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.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 factory 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 SessionFactoryRunFromToolResult( - /** Factory 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 factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Completed factory 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/SessionFactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java deleted file mode 100644 index 4a7cc978e4..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.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 factory 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 SessionFactoryRunResult( - /** Factory 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 factory run status. */ - @JsonProperty("status") FactoryRunStatus status, - /** Completed factory 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/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/SessionFsReadFileBytesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java new file mode 100644 index 0000000000..9e89b0c1a2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.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; + +/** + * 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 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReadFileBytesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** 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/SessionManagedPermissions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedPermissions.java index 6bf568c985..e746567651 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedPermissions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedPermissions.java @@ -29,6 +29,8 @@ public record SessionManagedPermissions( /** Permission rules that require explicit human approval. */ @JsonProperty("ask") List ask, /** Permission rules that allow matching operations unless another managed source, deny, or ask rule restricts them. */ - @JsonProperty("allow") List allow + @JsonProperty("allow") List allow, + /** Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected because every network request must be enforceable at host-level egress. Multiple managed sources intersect their lists; an empty list denies all hosts. */ + @JsonProperty("limitTo") List limitTo ) { } 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 ed66b7bc15..5f805354c4 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 @@ -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; @@ -32,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) { @@ -41,10 +44,27 @@ 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. + * Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + *

+ * 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 setConnectedIdeInfo(SessionMcpSetConnectedIdeInfoParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.setConnectedIdeInfo", _p, Void.class); + } + + /** + * 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 @@ -55,7 +75,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. @@ -71,7 +102,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. @@ -87,7 +118,22 @@ public CompletableFuture enable(SessionMcpEnableParams params) { } /** - * Name of the MCP server to disable for the session. + * Enables an MCP server for the session. + *

+ * 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 enable(SessionMcpEnableRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.enable", _p, Void.class); + } + + /** + * 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. @@ -103,7 +149,22 @@ public CompletableFuture disable(SessionMcpDisableParams params) { } /** - * Identifies the target session. + * Disables an MCP server for the session. + *

+ * 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 disable(SessionMcpDisableRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.disable", _p, Void.class); + } + + /** + * Reloads MCP server connections for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -114,7 +175,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 @@ -125,7 +186,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. @@ -134,14 +195,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. @@ -157,7 +218,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. @@ -173,7 +234,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. @@ -189,7 +250,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 @@ -200,7 +261,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. @@ -209,14 +270,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. @@ -232,7 +293,22 @@ public CompletableFuture startServer(SessionMcpStartServerParams 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. + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture startServer(SessionMcpStartServerRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.startServer", _p, Void.class); + } + + /** + * 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. @@ -248,7 +324,22 @@ public CompletableFuture restartServer(SessionMcpRestartServerParams param } /** - * Server name for an individual MCP server stop. + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture restartServer(SessionMcpRestartServerRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.restartServer", _p, Void.class); + } + + /** + * 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. @@ -264,7 +355,22 @@ public CompletableFuture stopServer(SessionMcpStopServerParams params) { } /** - * Registration parameters for an external MCP client. + * Stops an individual MCP server on the session's host. + *

+ * 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 stopServer(SessionMcpStopServerRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.stopServer", _p, Void.class); + } + + /** + * 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. @@ -273,14 +379,14 @@ public CompletableFuture stopServer(SessionMcpStopServerParams params) { * @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. @@ -289,14 +395,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/SessionMcpDisableRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpDisableRequest.java new file mode 100644 index 0000000000..1d8486fbc3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpDisableRequest.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Name of the MCP server to disable for the session. + *

+ * 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 SessionMcpDisableRequest { + + /** Name of the MCP server to disable */ + @JsonProperty("serverName") + private final String serverName; + + /** Required for an owned installation; omission preserves only manual-server behaviour. */ + @JsonProperty("expectedInstallationId") + private String expectedInstallationId; + + /** + * Creates a request with its required inputs. + * + * @param serverName Name of the MCP server to disable + */ + public SessionMcpDisableRequest(String serverName) { + this.serverName = Objects.requireNonNull(serverName, "serverName"); + } + + /** + * Returns the {@code serverName} property. + * + * @return Name of the MCP server to disable + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the {@code expectedInstallationId} property. + * + * @return Required for an owned installation; omission preserves only manual-server behaviour. + */ + public String getExpectedInstallationId() { + return expectedInstallationId; + } + + /** + * Sets the {@code expectedInstallationId} property. + * + * @param value Required for an owned installation; omission preserves only manual-server behaviour. + * @return this request + */ + public SessionMcpDisableRequest setExpectedInstallationId(String value) { + this.expectedInstallationId = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpEnableRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpEnableRequest.java new file mode 100644 index 0000000000..86e711db79 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpEnableRequest.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Name of the MCP server to enable for the session. + *

+ * 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 SessionMcpEnableRequest { + + /** Name of the MCP server to enable */ + @JsonProperty("serverName") + private final String serverName; + + /** Exact receipt identity for explicit owned activation in this session. */ + @JsonProperty("expectedInstallationId") + private String expectedInstallationId; + + /** + * Creates a request with its required inputs. + * + * @param serverName Name of the MCP server to enable + */ + public SessionMcpEnableRequest(String serverName) { + this.serverName = Objects.requireNonNull(serverName, "serverName"); + } + + /** + * Returns the {@code serverName} property. + * + * @return Name of the MCP server to enable + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the {@code expectedInstallationId} property. + * + * @return Exact receipt identity for explicit owned activation in this session. + */ + public String getExpectedInstallationId() { + return expectedInstallationId; + } + + /** + * Sets the {@code expectedInstallationId} property. + * + * @param value Exact receipt identity for explicit owned activation in this session. + * @return this request + */ + public SessionMcpEnableRequest setExpectedInstallationId(String value) { + this.expectedInstallationId = value; + return this; + } +} 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/SessionMcpListConfiguredParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java new file mode 100644 index 0000000000..d4d886b5b0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.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 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 cf4a3601ef..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 @@ -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 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. @@ -47,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. @@ -63,7 +64,23 @@ public CompletableFuture authenticationStateChanged(SessionMcpOauthAuthent } /** - * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture prepareLogin(SessionMcpOauthPrepareLoginParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.oauth.prepareLogin", _p, SessionMcpOauthPrepareLoginResult.class); + } + + /** + * 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. @@ -79,7 +96,38 @@ public CompletableFuture login(SessionMcpOauthLoginP } /** - * Remote MCP server name for a passive OAuth status probe. + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture login(SessionMcpOauthLoginRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.oauth.login", _p, SessionMcpOauthLoginResult.class); + } + + /** + * 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. @@ -95,7 +143,38 @@ public CompletableFuture probe(SessionMcpOauthProbeParams p } /** - * Pending MCP OAuth request id to respond to. + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture probe(SessionMcpOauthProbeRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.oauth.probe", _p, McpOauthProbeResult.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture cancelLogin(SessionMcpOauthCancelLoginParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.oauth.cancelLogin", _p, SessionMcpOauthCancelLoginResult.class); + } + + /** + * 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/SessionMcpOauthCancelLoginParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCancelLoginParams.java new file mode 100644 index 0000000000..b278ee76aa --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCancelLoginParams.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; + +/** + * Targets only the original prepared/applying owned login on this exact session requester. + * + * @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 SessionMcpOauthCancelLoginParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The same authoritative installation identity supplied during preparation. */ + @JsonProperty("expectedInstallationId") String expectedInstallationId, + /** Runtime-issued login handle known before the effectful login request begins. */ + @JsonProperty("loginId") String loginId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCancelLoginResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCancelLoginResult.java new file mode 100644 index 0000000000..3100837730 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCancelLoginResult.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; + +/** + * Honest terminal cancellation result; persistence or recovery failures remain RPC errors. + * + * @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 SessionMcpOauthCancelLoginResult( + /** True after cancellation settles, false when the original login already connected successfully. */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryAgentParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCompleteParams.java similarity index 63% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryAgentParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCompleteParams.java index 6ab02c27eb..be283f0b36 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryAgentParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCompleteParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Parameters for one factory-scoped subagent call. + * 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 @@ -23,16 +23,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionFactoryAgentParams( +public record SessionMcpOauthCompleteParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Factory run identifier that owns the subagent. */ - @JsonProperty("factoryRunId") String factoryRunId, - /** Opaque token identifying the current factory execution attempt. */ - @JsonProperty("executionToken") String executionToken, - /** Prompt to send to the subagent. */ - @JsonProperty("prompt") String prompt, - /** Subagent execution options. */ - @JsonProperty("opts") FactoryAgentOptions opts + /** 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 new file mode 100644 index 0000000000..82219d8042 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginRequest.java @@ -0,0 +1,290 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * 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. + * + * @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 SessionMcpOauthLoginRequest { + + /** Name of the remote MCP server to authenticate */ + @JsonProperty("serverName") + private final String serverName; + + /** 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. */ + @JsonProperty("forceReauth") + private Boolean forceReauth; + + /** 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. */ + @JsonProperty("clientName") + private String clientName; + + /** 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. */ + @JsonProperty("callbackSuccessMessage") + private String callbackSuccessMessage; + + /** Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. */ + @JsonProperty("clientId") + private String clientId; + + /** 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. */ + @JsonProperty("clientSecret") + private String clientSecret; + + /** 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. */ + @JsonProperty("publicClient") + private Boolean publicClient; + + /** Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. */ + @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; + + /** Exact owned receipt identity. Owned login never uses an implicit helper session. */ + @JsonProperty("expectedInstallationId") + private String expectedInstallationId; + + /** + * Creates a request with its required inputs. + * + * @param serverName Name of the remote MCP server to authenticate + */ + public SessionMcpOauthLoginRequest(String serverName) { + this.serverName = Objects.requireNonNull(serverName, "serverName"); + } + + /** + * Returns the {@code serverName} property. + * + * @return Name of the remote MCP server to authenticate + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the {@code forceReauth} property. + * + * @return 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. + */ + public Boolean getForceReauth() { + return forceReauth; + } + + /** + * Returns the {@code clientName} property. + * + * @return 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. + */ + public String getClientName() { + return clientName; + } + + /** + * Returns the {@code callbackSuccessMessage} property. + * + * @return 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. + */ + public String getCallbackSuccessMessage() { + return callbackSuccessMessage; + } + + /** + * Returns the {@code clientId} property. + * + * @return Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. + */ + public String getClientId() { + return clientId; + } + + /** + * Returns the {@code clientSecret} property. + * + * @return 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. + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * Returns the {@code publicClient} property. + * + * @return 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. + */ + public Boolean getPublicClient() { + return publicClient; + } + + /** + * Returns the {@code grantType} property. + * + * @return Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. + */ + 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. + * + * @return Required for owned login. Consumes the exact prepareLogin handle once. Set forceReauth and display options during preparation, not consumption. + */ + public String getLoginId() { + return loginId; + } + + /** + * Returns the {@code expectedInstallationId} property. + * + * @return Exact owned receipt identity. Owned login never uses an implicit helper session. + */ + public String getExpectedInstallationId() { + return expectedInstallationId; + } + + /** + * Sets the {@code forceReauth} property. + * + * @param value 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. + * @return this request + */ + public SessionMcpOauthLoginRequest setForceReauth(Boolean value) { + this.forceReauth = value; + return this; + } + + /** + * Sets the {@code clientName} property. + * + * @param value 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. + * @return this request + */ + public SessionMcpOauthLoginRequest setClientName(String value) { + this.clientName = value; + return this; + } + + /** + * Sets the {@code callbackSuccessMessage} property. + * + * @param value 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. + * @return this request + */ + public SessionMcpOauthLoginRequest setCallbackSuccessMessage(String value) { + this.callbackSuccessMessage = value; + return this; + } + + /** + * Sets the {@code clientId} property. + * + * @param value Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. + * @return this request + */ + public SessionMcpOauthLoginRequest setClientId(String value) { + this.clientId = value; + return this; + } + + /** + * Sets the {@code clientSecret} property. + * + * @param value 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. + * @return this request + */ + public SessionMcpOauthLoginRequest setClientSecret(String value) { + this.clientSecret = value; + return this; + } + + /** + * Sets the {@code publicClient} property. + * + * @param value 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. + * @return this request + */ + public SessionMcpOauthLoginRequest setPublicClient(Boolean value) { + this.publicClient = value; + return this; + } + + /** + * Sets the {@code grantType} property. + * + * @param value Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. + * @return this request + */ + public SessionMcpOauthLoginRequest setGrantType(McpOauthLoginGrantType value) { + this.grantType = 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. + * + * @param value Required for owned login. Consumes the exact prepareLogin handle once. Set forceReauth and display options during preparation, not consumption. + * @return this request + */ + public SessionMcpOauthLoginRequest setLoginId(String value) { + this.loginId = value; + return this; + } + + /** + * Sets the {@code expectedInstallationId} property. + * + * @param value Exact owned receipt identity. Owned login never uses an implicit helper session. + * @return this request + */ + public SessionMcpOauthLoginRequest setExpectedInstallationId(String value) { + this.expectedInstallationId = value; + return this; + } +} 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 d5f635dd54..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 @@ -24,7 +24,13 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record 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. */ - @JsonProperty("authorizationUrl") String authorizationUrl + /** Runtime-issued owned flow identity; never a server name or installation operation ID. */ + @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. 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/SessionMcpOauthPrepareLoginParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthPrepareLoginParams.java new file mode 100644 index 0000000000..fd907b041d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthPrepareLoginParams.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Effect-free preparation bound to the existing local session, requester and installation, with frozen options. + * + * @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 SessionMcpOauthPrepareLoginParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name recorded by the authoritative owned installation receipt. */ + @JsonProperty("serverName") String serverName, + /** Exact installation identity from owned inventory, never a server-name alias. */ + @JsonProperty("expectedInstallationId") String expectedInstallationId, + /** Request a new authorisation rather than accepting a usable cached grant. */ + @JsonProperty("forceReauth") Boolean forceReauth, + /** Display name used by the incumbent OAuth client-registration flow. */ + @JsonProperty("clientName") String clientName, + /** Text shown on the loopback callback page after successful authorisation. */ + @JsonProperty("callbackSuccessMessage") String callbackSuccessMessage +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthPrepareLoginResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthPrepareLoginResult.java new file mode 100644 index 0000000000..09c9653eda --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthPrepareLoginResult.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.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * An inert runtime-issued login handle. Preparation alone performs no activation or OAuth 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 SessionMcpOauthPrepareLoginResult( + /** Retain with the original requester and use for one login or cancellation. */ + @JsonProperty("loginId") String loginId, + /** Original expiry, not extended by consumption, retries or cancellation. */ + @JsonProperty("expiresAt") OffsetDateTime expiresAt +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthProbeRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthProbeRequest.java new file mode 100644 index 0000000000..f1d3b66131 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthProbeRequest.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Remote MCP server name for a passive OAuth status probe. + *

+ * 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 SessionMcpOauthProbeRequest { + + /** Name of the configured remote MCP server to probe. */ + @JsonProperty("serverName") + private final String serverName; + + /** Exact owned receipt identity; probing never activates a dormant installation. */ + @JsonProperty("expectedInstallationId") + private String expectedInstallationId; + + /** + * Creates a request with its required inputs. + * + * @param serverName Name of the configured remote MCP server to probe. + */ + public SessionMcpOauthProbeRequest(String serverName) { + this.serverName = Objects.requireNonNull(serverName, "serverName"); + } + + /** + * Returns the {@code serverName} property. + * + * @return Name of the configured remote MCP server to probe. + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the {@code expectedInstallationId} property. + * + * @return Exact owned receipt identity; probing never activates a dormant installation. + */ + public String getExpectedInstallationId() { + return expectedInstallationId; + } + + /** + * Sets the {@code expectedInstallationId} property. + * + * @param value Exact owned receipt identity; probing never activates a dormant installation. + * @return this request + */ + public SessionMcpOauthProbeRequest setExpectedInstallationId(String value) { + this.expectedInstallationId = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java similarity index 66% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalApi.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java index e5bfb4e66a..ec60f552ca 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryJournalApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java @@ -12,12 +12,12 @@ import javax.annotation.processing.Generated; /** - * API methods for the {@code factory.journal} namespace. + * API methods for the {@code mcp.prompts} namespace. * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class SessionFactoryJournalApi { +public final class SessionMcpPromptsApi { private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; @@ -25,13 +25,13 @@ public final class SessionFactoryJournalApi { private final String sessionId; /** @param caller the RPC transport function */ - SessionFactoryJournalApi(RpcCaller caller, String sessionId) { + SessionMcpPromptsApi(RpcCaller caller, String sessionId) { this.caller = caller; this.sessionId = sessionId; } /** - * Parameters for reading a factory journal entry. + * 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. @@ -40,14 +40,14 @@ public final class SessionFactoryJournalApi { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture get(SessionFactoryJournalGetParams params) { + public CompletableFuture list(SessionMcpPromptsListParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.journal.get", _p, SessionFactoryJournalGetResult.class); + return caller.invoke("session.mcp.prompts.list", _p, SessionMcpPromptsListResult.class); } /** - * Parameters for storing a factory journal entry. + * 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. @@ -56,10 +56,10 @@ public CompletableFuture get(SessionFactoryJourn * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture put(SessionFactoryJournalPutParams params) { + public CompletableFuture get(SessionMcpPromptsGetParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.factory.journal.put", _p, Void.class); + 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/FactoryAbortParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListParams.java similarity index 76% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAbortParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListParams.java index 178c135cdd..e1d21eef1a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAbortParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Parameters for cooperatively aborting a factory body. + * MCP server whose prompts to enumerate. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,12 +23,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryAbortParams( +public record SessionMcpPromptsListParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Opaque token identifying the execution attempt to abort. */ - @JsonProperty("executionToken") String executionToken + /** 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/SessionMcpRestartServerRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRestartServerRequest.java new file mode 100644 index 0000000000..dd98059e16 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRestartServerRequest.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; + +/** + * 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. + *

+ * 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 SessionMcpRestartServerRequest { + + /** Name of the MCP server to restart */ + @JsonProperty("serverName") + private final String serverName; + + /** 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). */ + @JsonProperty("config") + private Object config; + + /** Exact receipt identity for an explicit owned restart; configuration overrides are refused. */ + @JsonProperty("expectedInstallationId") + private String expectedInstallationId; + + /** + * Creates a request with its required inputs. + * + * @param serverName Name of the MCP server to restart + */ + public SessionMcpRestartServerRequest(String serverName) { + this.serverName = Objects.requireNonNull(serverName, "serverName"); + } + + /** + * Returns the {@code serverName} property. + * + * @return Name of the MCP server to restart + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the {@code config} property. + * + * @return 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). + */ + public Object getConfig() { + return config; + } + + /** + * Returns the {@code expectedInstallationId} property. + * + * @return Exact receipt identity for an explicit owned restart; configuration overrides are refused. + */ + public String getExpectedInstallationId() { + return expectedInstallationId; + } + + /** + * Sets the {@code config} property. + * + * @param value 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). + * @return this request + */ + public SessionMcpRestartServerRequest setConfig(Object value) { + this.config = value; + return this; + } + + /** + * Sets the {@code expectedInstallationId} property. + * + * @param value Exact receipt identity for an explicit owned restart; configuration overrides are refused. + * @return this request + */ + public SessionMcpRestartServerRequest setExpectedInstallationId(String value) { + this.expectedInstallationId = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpSetConnectedIdeInfoParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpSetConnectedIdeInfoParams.java new file mode 100644 index 0000000000..38e53b55d6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpSetConnectedIdeInfoParams.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; + +/** + * Records which IDE the host is connected to, or clears 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) +record SessionMcpSetConnectedIdeInfoParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. */ + @JsonProperty("ide") SessionConnectedIdeInfo ide +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStartServerRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStartServerRequest.java new file mode 100644 index 0000000000..8032ea9cc9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStartServerRequest.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; + +/** + * 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. + *

+ * 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 SessionMcpStartServerRequest { + + /** Name of the MCP server to start */ + @JsonProperty("serverName") + private final String serverName; + + /** MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). */ + @JsonProperty("config") + private Object config; + + /** Exact receipt identity for explicit owned activation in this session. */ + @JsonProperty("expectedInstallationId") + private String expectedInstallationId; + + /** + * Creates a request with its required inputs. + * + * @param serverName Name of the MCP server to start + */ + public SessionMcpStartServerRequest(String serverName) { + this.serverName = Objects.requireNonNull(serverName, "serverName"); + } + + /** + * Returns the {@code serverName} property. + * + * @return Name of the MCP server to start + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the {@code config} property. + * + * @return MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). + */ + public Object getConfig() { + return config; + } + + /** + * Returns the {@code expectedInstallationId} property. + * + * @return Exact receipt identity for explicit owned activation in this session. + */ + public String getExpectedInstallationId() { + return expectedInstallationId; + } + + /** + * Sets the {@code config} property. + * + * @param value MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). + * @return this request + */ + public SessionMcpStartServerRequest setConfig(Object value) { + this.config = value; + return this; + } + + /** + * Sets the {@code expectedInstallationId} property. + * + * @param value Exact receipt identity for explicit owned activation in this session. + * @return this request + */ + public SessionMcpStartServerRequest setExpectedInstallationId(String value) { + this.expectedInstallationId = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStopServerRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStopServerRequest.java new file mode 100644 index 0000000000..b59b8e2a62 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStopServerRequest.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Server name for an individual MCP server stop. + *

+ * 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 SessionMcpStopServerRequest { + + /** Name of the MCP server to stop */ + @JsonProperty("serverName") + private final String serverName; + + /** Exact owned receipt identity. Stop also forgets this session's durable activation. */ + @JsonProperty("expectedInstallationId") + private String expectedInstallationId; + + /** + * Creates a request with its required inputs. + * + * @param serverName Name of the MCP server to stop + */ + public SessionMcpStopServerRequest(String serverName) { + this.serverName = Objects.requireNonNull(serverName, "serverName"); + } + + /** + * Returns the {@code serverName} property. + * + * @return Name of the MCP server to stop + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the {@code expectedInstallationId} property. + * + * @return Exact owned receipt identity. Stop also forgets this session's durable activation. + */ + public String getExpectedInstallationId() { + return expectedInstallationId; + } + + /** + * Sets the {@code expectedInstallationId} property. + * + * @param value Exact owned receipt identity. Stop also forgets this session's durable activation. + * @return this request + */ + public SessionMcpStopServerRequest setExpectedInstallationId(String value) { + this.expectedInstallationId = value; + return this; + } +} 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/SessionMetadataContextInfoParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoParams.java index 0c5b909db7..a18643ccbf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoParams.java @@ -26,9 +26,9 @@ public record SessionMetadataContextInfoParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. */ + /** Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. */ @JsonProperty("promptTokenLimit") Long promptTokenLimit, - /** Maximum output tokens allowed by the target model. Pass 0 if unknown. */ + /** Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. */ @JsonProperty("outputTokenLimit") Long outputTokenLimit, /** Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. */ @JsonProperty("selectedModel") String selectedModel diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java index 6472956026..63350a4d53 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java @@ -44,13 +44,13 @@ public record SessionMetadataContextInfoResultContextInfo( @JsonProperty("mcpToolsTokens") Long mcpToolsTokens, /** Sum of system, conversation and tool-definition tokens */ @JsonProperty("totalTokens") Long totalTokens, - /** Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) */ + /** Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. */ @JsonProperty("promptTokenLimit") Long promptTokenLimit, /** Token count at which background compaction starts (configurable percentage of promptTokenLimit) */ @JsonProperty("compactionThreshold") Long compactionThreshold, - /** Prompt token limit plus the model's full output token limit. */ + /** Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. */ @JsonProperty("limit") Long limit, - /** Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) */ + /** Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). */ @JsonProperty("bufferTokens") Long bufferTokens ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java index c27f37afb0..44713111cf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java @@ -40,11 +40,11 @@ public record SessionMetadataGetContextAttributionResultContextAttribution( @JsonProperty("modelId") String modelId, /** 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). */ @JsonProperty("modelSource") String modelSource, - /** Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. */ + /** Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. */ @JsonProperty("promptTokenLimit") Long promptTokenLimit, - /** Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. */ + /** Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. */ @JsonProperty("limit") Long limit, - /** Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. */ + /** Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. */ @JsonProperty("bufferTokens") Long bufferTokens, /** Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. */ @JsonProperty("compactionThreshold") Long compactionThreshold, @@ -72,7 +72,7 @@ public record SessionMetadataGetContextAttributionResultContextAttributionCatego @JsonProperty("messages") Long messages, /** Remaining unused window capacity (clamped at 0). */ @JsonProperty("freeSpace") Long freeSpace, - /** Output reserve plus post-blocking-threshold buffer. */ + /** Overlapping output reservation plus post-blocking-threshold buffer. */ @JsonProperty("buffer") Long buffer ) { } 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 d24ac9e7c2..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. */ @@ -45,6 +45,10 @@ public record SessionModelApplyStartupOverlayParams( /** Model explicitly selected by the CLI, when provided. */ @JsonProperty("cliModel") String cliModel, /** Whether the overlay is being applied while resuming a deferred session. */ - @JsonProperty("deferredResume") Boolean deferredResume + @JsonProperty("deferredResume") Boolean deferredResume, + /** 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. */ + @JsonProperty("managedReasoningEffort") String managedReasoningEffort, + /** 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. */ + @JsonProperty("managedContextTier") String managedContextTier ) { } 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/SessionModelListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java index 8951499ef4..1c73b4b22f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java @@ -28,6 +28,8 @@ public record SessionModelListResult( /** 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`). */ @JsonProperty("list") List list, + /** 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. */ + @JsonProperty("providers") List providers, /** 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. */ @JsonProperty("modelPriceCategories") List modelPriceCategories, /** Per-quota snapshots returned alongside the model list, keyed by quota type. */ 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/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index 3deb651e25..dbb3080cdb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -101,7 +101,7 @@ public record SessionOpenOptions( @JsonProperty("shellProcessFlags") List shellProcessFlags, /** Resolved sandbox configuration. */ @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig, - /** Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. */ + /** 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. */ @JsonProperty("sandboxConfigSource") SandboxConfigSource sandboxConfigSource, /** Whether interactive shell sessions are logged. */ @JsonProperty("logInteractiveShells") Boolean logInteractiveShells, 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/SessionOptionsUpdateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index b3c3c14d48..3a30a13527 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -74,7 +74,7 @@ public record SessionOptionsUpdateParams( @JsonProperty("shellProcessFlags") List shellProcessFlags, /** Resolved sandbox configuration. */ @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig, - /** Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. */ + /** 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. */ @JsonProperty("sandboxConfigSource") SandboxConfigSource sandboxConfigSource, /** Whether interactive shell sessions are logged. */ @JsonProperty("logInteractiveShells") Boolean logInteractiveShells, 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/SessionPermissionsSetModeParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsSetModeParams.java index 712d460243..57abc3257f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsSetModeParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsSetModeParams.java @@ -30,7 +30,7 @@ public record SessionPermissionsSetModeParams( @JsonProperty("mode") PermissionMode mode, /** 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. */ @JsonProperty("assistedApprovalModel") String assistedApprovalModel, - /** Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. */ + /** 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. */ @JsonProperty("source") PermissionModeSource source ) { } 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/SessionFactoryLogParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawParams.java similarity index 72% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryLogParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawParams.java index b4f52617f1..8fbc84286d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryLogParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawParams.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Parameters for recording factory progress. + * 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 @@ -24,14 +24,10 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionFactoryLogParams( +public record SessionProviderWithdrawParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Opaque token identifying the current factory execution attempt. */ - @JsonProperty("executionToken") String executionToken, - /** Ordered progress lines to append. */ - @JsonProperty("lines") List lines + /** 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/SessionFactoryListRunsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawResult.java similarity index 52% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryListRunsResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawResult.java index d081003e47..4915e82aaf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryListRunsResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * A page of factory runs in durable creation order. + * What the withdrawal actually removed from the registry. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -24,16 +24,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionFactoryListRunsResult( - /** Factory run summaries in durable creation order. */ - @JsonProperty("runs") List runs, - /** Oldest terminal-run cursor in this page, or null when the terminal window is empty. */ - @JsonProperty("oldestSeq") Long oldestSeq, - /** Newest terminal-run cursor in this page, or null when the terminal window is empty. */ - @JsonProperty("newestSeq") Long newestSeq, - /** Whether terminal runs newer than this page exist. */ - @JsonProperty("hasMoreNewer") Boolean hasMoreNewer, - /** Number of terminal runs older than this page. */ - @JsonProperty("omittedOlder") Long omittedOlder +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/SessionProvidersDiscoverResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java new file mode 100644 index 0000000000..1467adc6a9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.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; + +/** + * Provider instances found by a discovery operation. + * + * @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 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 5f3f359551..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,16 +29,18 @@ 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. */ public final SessionGitHubAuthApi gitHubAuth; + /** API methods for the {@code accounts} namespace. */ + public final SessionAccountsApi accounts; /** API methods for the {@code debug} namespace. */ public final SessionDebugApi debug; /** API methods for the {@code canvas} namespace. */ public final SessionCanvasApi canvas; - /** API methods for the {@code factory} namespace. */ - public final SessionFactoryApi factory; /** API methods for the {@code workflow} namespace. */ public final SessionWorkflowApi workflow; /** API methods for the {@code model} 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,11 +131,12 @@ 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); this.debug = new SessionDebugApi(caller, sessionId); this.canvas = new SessionCanvasApi(caller, sessionId); - this.factory = new SessionFactoryApi(caller, sessionId); this.workflow = new SessionWorkflowApi(caller, sessionId); this.model = new SessionModelApi(caller, sessionId); this.mode = new SessionModeApi(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 02dbbea37a..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. @@ -57,4 +57,20 @@ public CompletableFuture disableForSessio return caller.invoke("session.sandbox.disableForSession", _p, SessionSandboxDisableForSessionResult.class); } + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture grantPathForRequest(SessionSandboxGrantPathForRequestParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.sandbox.grantPathForRequest", _p, SessionSandboxGrantPathForRequestResult.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryExecuteParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGrantPathForRequestParams.java similarity index 68% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryExecuteParams.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGrantPathForRequestParams.java index 6834dd4b12..a74dca91ec 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryExecuteParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGrantPathForRequestParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Parameters sent to the owning extension to execute a factory closure. + * Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,16 +23,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record FactoryExecuteParams( +public record SessionSandboxGrantPathForRequestParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Registered factory name. */ - @JsonProperty("name") String name, - /** Factory run identifier. */ - @JsonProperty("runId") String runId, - /** Opaque token identifying this factory execution attempt. */ - @JsonProperty("executionToken") String executionToken, - /** Factory input value. */ - @JsonProperty("args") Object args + /** Identifier of the exact pending sandbox escalation permission request whose sandboxPathGrant to accept. */ + @JsonProperty("requestId") String requestId, + /** Optional attribution for the permission decision. */ + @JsonProperty("decisionContext") PermissionDecisionContext decisionContext ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGrantPathForRequestResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGrantPathForRequestResult.java new file mode 100644 index 0000000000..95d176ab66 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGrantPathForRequestResult.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 accepting a sandbox path grant. + * + * @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 SessionSandboxGrantPathForRequestResult( + /** Whether this call resolved the pending request and added the path to the session's sandbox policy. */ + @JsonProperty("success") Boolean success +) { +} 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/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/SessionUiElicitationParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiElicitationParams.java index 72207e55bb..906d4bfa89 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiElicitationParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiElicitationParams.java @@ -34,7 +34,7 @@ public record SessionUiElicitationParams( /** JSON Schema describing the form fields to present to the user */ @JsonProperty("requestedSchema") UIElicitationSchema requestedSchema, /** MCP request metadata. */ - @JsonProperty("_meta") Map> meta, + @JsonProperty("_meta") Map meta, /** MCP task metadata. */ @JsonProperty("task") McpTaskMetadata task ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiElicitationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiElicitationResult.java index bf250f9d7d..c9e59bc2bd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiElicitationResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiElicitationResult.java @@ -30,6 +30,6 @@ public record SessionUiElicitationResult( /** The form values submitted by the user (present when action is 'accept') */ @JsonProperty("content") Map content, /** MCP response metadata. */ - @JsonProperty("_meta") Map> meta + @JsonProperty("_meta") Map meta ) { } 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/SessionWorkingDirectoryContextWithClient.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkingDirectoryContextWithClient.java new file mode 100644 index 0000000000..d909895e2d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkingDirectoryContextWithClient.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 javax.annotation.processing.Generated; + +/** + * A working-directory context together with the client that produced it. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionWorkingDirectoryContextWithClient( + /** Current working directory path */ + @JsonProperty("cwd") String cwd, + /** Root directory of the git repository */ + @JsonProperty("gitRoot") String gitRoot, + /** Repository identifier derived from the git remote URL */ + @JsonProperty("repository") String repository, + /** Hosting platform type of the repository */ + @JsonProperty("hostType") String hostType, + /** Current git branch name */ + @JsonProperty("branch") String branch, + /** Raw host string from the git remote URL */ + @JsonProperty("repositoryHost") String repositoryHost, + /** Head commit of the current git branch */ + @JsonProperty("headCommit") String headCommit, + /** Merge-base commit SHA */ + @JsonProperty("baseCommit") String baseCommit, + /** Name of the client that created the session */ + @JsonProperty("clientName") String clientName +) { +} 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/SessionsCreateWorkspaceParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceParams.java new file mode 100644 index 0000000000..04c54ddedb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceParams.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 javax.annotation.processing.Generated; + +/** + * Identity, state location and starting context for a workspace record. + * + * @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 SessionsCreateWorkspaceParams( + /** Session ID the workspace record belongs to */ + @JsonProperty("sessionId") String sessionId, + /** Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. */ + @JsonProperty("sessionStatePath") String sessionStatePath, + /** `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. */ + @JsonProperty("convention") String convention, + /** Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. */ + @JsonProperty("context") SessionWorkingDirectoryContextWithClient context, + /** User-supplied display name for the workspace */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceResult.java new file mode 100644 index 0000000000..b2cbe17721 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceResult.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; + +/** + * The workspace record that was written. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionsCreateWorkspaceResult( + /** The created workspace record, as JSON */ + @JsonProperty("workspaceJson") String workspaceJson +) { +} 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/SessionsLoadWorkspaceParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceParams.java new file mode 100644 index 0000000000..bb4b83da26 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceParams.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; + +/** + * Where the session's state lives, as a root directory and the session ID under 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) +record SessionsLoadWorkspaceParams( + /** Root directory every session's state directory sits under */ + @JsonProperty("sessionsHome") String sessionsHome, + /** Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceResult.java new file mode 100644 index 0000000000..f17c2c85d6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceResult.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; + +/** + * The workspace record on disk, omitted when the session has none. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionsLoadWorkspaceResult( + /** The workspace record, as JSON. Omitted when the record does not exist. */ + @JsonProperty("workspaceJson") String workspaceJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsUpdateWorkspaceFieldsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsUpdateWorkspaceFieldsParams.java new file mode 100644 index 0000000000..770e8269be --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsUpdateWorkspaceFieldsParams.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; + +/** + * Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. + * + * @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 SessionsUpdateWorkspaceFieldsParams( + /** Root directory every session's state directory sits under */ + @JsonProperty("sessionsHome") String sessionsHome, + /** Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. */ + @JsonProperty("sessionId") String sessionId, + /** Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. */ + @JsonProperty("fieldsJson") String fieldsJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallPlan.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallPlan.java new file mode 100644 index 0000000000..20ef7d4b3c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallPlan.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; + +/** + * A computed Skill install plan. Nothing has been applied. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillInstallPlan( + /** One-use plan handle, bound to the original candidate authority. */ + @JsonProperty("planHandle") String planHandle, + /** Original operation identifier returned before confirmation. */ + @JsonProperty("operationId") String operationId, + /** Original wall-clock expiry as an ISO 8601 timestamp. */ + @JsonProperty("expiresAt") String expiresAt, + /** Safe review to present before applying the plan. */ + @JsonProperty("review") SkillInstallationReview review +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationFailureReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationFailureReason.java new file mode 100644 index 0000000000..7751666ecd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationFailureReason.java @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Bounded refusal categories for verified Skill installation management. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SkillInstallationFailureReason { + /** The {@code feature-disabled} variant. */ + FEATURE_DISABLED("feature-disabled"), + /** The {@code invalid-request} variant. */ + INVALID_REQUEST("invalid-request"), + /** The {@code operation-limit} variant. */ + OPERATION_LIMIT("operation-limit"), + /** The {@code cancelled} variant. */ + CANCELLED("cancelled"), + /** The {@code confirmation-unavailable} variant. */ + CONFIRMATION_UNAVAILABLE("confirmation-unavailable"), + /** The {@code confirmation-invalid} variant. */ + CONFIRMATION_INVALID("confirmation-invalid"), + /** The {@code policy-context-unavailable} variant. */ + POLICY_CONTEXT_UNAVAILABLE("policy-context-unavailable"), + /** The {@code policy-changed} variant. */ + POLICY_CHANGED("policy-changed"), + /** The {@code resource-not-found} variant. */ + RESOURCE_NOT_FOUND("resource-not-found"), + /** The {@code plan-expired} variant. */ + PLAN_EXPIRED("plan-expired"), + /** The {@code plan-replayed} variant. */ + PLAN_REPLAYED("plan-replayed"), + /** The {@code foreign-runtime} variant. */ + FOREIGN_RUNTIME("foreign-runtime"), + /** The {@code wrong-kind} variant. */ + WRONG_KIND("wrong-kind"), + /** The {@code search-mismatch} variant. */ + SEARCH_MISMATCH("search-mismatch"), + /** The {@code expired} variant. */ + EXPIRED("expired"), + /** The {@code invalid-candidate} variant. */ + INVALID_CANDIDATE("invalid-candidate"), + /** The {@code descriptor-unavailable} variant. */ + DESCRIPTOR_UNAVAILABLE("descriptor-unavailable"), + /** The {@code descriptor-invalid} variant. */ + DESCRIPTOR_INVALID("descriptor-invalid"), + /** The {@code entrypoint-unavailable} variant. */ + ENTRYPOINT_UNAVAILABLE("entrypoint-unavailable"), + /** The {@code invalid-skill} variant. */ + INVALID_SKILL("invalid-skill"), + /** The {@code payload-unavailable} variant. */ + PAYLOAD_UNAVAILABLE("payload-unavailable"), + /** The {@code payload-mismatch} variant. */ + PAYLOAD_MISMATCH("payload-mismatch"), + /** The {@code source-changed} variant. */ + SOURCE_CHANGED("source-changed"), + /** The {@code lifecycle-unavailable} variant. */ + LIFECYCLE_UNAVAILABLE("lifecycle-unavailable"), + /** The {@code recovery-required} variant. */ + RECOVERY_REQUIRED("recovery-required"), + /** The {@code review-too-large} variant. */ + REVIEW_TOO_LARGE("review-too-large"), + /** The {@code busy} variant. */ + BUSY("busy"), + /** The {@code already-installed} variant. */ + ALREADY_INSTALLED("already-installed"), + /** The {@code configuration-modified} variant. */ + CONFIGURATION_MODIFIED("configuration-modified"), + /** The {@code write-failed} variant. */ + WRITE_FAILED("write-failed"); + + private final String value; + SkillInstallationFailureReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SkillInstallationFailureReason fromValue(String value) { + for (SkillInstallationFailureReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SkillInstallationFailureReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationFileReview.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationFileReview.java new file mode 100644 index 0000000000..0164b1f75a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationFileReview.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 javax.annotation.processing.Generated; + +/** + * One reviewed Skill file. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillInstallationFileReview( + /** Relative file path within the Skill root. */ + @JsonProperty("path") String path, + /** Exact reviewed file size in bytes. */ + @JsonProperty("sizeBytes") Long sizeBytes, + /** Declared media type for the file. */ + @JsonProperty("mediaType") String mediaType, + /** Whether the file is installed with executable permissions. */ + @JsonProperty("executable") Boolean executable, + /** SHA-256 digest of the exact file bytes. */ + @JsonProperty("digest") String digest +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationLocation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationLocation.java new file mode 100644 index 0000000000..770b3cbc45 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationLocation.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; + +/** + * A user-facing personal Skill installation location without absolute host paths. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillInstallationLocation( + /** Installation scope. Agent Finder Skills are installed in the user's personal Copilot home. */ + @JsonProperty("scope") SkillInstallationScope scope, + /** Path relative to the Copilot home. */ + @JsonProperty("relativePath") String relativePath, + /** Safe display label, for example ~/.copilot/skills/run-checks. */ + @JsonProperty("displayLabel") String displayLabel, + /** Diagnostics-only absolute host path. Hosts must not display it by default. */ + @JsonProperty("diagnosticsAbsolutePath") String diagnosticsAbsolutePath +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcome.java new file mode 100644 index 0000000000..f3d70c2908 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcome.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.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Management outcome for verified Skill inventory, planning and removal. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeRecoveryRequired.class, name = "recovery-required"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeInstallPlanned.class, name = "install-planned"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeListed.class, name = "listed"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeRecovered.class, name = "recovered"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeRolledBack.class, name = "rolled-back"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeUninstallPlanned.class, name = "uninstall-planned"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeOperation.class, name = "operation"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeEnabledChanged.class, name = "enabled-changed"), + @JsonSubTypes.Type(value = SkillInstallationManagementOutcomeRefused.class, name = "refused") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public sealed interface SkillInstallationManagementOutcome permits SkillInstallationManagementOutcomeRecoveryRequired, SkillInstallationManagementOutcomeInstallPlanned, SkillInstallationManagementOutcomeListed, SkillInstallationManagementOutcomeRecovered, SkillInstallationManagementOutcomeRolledBack, SkillInstallationManagementOutcomeUninstallPlanned, SkillInstallationManagementOutcomeOperation, SkillInstallationManagementOutcomeEnabledChanged, SkillInstallationManagementOutcomeRefused { + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + @JsonProperty("kind") + String kind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeEnabledChanged.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeEnabledChanged.java new file mode 100644 index 0000000000..d1ab0a8426 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeEnabledChanged.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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Enablement changed and the selected session was reconciled. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeEnabledChanged( + /** Updated installation summary. */ + @JsonProperty("installation") SkillInstallationSummary installation, + /** Safe reload or reconciliation diagnostics. */ + @JsonProperty("diagnostics") List diagnostics, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeEnabledChanged { + kind = "enabled-changed"; + } + + public SkillInstallationManagementOutcomeEnabledChanged( + SkillInstallationSummary installation, + List diagnostics + ) { + this(installation, diagnostics, "enabled-changed"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeInstallPlanned.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeInstallPlanned.java new file mode 100644 index 0000000000..f8db595bf8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeInstallPlanned.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * A verified Skill installation plan was prepared. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeInstallPlanned( + /** Prepared install plan. */ + @JsonProperty("plan") SkillInstallPlan plan, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeInstallPlanned { + kind = "install-planned"; + } + + public SkillInstallationManagementOutcomeInstallPlanned( + SkillInstallPlan plan + ) { + this(plan, "install-planned"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeListed.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeListed.java new file mode 100644 index 0000000000..ae06228f42 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeListed.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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Owned Skill installations were listed. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeListed( + /** Owned Skill installation summaries. */ + @JsonProperty("installations") List installations, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeListed { + kind = "listed"; + } + + public SkillInstallationManagementOutcomeListed( + List installations + ) { + this(installations, "listed"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeOperation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeOperation.java new file mode 100644 index 0000000000..082a124477 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeOperation.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Original-connection operation snapshot. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeOperation( + /** Operation status. */ + @JsonProperty("operation") SkillInstallationOperationStatus operation, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeOperation { + kind = "operation"; + } + + public SkillInstallationManagementOutcomeOperation( + SkillInstallationOperationStatus operation + ) { + this(operation, "operation"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRecovered.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRecovered.java new file mode 100644 index 0000000000..71a8674fc1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRecovered.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.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Recovery completed and inventory was inspected. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeRecovered( + /** Owned Skill installation summaries after recovery. */ + @JsonProperty("installations") List installations, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeRecovered { + kind = "recovered"; + } + + public SkillInstallationManagementOutcomeRecovered( + List installations + ) { + this(installations, "recovered"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRecoveryRequired.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRecoveryRequired.java new file mode 100644 index 0000000000..9009fc8fbe --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRecoveryRequired.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Already-confirmed durable work must be reconciled before new mutations or inventory. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeRecoveryRequired( + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeRecoveryRequired { + kind = "recovery-required"; + } + + public SkillInstallationManagementOutcomeRecoveryRequired() { + this("recovery-required"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRefused.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRefused.java new file mode 100644 index 0000000000..062ac877b5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRefused.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * The management request was refused. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeRefused( + /** Bounded refusal reason. */ + @JsonProperty("reason") SkillInstallationFailureReason reason, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeRefused { + kind = "refused"; + } + + public SkillInstallationManagementOutcomeRefused( + SkillInstallationFailureReason reason + ) { + this(reason, "refused"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRolledBack.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRolledBack.java new file mode 100644 index 0000000000..c47f290465 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeRolledBack.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Interrupted work was safely compensated and the pending marker was cleared. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeRolledBack( + /** Cause of the compensation. */ + @JsonProperty("reason") SkillInstallationFailureReason reason, + /** Original operation identity. */ + @JsonProperty("operation_id") String operationId, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeRolledBack { + kind = "rolled-back"; + } + + public SkillInstallationManagementOutcomeRolledBack( + SkillInstallationFailureReason reason, + String operationId + ) { + this(reason, operationId, "rolled-back"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeUninstallPlanned.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeUninstallPlanned.java new file mode 100644 index 0000000000..74c27e3749 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementOutcomeUninstallPlanned.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * An owned Skill uninstall plan was prepared. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationManagementOutcomeUninstallPlanned( + /** Prepared uninstall plan. */ + @JsonProperty("plan") SkillUninstallPlan plan, + /** Skill installation management outcome discriminator. */ + @JsonProperty("kind") String kind +) implements SkillInstallationManagementOutcome { + public SkillInstallationManagementOutcomeUninstallPlanned { + kind = "uninstall-planned"; + } + + public SkillInstallationManagementOutcomeUninstallPlanned( + SkillUninstallPlan plan + ) { + this(plan, "uninstall-planned"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResult.java new file mode 100644 index 0000000000..d610c7b892 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResult.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Skill installation management result with the honoured contract, or a typed refusal. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SkillInstallationManagementResultOutcome.class, name = "outcome"), + @JsonSubTypes.Type(value = SkillInstallationManagementResultCatalogNegotiationRefusedError.class, name = "negotiation-refused"), + @JsonSubTypes.Type(value = SkillInstallationManagementResultCatalogInvalidRequestError.class, name = "invalid-request") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class SkillInstallationManagementResult { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultCatalogInvalidRequestError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultCatalogInvalidRequestError.java new file mode 100644 index 0000000000..126e640418 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultCatalogInvalidRequestError.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; + +/** + * The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationManagementResultCatalogInvalidRequestError extends SkillInstallationManagementResult { + + @JsonProperty("kind") + private final String kind = "invalid-request"; + + @Override + public String getKind() { return kind; } + + /** Which request field was rejected. */ + @JsonProperty("field") + private CatalogInvalidRequestField field; + + /** Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogInvalidRequestField getField() { return field; } + public void setField(CatalogInvalidRequestField field) { this.field = field; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultCatalogNegotiationRefusedError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultCatalogNegotiationRefusedError.java new file mode 100644 index 0000000000..8ac3b5b603 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultCatalogNegotiationRefusedError.java @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationManagementResultCatalogNegotiationRefusedError extends SkillInstallationManagementResult { + + @JsonProperty("kind") + private final String kind = "negotiation-refused"; + + @Override + public String getKind() { return kind; } + + /** Whether the version or the capability set was the problem. */ + @JsonProperty("reason") + private CatalogNegotiationRefusedReason reason; + + /** Protocol version of the runtime that refused the request. */ + @JsonProperty("runtimeProtocolVersion") + private Long runtimeProtocolVersion; + + /** Lowest caller protocol version this runtime will serve. */ + @JsonProperty("minimumSupportedProtocolVersion") + private Long minimumSupportedProtocolVersion; + + /** Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. */ + @JsonProperty("supportedCapabilities") + private List supportedCapabilities; + + /** The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. */ + @JsonProperty("unsupportedCapabilities") + private List unsupportedCapabilities; + + /** Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogNegotiationRefusedReason getReason() { return reason; } + public void setReason(CatalogNegotiationRefusedReason reason) { this.reason = reason; } + + public Long getRuntimeProtocolVersion() { return runtimeProtocolVersion; } + public void setRuntimeProtocolVersion(Long runtimeProtocolVersion) { this.runtimeProtocolVersion = runtimeProtocolVersion; } + + public Long getMinimumSupportedProtocolVersion() { return minimumSupportedProtocolVersion; } + public void setMinimumSupportedProtocolVersion(Long minimumSupportedProtocolVersion) { this.minimumSupportedProtocolVersion = minimumSupportedProtocolVersion; } + + public List getSupportedCapabilities() { return supportedCapabilities; } + public void setSupportedCapabilities(List supportedCapabilities) { this.supportedCapabilities = supportedCapabilities; } + + public List getUnsupportedCapabilities() { return unsupportedCapabilities; } + public void setUnsupportedCapabilities(List unsupportedCapabilities) { this.unsupportedCapabilities = unsupportedCapabilities; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultOutcome.java new file mode 100644 index 0000000000..aab2602572 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationManagementResultOutcome.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; + +/** + * Variant {@code outcome} of {@link SkillInstallationManagementResult}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationManagementResultOutcome extends SkillInstallationManagementResult { + + @JsonProperty("kind") + private final String kind = "outcome"; + + @Override + public String getKind() { return kind; } + + /** Observed management outcome. */ + @JsonProperty("outcome") + private SkillInstallationManagementOutcome outcome; + + /** Capabilities honoured for this request. */ + @JsonProperty("negotiated") + private CatalogNegotiatedContract negotiated; + + public SkillInstallationManagementOutcome getOutcome() { return outcome; } + public void setOutcome(SkillInstallationManagementOutcome outcome) { this.outcome = outcome; } + + public CatalogNegotiatedContract getNegotiated() { return negotiated; } + public void setNegotiated(CatalogNegotiatedContract negotiated) { this.negotiated = negotiated; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatus.java new file mode 100644 index 0000000000..4d34398a93 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatus.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Status snapshot from the original connection, independent of new-work account availability. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "phase", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SkillInstallationOperationStatusPreparing.class, name = "preparing"), + @JsonSubTypes.Type(value = SkillInstallationOperationStatusPrepared.class, name = "prepared"), + @JsonSubTypes.Type(value = SkillInstallationOperationStatusAwaitingConfirmation.class, name = "awaiting-confirmation"), + @JsonSubTypes.Type(value = SkillInstallationOperationStatusRevalidating.class, name = "revalidating"), + @JsonSubTypes.Type(value = SkillInstallationOperationStatusApplying.class, name = "applying"), + @JsonSubTypes.Type(value = SkillInstallationOperationStatusCompleted.class, name = "completed") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public sealed interface SkillInstallationOperationStatus permits SkillInstallationOperationStatusPreparing, SkillInstallationOperationStatusPrepared, SkillInstallationOperationStatusAwaitingConfirmation, SkillInstallationOperationStatusRevalidating, SkillInstallationOperationStatusApplying, SkillInstallationOperationStatusCompleted { + /** + * Returns the discriminator value for this variant. + * + * @return the phase discriminator + */ + @JsonProperty("phase") + String phase(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusApplying.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusApplying.java new file mode 100644 index 0000000000..c0e14e4bb4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusApplying.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Original operation progress discriminator. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationOperationStatusApplying( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation has been requested; already-started effects require recovery. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + @JsonProperty("phase") String phase +) implements SkillInstallationOperationStatus { + public SkillInstallationOperationStatusApplying { + phase = "applying"; + } + + public SkillInstallationOperationStatusApplying( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "applying"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusAwaitingConfirmation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusAwaitingConfirmation.java new file mode 100644 index 0000000000..f6729fb980 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusAwaitingConfirmation.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Original operation progress discriminator. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationOperationStatusAwaitingConfirmation( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation has been requested. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + @JsonProperty("phase") String phase +) implements SkillInstallationOperationStatus { + public SkillInstallationOperationStatusAwaitingConfirmation { + phase = "awaiting-confirmation"; + } + + public SkillInstallationOperationStatusAwaitingConfirmation( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "awaiting-confirmation"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusCompleted.java new file mode 100644 index 0000000000..77667eb430 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusCompleted.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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Original operation progress discriminator. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationOperationStatusCompleted( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation was requested before the terminal result. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + /** Immutable terminal receipt. */ + @JsonProperty("outcome") Object outcome, + @JsonProperty("phase") String phase +) implements SkillInstallationOperationStatus { + public SkillInstallationOperationStatusCompleted { + phase = "completed"; + } + + public SkillInstallationOperationStatusCompleted( + String operationId, + Boolean cancellationRequested, + Object outcome + ) { + this(operationId, cancellationRequested, outcome, "completed"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusPrepared.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusPrepared.java new file mode 100644 index 0000000000..d22cc62052 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusPrepared.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Original operation progress discriminator. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationOperationStatusPrepared( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation has been requested. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + @JsonProperty("phase") String phase +) implements SkillInstallationOperationStatus { + public SkillInstallationOperationStatusPrepared { + phase = "prepared"; + } + + public SkillInstallationOperationStatusPrepared( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "prepared"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusPreparing.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusPreparing.java new file mode 100644 index 0000000000..63e8dec3d2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusPreparing.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Original operation progress discriminator. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationOperationStatusPreparing( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation has been requested. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + @JsonProperty("phase") String phase +) implements SkillInstallationOperationStatus { + public SkillInstallationOperationStatusPreparing { + phase = "preparing"; + } + + public SkillInstallationOperationStatusPreparing( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "preparing"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusRevalidating.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusRevalidating.java new file mode 100644 index 0000000000..c0395ed887 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOperationStatusRevalidating.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.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Original operation progress discriminator. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public record SkillInstallationOperationStatusRevalidating( + /** Original runtime-issued operation identity. */ + @JsonProperty("operationId") String operationId, + /** Whether cancellation has been requested. */ + @JsonProperty("cancellationRequested") Boolean cancellationRequested, + @JsonProperty("phase") String phase +) implements SkillInstallationOperationStatus { + public SkillInstallationOperationStatusRevalidating { + phase = "revalidating"; + } + + public SkillInstallationOperationStatusRevalidating( + String operationId, + Boolean cancellationRequested + ) { + this(operationId, cancellationRequested, "revalidating"); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOwnershipState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOwnershipState.java new file mode 100644 index 0000000000..5b2929e557 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationOwnershipState.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; + +/** + * Owned Skill state observed from files and receipts. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SkillInstallationOwnershipState { + /** The {@code intact} variant. */ + INTACT("intact"), + /** The {@code modified} variant. */ + MODIFIED("modified"), + /** The {@code recovery-required} variant. */ + RECOVERY_REQUIRED("recovery-required"); + + private final String value; + SkillInstallationOwnershipState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SkillInstallationOwnershipState fromValue(String value) { + for (SkillInstallationOwnershipState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SkillInstallationOwnershipState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResult.java new file mode 100644 index 0000000000..c16185e1b7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResult.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Skill installation result with the honoured contract, or a typed request/negotiation refusal. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SkillInstallationResultOutcome.class, name = "outcome"), + @JsonSubTypes.Type(value = SkillInstallationResultCatalogNegotiationRefusedError.class, name = "negotiation-refused"), + @JsonSubTypes.Type(value = SkillInstallationResultCatalogInvalidRequestError.class, name = "invalid-request") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class SkillInstallationResult { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultCatalogInvalidRequestError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultCatalogInvalidRequestError.java new file mode 100644 index 0000000000..04494500ef --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultCatalogInvalidRequestError.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; + +/** + * The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationResultCatalogInvalidRequestError extends SkillInstallationResult { + + @JsonProperty("kind") + private final String kind = "invalid-request"; + + @Override + public String getKind() { return kind; } + + /** Which request field was rejected. */ + @JsonProperty("field") + private CatalogInvalidRequestField field; + + /** Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogInvalidRequestField getField() { return field; } + public void setField(CatalogInvalidRequestField field) { this.field = field; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultCatalogNegotiationRefusedError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultCatalogNegotiationRefusedError.java new file mode 100644 index 0000000000..2d98038965 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultCatalogNegotiationRefusedError.java @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationResultCatalogNegotiationRefusedError extends SkillInstallationResult { + + @JsonProperty("kind") + private final String kind = "negotiation-refused"; + + @Override + public String getKind() { return kind; } + + /** Whether the version or the capability set was the problem. */ + @JsonProperty("reason") + private CatalogNegotiationRefusedReason reason; + + /** Protocol version of the runtime that refused the request. */ + @JsonProperty("runtimeProtocolVersion") + private Long runtimeProtocolVersion; + + /** Lowest caller protocol version this runtime will serve. */ + @JsonProperty("minimumSupportedProtocolVersion") + private Long minimumSupportedProtocolVersion; + + /** Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. */ + @JsonProperty("supportedCapabilities") + private List supportedCapabilities; + + /** The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. */ + @JsonProperty("unsupportedCapabilities") + private List unsupportedCapabilities; + + /** Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ + @JsonProperty("message") + private String message; + + public CatalogNegotiationRefusedReason getReason() { return reason; } + public void setReason(CatalogNegotiationRefusedReason reason) { this.reason = reason; } + + public Long getRuntimeProtocolVersion() { return runtimeProtocolVersion; } + public void setRuntimeProtocolVersion(Long runtimeProtocolVersion) { this.runtimeProtocolVersion = runtimeProtocolVersion; } + + public Long getMinimumSupportedProtocolVersion() { return minimumSupportedProtocolVersion; } + public void setMinimumSupportedProtocolVersion(Long minimumSupportedProtocolVersion) { this.minimumSupportedProtocolVersion = minimumSupportedProtocolVersion; } + + public List getSupportedCapabilities() { return supportedCapabilities; } + public void setSupportedCapabilities(List supportedCapabilities) { this.supportedCapabilities = supportedCapabilities; } + + public List getUnsupportedCapabilities() { return unsupportedCapabilities; } + public void setUnsupportedCapabilities(List unsupportedCapabilities) { this.unsupportedCapabilities = unsupportedCapabilities; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultOutcome.java new file mode 100644 index 0000000000..d288a8abf9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationResultOutcome.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; + +/** + * Variant {@code outcome} of {@link SkillInstallationResult}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationResultOutcome extends SkillInstallationResult { + + @JsonProperty("kind") + private final String kind = "outcome"; + + @Override + public String getKind() { return kind; } + + /** Terminal operation outcome. */ + @JsonProperty("outcome") + private Object outcome; + + /** Capabilities honoured for this request. */ + @JsonProperty("negotiated") + private CatalogNegotiatedContract negotiated; + + public Object getOutcome() { return outcome; } + public void setOutcome(Object outcome) { this.outcome = outcome; } + + public CatalogNegotiatedContract getNegotiated() { return negotiated; } + public void setNegotiated(CatalogNegotiatedContract negotiated) { this.negotiated = negotiated; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReview.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReview.java new file mode 100644 index 0000000000..2b0eca3042 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReview.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.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Safe verified Skill review fields. No raw credential, candidate handle or plan handle. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "action", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SkillInstallationReviewInstall.class, name = "install"), + @JsonSubTypes.Type(value = SkillInstallationReviewUninstall.class, name = "uninstall") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class SkillInstallationReview { + + /** + * Returns the discriminator value for this variant. + * + * @return the action discriminator + */ + public abstract String getAction(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReviewInstall.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReviewInstall.java new file mode 100644 index 0000000000..baaef1a326 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReviewInstall.java @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Review for installing a verified Skill. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationReviewInstall extends SkillInstallationReview { + + @JsonProperty("action") + private final String action = "install"; + + @Override + public String getAction() { return action; } + + /** Skill invocation name from SKILL.md. */ + @JsonProperty("name") + private String name; + + /** Skill description from SKILL.md when present. */ + @JsonProperty("description") + private String description; + + /** Catalogue identity retained from the bound candidate before consent. */ + @JsonProperty("catalogue") + private InstallationCatalogueIdentity catalogue; + + /** Exact verified source identity. */ + @JsonProperty("source") + private SkillInstallationSource source; + + /** Exact user-scope target location without an absolute host path. */ + @JsonProperty("target") + private SkillInstallationLocation target; + + /** Installing never grants immediate use; the Skill is written disabled. */ + @JsonProperty("installsDisabled") + private Boolean installsDisabled; + + /** Reviewed files and digests. */ + @JsonProperty("files") + private List files; + + /** Total reviewed payload size in bytes. */ + @JsonProperty("totalBytes") + private Long totalBytes; + + /** Relative path of the verified Skill entrypoint. */ + @JsonProperty("entrypointPath") + private String entrypointPath; + + /** Complete verified SKILL.md content. Planning refuses with review-too-large +when this exceeds 262144 UTF-8 bytes; it is never truncated. */ + @JsonProperty("entrypointContent") + private String entrypointContent; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public InstallationCatalogueIdentity getCatalogue() { return catalogue; } + public void setCatalogue(InstallationCatalogueIdentity catalogue) { this.catalogue = catalogue; } + + public SkillInstallationSource getSource() { return source; } + public void setSource(SkillInstallationSource source) { this.source = source; } + + public SkillInstallationLocation getTarget() { return target; } + public void setTarget(SkillInstallationLocation target) { this.target = target; } + + public Boolean getInstallsDisabled() { return installsDisabled; } + public void setInstallsDisabled(Boolean installsDisabled) { this.installsDisabled = installsDisabled; } + + public List getFiles() { return files; } + public void setFiles(List files) { this.files = files; } + + public Long getTotalBytes() { return totalBytes; } + public void setTotalBytes(Long totalBytes) { this.totalBytes = totalBytes; } + + public String getEntrypointPath() { return entrypointPath; } + public void setEntrypointPath(String entrypointPath) { this.entrypointPath = entrypointPath; } + + public String getEntrypointContent() { return entrypointContent; } + public void setEntrypointContent(String entrypointContent) { this.entrypointContent = entrypointContent; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReviewUninstall.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReviewUninstall.java new file mode 100644 index 0000000000..cd2f24c0cc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationReviewUninstall.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.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; + +/** + * Review for uninstalling an owned verified Skill. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInstallationReviewUninstall extends SkillInstallationReview { + + @JsonProperty("action") + private final String action = "uninstall"; + + @Override + public String getAction() { return action; } + + /** Owned installation being removed. */ + @JsonProperty("installation") + private SkillInstallationSummary installation; + + /** Whether current files differ from the receipt. Apply refuses drift. */ + @JsonProperty("filesModified") + private Boolean filesModified; + + /** Files recorded by the installation receipt. */ + @JsonProperty("files") + private List files; + + /** Total receipt-owned payload size in bytes. */ + @JsonProperty("totalBytes") + private Long totalBytes; + + /** Catalogue identity retained at install time. */ + @JsonProperty("catalogue") + private InstallationCatalogueIdentity catalogue; + + public SkillInstallationSummary getInstallation() { return installation; } + public void setInstallation(SkillInstallationSummary installation) { this.installation = installation; } + + public Boolean getFilesModified() { return filesModified; } + public void setFilesModified(Boolean filesModified) { this.filesModified = filesModified; } + + public List getFiles() { return files; } + public void setFiles(List files) { this.files = files; } + + public Long getTotalBytes() { return totalBytes; } + public void setTotalBytes(Long totalBytes) { this.totalBytes = totalBytes; } + + public InstallationCatalogueIdentity getCatalogue() { return catalogue; } + public void setCatalogue(InstallationCatalogueIdentity catalogue) { this.catalogue = catalogue; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationScope.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationScope.java new file mode 100644 index 0000000000..9c23d3c971 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationScope.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SkillInstallationScope { + /** The {@code personal} variant. */ + PERSONAL("personal"); + + private final String value; + SkillInstallationScope(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SkillInstallationScope fromValue(String value) { + for (SkillInstallationScope v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SkillInstallationScope value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSessionState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSessionState.java new file mode 100644 index 0000000000..8c8ea3b86f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSessionState.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; + +/** + * Bound-session observation after reconciling persisted enablement. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SkillInstallationSessionState { + /** The {@code loaded-enabled} variant. */ + LOADED_ENABLED("loaded-enabled"), + /** The {@code loaded-disabled} variant. */ + LOADED_DISABLED("loaded-disabled"), + /** The {@code not-loaded} variant. */ + NOT_LOADED("not-loaded"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"); + + private final String value; + SkillInstallationSessionState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SkillInstallationSessionState fromValue(String value) { + for (SkillInstallationSessionState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SkillInstallationSessionState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSource.java new file mode 100644 index 0000000000..30c5b78b3d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSource.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 javax.annotation.processing.Generated; + +/** + * Source identity retained from Agent Finder and the pinned GitHub descriptor. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillInstallationSource( + /** Agent Finder resource identifier. */ + @JsonProperty("resourceId") String resourceId, + /** Agent Finder materialisation revision identifier. */ + @JsonProperty("catalogRevisionId") String catalogRevisionId, + /** GitHub repository database identifier. */ + @JsonProperty("repositoryId") String repositoryId, + /** Repository full name, for example owner/name. */ + @JsonProperty("repository") String repository, + /** Pinned Git commit revision. */ + @JsonProperty("revision") String revision, + /** Root path within the pinned repository. */ + @JsonProperty("root") String root, + /** Digest of the canonical materialisation descriptor. */ + @JsonProperty("descriptorDigest") String descriptorDigest, + /** Digest of the descriptor's bundle manifest. */ + @JsonProperty("bundleDigest") String bundleDigest +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSummary.java new file mode 100644 index 0000000000..9395587bd4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillInstallationSummary.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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Durable verified Skill ownership summary. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillInstallationSummary( + /** Exact durable installation receipt identity. */ + @JsonProperty("installationId") String installationId, + /** Operation that installed this Skill. */ + @JsonProperty("operationId") String operationId, + /** Skill invocation name. */ + @JsonProperty("name") String name, + /** User-facing installation location without an absolute host path. */ + @JsonProperty("target") SkillInstallationLocation target, + /** Persisted enablement requested for this installation. */ + @JsonProperty("configuredEnabled") Boolean configuredEnabled, + /** Bound-session load observation. */ + @JsonProperty("sessionState") SkillInstallationSessionState sessionState, + /** Exact retained verified source identity. */ + @JsonProperty("source") SkillInstallationSource source, + /** Catalogue identity retained at install time. */ + @JsonProperty("catalogue") InstallationCatalogueIdentity catalogue, + /** ISO 8601 wall-clock installation time. */ + @JsonProperty("installedAt") String installedAt, + /** Ownership state observed from files and receipts. */ + @JsonProperty("ownershipState") SkillInstallationOwnershipState ownershipState +) { +} 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..64a287661b 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 @@ -13,14 +13,14 @@ import javax.annotation.processing.Generated; /** - * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is fetched separately and lazily. * * @since 1.0.0 */ @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..056b65f811 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,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 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. + * Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter is optional: fields it omits come from the catalog descriptor, fields it declares must match the descriptor, and `allowed-tools` is read only from frontmatter. Related files and assets are not supported. * - * @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( - /** Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. */ +record SkillProviderReadResult( + /** SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. */ @JsonProperty("markdown") String markdown ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillUninstallPlan.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillUninstallPlan.java new file mode 100644 index 0000000000..36c498f70a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillUninstallPlan.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 javax.annotation.processing.Generated; + +/** + * A computed Skill uninstall plan. Nothing has been removed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillUninstallPlan( + /** One-use uninstall plan handle. */ + @JsonProperty("planHandle") String planHandle, + /** Original removal operation identifier returned before confirmation. */ + @JsonProperty("operationId") String operationId, + /** Original wall-clock expiry as an ISO 8601 timestamp. */ + @JsonProperty("expiresAt") String expiresAt, + /** Owned Skill installation being removed. */ + @JsonProperty("installation") SkillInstallationSummary installation, + /** Safe review to present before applying removal. */ + @JsonProperty("review") SkillInstallationReview review +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsApplyInstallParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsApplyInstallParams.java new file mode 100644 index 0000000000..dab016b5f6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsApplyInstallParams.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; + +/** + * Applies exactly one retained verified Skill installation plan. + * + * @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 SkillsApplyInstallParams( + /** Required authenticated bound catalogue and Skill installation capabilities. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Opaque original plan, consumed once. */ + @JsonProperty("planHandle") String planHandle, + /** Same existing selected session as planning. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeFromToolResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsApplyUninstallParams.java similarity index 66% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeFromToolResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsApplyUninstallParams.java index d4566384c5..f7bc4b26ee 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryResumeFromToolResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsApplyUninstallParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Resolved persisted factory identity and resumed run envelope. + * One-use application of the exact retained Skill removal plan. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,10 +23,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionFactoryResumeFromToolResult( - /** Persisted factory name resolved for the resumed run. */ - @JsonProperty("factoryName") String factoryName, - /** Terminal resumed run envelope. */ - @JsonProperty("run") FactoryRunResult run +public record SkillsApplyUninstallParams( + /** Required authenticated bound installation contract. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Opaque original removal plan, consumed once. */ + @JsonProperty("planHandle") String planHandle, + /** Same existing selected session as removal preparation. */ + @JsonProperty("policySessionId") String policySessionId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsCancelParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsCancelParams.java new file mode 100644 index 0000000000..069f33a25e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsCancelParams.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; + +/** + * Existing-operation control. A new session selector is deliberately not accepted. + * + * @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 SkillsInstallationsCancelParams( + /** Required authenticated bound Skill installation capability. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Exact runtime-issued operation ID on the original connection. */ + @JsonProperty("operationId") String operationId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsListParams.java new file mode 100644 index 0000000000..4b53413b2d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsListParams.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; + +/** + * Inventory request under an explicitly selected existing 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 SkillsInstallationsListParams( + /** Required authenticated bound installation contract. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Existing selected local session on this connection. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsRecoverParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsRecoverParams.java new file mode 100644 index 0000000000..5837b2b374 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsRecoverParams.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; + +/** + * Inventory request under an explicitly selected existing 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 SkillsInstallationsRecoverParams( + /** Required authenticated bound installation contract. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Existing selected local session on this connection. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsSetEnabledParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsSetEnabledParams.java new file mode 100644 index 0000000000..86526724fd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsSetEnabledParams.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; + +/** + * Persisted enablement update for one owned Skill installation. + * + * @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 SkillsInstallationsSetEnabledParams( + /** Required authenticated bound Skill installation capability. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Exact receipt identity to update. */ + @JsonProperty("installationId") String installationId, + /** Persisted enablement value. */ + @JsonProperty("enabled") Boolean enabled, + /** Existing selected local session to reconcile after persistence. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsStatusParams.java new file mode 100644 index 0000000000..a0fbbbec6d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInstallationsStatusParams.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; + +/** + * Existing-operation control. A new session selector is deliberately not accepted. + * + * @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 SkillsInstallationsStatusParams( + /** Required authenticated bound Skill installation capability. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Exact runtime-issued operation ID on the original connection. */ + @JsonProperty("operationId") String operationId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsPlanInstallParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsPlanInstallParams.java new file mode 100644 index 0000000000..c1e2368630 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsPlanInstallParams.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; + +/** + * Side-effect-free planning of one verified Agent Finder Skill candidate. + * + * @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 SkillsPlanInstallParams( + /** Required authenticated bound catalogue and Skill installation capabilities. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Fresh single-use AI skill candidate handle returned by a bound catalogue search. */ + @JsonProperty("candidateHandle") String candidateHandle, + /** Existing local session attached to this connection. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsPlanUninstallParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsPlanUninstallParams.java new file mode 100644 index 0000000000..8b91e5bee3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsPlanUninstallParams.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; + +/** + * Read-only preparation of one owned Skill removal under fresh selected-session authority. + * + * @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 SkillsPlanUninstallParams( + /** Required authenticated bound installation contract. */ + @JsonProperty("contract") CatalogClientContract contract, + /** Exact receipt to inspect. */ + @JsonProperty("installationId") String installationId, + /** Existing selected local session on this connection. */ + @JsonProperty("policySessionId") String policySessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResultSetModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResultSetModel.java index 95e4d8544c..4502ccb34b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResultSetModel.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResultSetModel.java @@ -33,6 +33,10 @@ public final class SlashCommandInvocationResultSetModel extends SlashCommandInvo @JsonProperty("model") private String model; + /** Auto routing profile selected by the command, when the model is Auto. */ + @JsonProperty("autoTier") + private AutoTier autoTier; + /** Settings scope modified by the command. */ @JsonProperty("scope") private String scope; @@ -60,6 +64,9 @@ public final class SlashCommandInvocationResultSetModel extends SlashCommandInvo public String getModel() { return model; } public void setModel(String model) { this.model = model; } + public AutoTier getAutoTier() { return autoTier; } + public void setAutoTier(AutoTier autoTier) { this.autoTier = autoTier; } + public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } 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/UIElicitationResponse.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResponse.java index d611a8b447..5bf7ba8454 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResponse.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResponse.java @@ -27,6 +27,6 @@ public record UIElicitationResponse( /** The form values submitted by the user (present when action is 'accept') */ @JsonProperty("content") Map content, /** MCP response metadata. */ - @JsonProperty("_meta") Map> meta + @JsonProperty("_meta") Map meta ) { } 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/UserSettingsGetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsGetResult.java index c94e90fcc6..ac7b3cf933 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsGetResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsGetResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + * Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. * * @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/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 new file mode 100644 index 0000000000..9258289c85 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/AhpHost.java @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.github.copilot.generated.rpc.HostDisposeParams; +import com.github.copilot.generated.rpc.HostPublishSessionParams; +import com.github.copilot.generated.rpc.HostPublishSessionResult; +import com.github.copilot.generated.rpc.HostStartResult; +import com.github.copilot.generated.rpc.ServerHostApi; +import java.util.concurrent.CompletableFuture; + +/** + * A ready, connection-owned AHP listener. The handle remains bound to its + * original transport across client reconnects. Disposal never deletes + * application sessions. + */ +@CopilotExperimental +public final class AhpHost implements AutoCloseable { + private final HostStartResult info; + private final ServerHostApi rpc; + + AhpHost(HostStartResult info, ServerHostApi rpc) { + this.info = info; + this.rpc = rpc; + } + + /** + * Gets the listener identity. + * + * @return the listener identity + */ + public String getHostId() { + return info.hostId(); + } + + /** + * Gets the bound address. + * + * @return the actual bound WebSocket URL, or {@code null} without a local + * server + */ + public String getUrl() { + return info.url(); + } + + /** + * Gets the secret token. + * + * @return the token, or {@code null} without an authenticated local server + */ + public String getToken() { + return info.token(); + } + + /** + * Gets the legacy process ID. + * + * @return the process ID, or {@code null} for in-process hosting + */ + 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. + * + * @return completion of listener cleanup + */ + public CompletableFuture dispose() { + return rpc.dispose(new HostDisposeParams(info.hostId())); + } + + /** + * Publishes an exact resident session attached to the owning client, without + * invoking a factory or transferring session ownership. + * + * @param sessionId + * resident session identity + * @return the published AHP session identity + */ + public CompletableFuture publishSession(String sessionId) { + return rpc.publishSession(new HostPublishSessionParams(info.hostId(), sessionId)); + } + + /** Disposes the listener and waits for acknowledged cleanup. */ + @Override + public void close() { + dispose().join(); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/AhpHostExit.java b/java/sdk/src/main/java/com/github/copilot/AhpHostExit.java new file mode 100644 index 0000000000..6846442333 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/AhpHostExit.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.github.copilot.generated.rpc.HostExitReason; + +/** + * A connection-owned listener's termination and cleanup outcome. + * + * @param hostId + * listener identity + * @param reason + * termination reason + * @param exitCode + * legacy child process status, absent for in-process hosting + * @param error + * startup or cleanup failure, or {@code null} on acknowledged + * success + */ +@CopilotExperimental +public record AhpHostExit(String hostId, HostExitReason reason, Long exitCode, String error) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/AhpHostManager.java b/java/sdk/src/main/java/com/github/copilot/AhpHostManager.java new file mode 100644 index 0000000000..afd06e8f0e --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/AhpHostManager.java @@ -0,0 +1,398 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.generated.rpc.HostExitReason; +import com.github.copilot.generated.rpc.HostExitedNotification; +import com.github.copilot.generated.rpc.HostStartParams; +import com.github.copilot.generated.rpc.ServerHostApi; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.SessionConfig; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +@AllowCopilotExperimental +final class AhpHostManager { + private static final Logger LOG = Logger.getLogger(AhpHostManager.class.getName()); + // Optional getters are excluded from config serialization, not from incoming + // host settings. + private static final ObjectMapper CONFIG_MAPPER = JsonRpcClient.getObjectMapper().copy() + .setAnnotationIntrospector(new JacksonAnnotationIntrospector() { + @Override + public boolean hasIgnoreMarker(AnnotatedMember member) { + if ((member.getDeclaringClass() == SessionConfig.class + || member.getDeclaringClass() == ResumeSessionConfig.class) + && member.getRawType() == Optional.class) { + return false; + } + return super.hasIgnoreMarker(member); + } + }); + private final Object gate = new Object(); + private final Map hosts = new HashMap<>(); + private final Map sessions; + private final Executor executor; + + private static final class Host { + final String id = UUID.randomUUID().toString(); + final AhpHostOptions options; + final ServerHostApi rpc; + final Map handoffs = new HashMap<>(); + boolean exited; + + Host(ServerHostApi rpc, AhpHostOptions options) { + this.rpc = rpc; + this.options = new AhpHostOptions(options); + } + } + + private static final class Handoff { + final Host host; + final String id; + final String sessionId; + final ObjectNode expected; + final CopilotSession retained; + final CompletableFuture cancellation = new CompletableFuture<>(); + final CompletableFuture> response = new CompletableFuture<>(); + Map configs = new IdentityHashMap<>(); + CopilotSession session; + boolean released; + + Handoff(Host host, String id, String sessionId, ObjectNode expected, CopilotSession retained) { + this.host = host; + this.id = id; + this.sessionId = sessionId; + this.expected = expected; + this.retained = retained; + } + } + + AhpHostManager(Map sessions, Executor executor) { + this.sessions = sessions; + this.executor = executor; + } + + CompletableFuture start(ServerHostApi rpc, AhpHostOptions options) { + var host = new Host(rpc, options); + synchronized (gate) { + hosts.put(host.id, host); + } + var snapshot = host.options; + return rpc + .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); + } + }); + } + + void register(JsonRpcClient rpc) { + rpc.registerMethodHandler("host.materializeSession", (id, params) -> { + CompletableFuture> response; + try { + response = materialize(params); + } catch (Exception error) { + response = CompletableFuture.failedFuture(error); + } + response.whenComplete((result, error) -> { + try { + long requestId = Long.parseLong(id); + if (error == null) { + rpc.sendResponse(requestId, result); + } else { + rpc.sendErrorResponse(requestId, -32603, unwrap(error).getMessage()); + } + } catch (IOException | NumberFormatException errorSending) { + LOG.log(Level.WARNING, "Could not send AHP factory response", errorSending); + } + }); + }); + rpc.registerMethodHandler("host.sessionReleased", (id, params) -> { + try { + Handoff handoff; + synchronized (gate) { + Host host = hosts.get(text(params, "hostId")); + handoff = host == null ? null : host.handoffs.get(text(params, "handoffId")); + } + if (handoff != null) { + release(handoff); + } + } catch (Exception error) { + LOG.log(Level.WARNING, "Invalid AHP session release notification", error); + } + }); + rpc.registerMethodHandler("host.exited", (id, params) -> { + try { + var info = JsonRpcClient.getObjectMapper().treeToValue(params, HostExitedNotification.class); + Host host; + synchronized (gate) { + host = hosts.get(info.hostId()); + } + if (host != null) { + endHost(host, new AhpHostExit(info.hostId(), info.reason(), info.exitCode(), info.error())); + } + } catch (Exception error) { + LOG.log(Level.WARNING, "Invalid AHP host exit notification", error); + } + }); + } + + void capture(CopilotSession session, Object request) { + synchronized (gate) { + for (Host host : hosts.values()) { + for (Handoff handoff : host.handoffs.values()) { + if (handoff.configs != null && handoff.sessionId.equals(session.getSessionId())) { + handoff.configs.put(session, JsonRpcClient.getObjectMapper().valueToTree(request)); + } + } + } + } + } + + void disconnect() { + disconnect(null); + } + + void disconnect(ServerHostApi rpc) { + ArrayList active; + synchronized (gate) { + active = new ArrayList<>(hosts.values().stream().filter(host -> rpc == null || host.rpc == rpc).toList()); + } + for (Host host : active) { + endHost(host, new AhpHostExit(host.id, HostExitReason.OWNERDISCONNECTED, null, + "SDK owner connection closed before listener cleanup could be acknowledged")); + } + } + + private CompletableFuture> materialize(JsonNode params) throws IOException { + String hostId = text(params, "hostId"); + String handoffId = text(params, "handoffId"); + JsonNode config = params.required("config"); + if (!(config instanceof ObjectNode expected)) { + throw new IllegalArgumentException("AHP factory config must be an object"); + } + String sessionId = text(expected, "sessionId"); + boolean resume = params.path("resume").asBoolean(false); + Handoff handoff; + synchronized (gate) { + Host host = hosts.get(hostId); + if (host == null || host.exited) { + throw new IllegalStateException("AHP host is not active"); + } + if (host.handoffs.containsKey(handoffId)) { + throw new IllegalStateException("Duplicate AHP session handoff"); + } + if (resume ? host.options.getResumeSession() == null : host.options.getCreateSession() == null) { + throw new IllegalStateException("No application AHP session factory configured"); + } + handoff = new Handoff(host, handoffId, sessionId, expected.deepCopy(), + resume ? sessions.get(sessionId) : null); + host.handoffs.put(handoffId, handoff); + } + try { + ObjectNode converted = expected.deepCopy(); + rename(converted, "configDir", "configDirectory"); + rename(converted, "suppressResumeEvent", "disableResume"); + Supplier> factory; + if (resume) { + converted.remove("sessionId"); + var settings = CONFIG_MAPPER.treeToValue(converted, ResumeSessionConfig.class); + var request = new AhpSessionResumeRequest(sessionId, settings, + handoff.cancellation.minimalCompletionStage()); + factory = () -> handoff.host.options.getResumeSession().apply(request); + } else { + var settings = CONFIG_MAPPER.treeToValue(converted, SessionConfig.class); + var request = new AhpSessionCreateRequest(settings, handoff.cancellation.minimalCompletionStage()); + factory = () -> handoff.host.options.getCreateSession().apply(request); + } + CompletableFuture.supplyAsync(() -> { + synchronized (gate) { + if (handoff.released) { + throw new CancellationException("AHP session participation ended"); + } + } + return Objects.requireNonNull(factory.get(), "AHP factory returned a null future"); + }, executor).thenCompose(future -> future) + .whenComplete((session, error) -> finish(handoff, session, error)); + } catch (Exception error) { + finish(handoff, null, error); + } + return handoff.response; + } + + private void finish(Handoff handoff, CopilotSession session, Throwable failure) { + Throwable error = failure == null ? null : unwrap(failure); + boolean released; + synchronized (gate) { + released = handoff.released; + if (!released) { + handoff.session = session; + if (error == null) { + try { + validate(handoff, session); + } catch (IllegalArgumentException invalid) { + error = invalid; + } + } + handoff.configs = null; + } + } + if (released) { + if (session != null) { + notifyReleased(handoff, session); + } + if (error != null && !(error instanceof CancellationException)) { + LOG.log(Level.WARNING, "AHP factory failed after participation ended", error); + } + } else if (error != null) { + handoff.response.completeExceptionally(error); + release(handoff); + } else { + handoff.response.complete(Map.of("sessionId", handoff.sessionId)); + } + } + + private void validate(Handoff handoff, CopilotSession session) { + if (session == null || !handoff.sessionId.equals(session.getSessionId()) + || sessions.get(handoff.sessionId) != session) { + throw new IllegalArgumentException( + "AHP factory must return the exact requested session object registered on the owning client"); + } + JsonNode actual = handoff.configs.get(session); + if (actual == null && session == handoff.retained) { + return; + } + if (actual == null) { + throw new IllegalArgumentException("AHP factory must create or resume its session during this handoff"); + } + ObjectNode expected = handoff.expected.deepCopy(); + rename(expected, "suppressResumeEvent", "disableResume"); + rename(expected, "enableExperimentalMode", "isExperimentalMode"); + rename(expected, "enableMcpApps", "requestMcpApps"); + for (var field : expected.properties()) { + // These default-false flags are omitted by the session request builder. + if (!actual.has(field.getKey()) && field.getValue().isBoolean() && !field.getValue().booleanValue() + && (field.getKey().equals("streaming") || field.getKey().equals("requestMcpApps") + || field.getKey().equals("disableResume"))) { + continue; + } + if (!contains(actual.get(field.getKey()), field.getValue())) { + throw new IllegalArgumentException("AHP factory changed host-selected config." + field.getKey()); + } + } + } + + private static boolean contains(JsonNode actual, JsonNode expected) { + if (!expected.isObject()) { + return actual != null && expected.equals((left, right) -> { + if (left.isNumber() && right.isNumber()) { + return left.decimalValue().compareTo(right.decimalValue()); + } + return left.equals(right) ? 0 : 1; + }, actual); + } + if (actual == null || !actual.isObject()) { + return false; + } + for (var field : expected.properties()) { + if (!contains(actual.get(field.getKey()), field.getValue())) { + return false; + } + } + return true; + } + + private void release(Handoff handoff) { + CopilotSession session; + synchronized (gate) { + if (handoff.released) { + return; + } + handoff.released = true; + handoff.host.handoffs.remove(handoff.id, handoff); + handoff.configs = null; + session = handoff.session; + handoff.session = null; + } + handoff.response.completeExceptionally(new CancellationException("AHP session participation ended")); + handoff.cancellation.completeAsync(() -> null); + if (session != null) { + notifyReleased(handoff, session); + } + } + + private void endHost(Host host, AhpHostExit info) { + ArrayList active; + synchronized (gate) { + if (host.exited) { + return; + } + host.exited = true; + hosts.remove(host.id, host); + active = new ArrayList<>(host.handoffs.values()); + } + active.forEach(this::release); + if (info != null && host.options.getOnExit() != null) { + notifyCallback(() -> host.options.getOnExit().apply(info)); + } + } + + private void notifyReleased(Handoff handoff, CopilotSession session) { + if (handoff.host.options.getOnSessionReleased() != null) { + handoff.cancellation + .thenRun(() -> notifyCallback(() -> handoff.host.options.getOnSessionReleased().apply(session))); + } + } + + private static void notifyCallback(Supplier> callback) { + // Late results can arrive after the owning client's executor has shut down. + CompletableFuture + .supplyAsync(() -> Objects.requireNonNull(callback.get(), "AHP callback returned a null future")) + .thenCompose(future -> future).whenComplete((ignored, error) -> { + if (error != null) { + LOG.log(Level.WARNING, "AHP lifecycle callback failed", unwrap(error)); + } + }); + } + + private static String text(JsonNode object, String field) { + JsonNode value = object.required(field); + if (!value.isTextual() || value.textValue().isEmpty()) { + throw new IllegalArgumentException("AHP " + field + " must be a nonempty string"); + } + return value.textValue(); + } + + private static void rename(ObjectNode object, String source, String target) { + JsonNode value = object.remove(source); + if (value != null) { + object.set(target, value); + } + } + + private static Throwable unwrap(Throwable error) { + return error instanceof CompletionException && error.getCause() != null ? error.getCause() : error; + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/AhpHostOptions.java b/java/sdk/src/main/java/com/github/copilot/AhpHostOptions.java new file mode 100644 index 0000000000..2ee29adab0 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/AhpHostOptions.java @@ -0,0 +1,171 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +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 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 HostLocalServerOptions localServer; + private HostGitHubEnvironmentOptions githubEnvironment; + private Function> createSession; + private Function> resumeSession; + private Function> onSessionReleased; + private Function> onExit; + + /** + * Creates options without a transport. Set a local server, a GitHub + * environment, or both before starting the host. + */ + public AhpHostOptions() { + } + + AhpHostOptions(AhpHostOptions options) { + localServer = options.localServer; + githubEnvironment = options.githubEnvironment; + createSession = options.createSession; + resumeSession = options.resumeSession; + onSessionReleased = options.onSessionReleased; + onExit = options.onExit; + } + + /** + * Gets the local WebSocket transport settings. + * + * @return the settings, or {@code null} when local hosting is disabled + */ + public HostLocalServerOptions getLocalServer() { + return localServer; + } + + /** + * Enables or disables the local WebSocket transport. + * + * @param localServer + * local settings, or {@code null} to disable local hosting + * @return these options + */ + public AhpHostOptions setLocalServer(HostLocalServerOptions localServer) { + this.localServer = localServer; + return this; + } + + /** + * Gets the GitHub Mission Control transport settings. + * + * @return the settings, or {@code null} when GitHub hosting is disabled + */ + public HostGitHubEnvironmentOptions getGithubEnvironment() { + return githubEnvironment; + } + + /** + * Enables or disables GitHub Mission Control hosting. + * + * @param githubEnvironment + * settings with a required name and compute ID, or {@code null} + * @return these options + */ + public AhpHostOptions setGithubEnvironment(HostGitHubEnvironmentOptions githubEnvironment) { + this.githubEnvironment = githubEnvironment; + return this; + } + + /** + * Gets the creation callback. + * + * @return the application creation callback, or {@code null} + */ + public Function> getCreateSession() { + return createSession; + } + + /** + * Sets the application creation callback. + * + * @param callback + * application creation callback + * @return these options + */ + public AhpHostOptions setCreateSession( + Function> callback) { + createSession = callback; + return this; + } + + /** + * Gets the resume callback. + * + * @return the application resume callback, or {@code null} + */ + public Function> getResumeSession() { + return resumeSession; + } + + /** + * Sets the application resume callback. + * + * @param callback + * application resume callback + * @return these options + */ + public AhpHostOptions setResumeSession( + Function> callback) { + resumeSession = callback; + return this; + } + + /** + * Gets the release callback. + * + * @return the participation-release callback, or {@code null} + */ + public Function> getOnSessionReleased() { + return onSessionReleased; + } + + /** + * Sets the participation-release callback. + * + * @param callback + * callback receiving the exact factory result once + * @return these options + */ + public AhpHostOptions setOnSessionReleased(Function> callback) { + onSessionReleased = callback; + return this; + } + + /** + * Gets the exit callback. + * + * @return the listener exit callback, or {@code null} + */ + public Function> getOnExit() { + return onExit; + } + + /** + * Sets the listener exit callback. + * + * @param callback + * callback receiving the listener's final outcome once + * @return these options + */ + public AhpHostOptions setOnExit(Function> callback) { + onExit = callback; + return this; + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/AhpSessionCreateRequest.java b/java/sdk/src/main/java/com/github/copilot/AhpSessionCreateRequest.java new file mode 100644 index 0000000000..98c9a247dc --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/AhpSessionCreateRequest.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.github.copilot.rpc.SessionConfig; +import java.util.concurrent.CompletionStage; + +/** + * An application-owned AHP session creation request. Preserve the supplied + * settings when adding application tools, prompts, permissions, and hooks. + * + * @param config + * host-selected session settings + * @param cancellation + * completes normally when the participation ends; a late factory + * result is still delivered to the release callback + */ +@CopilotExperimental +public record AhpSessionCreateRequest(SessionConfig config, CompletionStage cancellation) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/AhpSessionResumeRequest.java b/java/sdk/src/main/java/com/github/copilot/AhpSessionResumeRequest.java new file mode 100644 index 0000000000..cbb8566c7e --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/AhpSessionResumeRequest.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.github.copilot.rpc.ResumeSessionConfig; +import java.util.concurrent.CompletionStage; + +/** + * An application-owned durable AHP session resume request. Reattach application + * callbacks using the supplied settings, or return the exact retained session. + * + * @param sessionId + * durable session identity to resume + * @param config + * host-selected resume settings + * @param cancellation + * completes normally when the participation ends + */ +@CopilotExperimental +public record AhpSessionResumeRequest(String sessionId, ResumeSessionConfig config, + CompletionStage cancellation) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/CliServerManager.java b/java/sdk/src/main/java/com/github/copilot/CliServerManager.java index a7cbf2618f..e742b242e5 100644 --- a/java/sdk/src/main/java/com/github/copilot/CliServerManager.java +++ b/java/sdk/src/main/java/com/github/copilot/CliServerManager.java @@ -21,6 +21,7 @@ import java.util.regex.Pattern; import com.github.copilot.ffi.NativeRuntimeLoader; +import com.github.copilot.rpc.CopilotClientMode; import com.github.copilot.rpc.CopilotClientOptions; /** @@ -274,6 +275,9 @@ void configureProcessEnvironment(ProcessBuilder pb) { pb.environment().putAll(options.getEnvironment()); } pb.environment().remove("NODE_DEBUG"); + if (options.getMode() != CopilotClientMode.EMPTY) { + pb.environment().put("COPILOT_RUNTIME_PROCESS_FILE_LOGGING", "1"); + } // Set auth token in environment if provided if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { 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 6f929b683e..3e971b1e0d 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -25,11 +25,13 @@ 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; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.ExtensionLaunchProvider; +import com.github.copilot.rpc.InstallationConfirmationHandler; import com.github.copilot.rpc.InProcessRuntimeConnection; import com.github.copilot.rpc.RuntimeConnection; import com.github.copilot.rpc.StdioRuntimeConnection; @@ -38,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; @@ -92,11 +93,12 @@ public final class CopilotClient implements AutoCloseable { private static final Logger LOG = Logger.getLogger(CopilotClient.class.getName()); /** - * Timeout, in seconds, used by {@link #close()} when waiting for graceful - * shutdown via {@link #stop()}. + * Timeout, in seconds, allowed by {@link #close()} for session and executor + * cleanup, in addition to the bounded runtime shutdown phases. */ public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10; private static final int RUNTIME_SHUTDOWN_TIMEOUT_SECONDS = 10; + private static final int PROCESS_EXIT_TIMEOUT_SECONDS = 10; private static final int FORCE_KILL_TIMEOUT_SECONDS = 10; /** @@ -119,6 +121,7 @@ public final class CopilotClient implements AutoCloseable { private final CliServerManager serverManager; private final LifecycleEventManager lifecycleManager = new LifecycleEventManager(); private final Map sessions = new ConcurrentHashMap<>(); + private final AhpHostManager ahpHosts; private final GitHubTokenProviderRegistry gitHubTokenProviders = new GitHubTokenProviderRegistry(); private volatile CompletableFuture connectionFuture; private volatile boolean disposed = false; @@ -234,6 +237,7 @@ public CopilotClient(CopilotClientOptions options) { InternalExecutorProvider executorProvider = new InternalExecutorProvider(this.options.getExecutor()); this.executor = executorProvider.get(); + this.ahpHosts = new AhpHostManager(sessions, executor); this.executorCanBeShutdown = executorProvider.canBeShutdown(); this.serverManager = new CliServerManager(this.options); @@ -559,6 +563,7 @@ private Connection startCoreBody() { RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch, executor, gitHubTokenProviders); dispatcher.registerHandlers(connectedRpc); + ahpHosts.register(connectedRpc); // Register the LLM inference request handler when configured. com.github.copilot.CopilotRequestHandler requestHandler = this.options.getRequestHandler(); @@ -570,11 +575,37 @@ private Connection startCoreBody() { llmAdapter.registerHandlers(connectedRpc); } LlmInferenceAdapter connectedLlmAdapter = llmAdapter; + + InstallationConfirmationHandler installationConfirmationHandler = this.options + .getInstallationConfirmationHandler(); + InstallationConfirmationAdapter installationConfirmationAdapter = null; + if (installationConfirmationHandler != null) { + installationConfirmationAdapter = new InstallationConfirmationAdapter(installationConfirmationHandler, + executor); + installationConfirmationAdapter.registerHandlers(connectedRpc); + } + InstallationConfirmationAdapter connectedInstallationConfirmationAdapter = installationConfirmationAdapter; + // JsonRpcClient keeps one handler per method, so this one serves every request + // that honors cancellation. + connectedRpc.registerMethodHandler("$/cancelRequest", (rpcId, params) -> { + dispatcher.handleCancelRequest(params); + if (connectedInstallationConfirmationAdapter != null) { + connectedInstallationConfirmationAdapter.handleCancel(connectedRpc, params); + } + }); connectedRpc.setCloseHandler(() -> { - sessions.values().forEach(CopilotSession::cancelPendingExternalTools); + ahpHosts.disconnect(connection.serverRpc().host); + sessions.values().forEach(session -> { + session.cancelPendingExternalTools(); + session.clearSkillProvider(); + }); + dispatcher.cancelSkillProviderCalls(); if (connectedLlmAdapter != null) { connectedLlmAdapter.cancelPending(); } + if (connectedInstallationConfirmationAdapter != null) { + connectedInstallationConfirmationAdapter.closePending(); + } }); // Register the GitHub telemetry forwarding handler when configured. @@ -621,10 +652,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 { @@ -673,11 +716,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; @@ -721,8 +763,10 @@ private static boolean isUnsupportedConnectMethod(JsonRpcException ex) { *

    *
  1. Closes all active sessions (releases in-memory resources)
  2. *
  3. Requests runtime shutdown for SDK-owned CLI processes
  4. - *
  5. Closes the JSON-RPC connection
  6. - *
  7. Terminates the CLI server process (if spawned by this client)
  8. + *
  9. Closes stdin for an owned stdio process and waits for its host + * cleanup
  10. + *
  11. Closes the JSON-RPC connection, terminating an owned process if + * needed
  12. *
*

* Note: session data on disk is preserved, so sessions can be resumed later. To @@ -732,6 +776,7 @@ private static boolean isUnsupportedConnectMethod(JsonRpcException ex) { * @return A future that completes when the client is stopped */ public CompletableFuture stop() { + ahpHosts.disconnect(); var closeFutures = new ArrayList>(); for (CopilotSession session : new ArrayList<>(sessions.values())) { @@ -765,10 +810,12 @@ public CompletableFuture stop() { * @return A future that completes when the client is stopped */ public CompletableFuture forceStop() { + ahpHosts.disconnect(); disposed = true; var activeSessions = new ArrayList<>(sessions.values()); sessions.clear(); activeSessions.forEach(CopilotSession::cancelPendingExternalTools); + activeSessions.forEach(CopilotSession::clearSkillProvider); gitHubTokenProviders.clear(); // Dispatch the blocking shutdownOwnedExecutor() on a dedicated thread: // cleanupConnection() is chained off async work running on the owned @@ -814,7 +861,10 @@ private CompletableFuture cleanupConnection(boolean gracefulRuntimeShutdow }); } - return shutdownFuture.handle((ignored, error) -> { + return shutdownFuture.handleAsync((ignored, error) -> { + if (gracefulRuntimeShutdown && connection.process != null && options.isUseStdio()) { + awaitStdioProcessExit(connection.process); + } try { connection.rpc.close(); } catch (Exception e) { @@ -828,10 +878,26 @@ private CompletableFuture cleanupConnection(boolean gracefulRuntimeShutdow closeRuntimeHost(connection.runtimeHost); } return (Void) null; - }); + }, SHUTDOWN_DISPATCHER); }).thenCompose(result -> result); } + private static void awaitStdioProcessExit(Process process) { + try { + // Host telemetry flushes after stdio EOF, not the shutdown RPC response. + // Keep the reader draining stdout until the child has finished. + process.getOutputStream().close(); + if (!process.waitFor(PROCESS_EXIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + LOG.fine("Process did not exit after stdin EOF within graceful shutdown timeout; terminating"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.log(Level.FINE, "Interrupted while waiting for process exit", e); + } catch (IOException e) { + LOG.log(Level.FINE, "Error closing process stdin", e); + } + } + /** * Returns true only when the child had already exited and no streams were * destroyed. @@ -839,12 +905,6 @@ private CompletableFuture cleanupConnection(boolean gracefulRuntimeShutdow private static boolean cleanupCliProcess(Process process, boolean forceImmediately) { try { if (process.isAlive()) { - // The runtime completes all cleanup before responding to - // runtime.shutdown and then leaves termination to us; it - // deliberately keeps its JSON-RPC server alive to send the - // response and never self-exits. Waiting for a self-exit that - // will never come just wastes time, so terminate the child - // immediately and only wait to reap it. if (forceImmediately) { process.destroyForcibly(); if (!process.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { @@ -914,6 +974,10 @@ public CompletableFuture createSession(SessionConfig config) { return CompletableFuture.failedFuture( new IllegalArgumentException("gitHubToken and gitHubTokenProvider are mutually exclusive")); } + if (config.getCloud() != null && config.getSkillProvider() != null) { + return CompletableFuture.failedFuture( + new IllegalArgumentException("Skill providers are not supported for cloud sessions.")); + } return ensureConnected().thenCompose(connection -> { long totalNanos = System.nanoTime(); // For cloud sessions, let the CLI/server assign the session id @@ -982,7 +1046,7 @@ public CompletableFuture createSession(SessionConfig config) { if (options.getMode() == CopilotClientMode.EMPTY) { if (config.getAvailableTools() == null) { if (registeredIdHolder[0] != null) { - sessions.remove(registeredIdHolder[0]); + sessions.remove(registeredIdHolder[0], preRegisteredSessionHolder[0]); } throw new IllegalArgumentException( "CopilotClient is in Mode = EMPTY but the session config did not specify " @@ -1076,14 +1140,16 @@ public CompletableFuture createSession(SessionConfig config) { "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" + session.getSessionId(), totalNanos); + ahpHosts.capture(session, request); return session; }); }).exceptionally(ex -> { if (preRegisteredSessionHolder[0] != null) { preRegisteredSessionHolder[0].cancelPendingExternalTools(); + preRegisteredSessionHolder[0].clearSkillProvider(); } if (registeredIdHolder[0] != null) { - sessions.remove(registeredIdHolder[0]); + sessions.remove(registeredIdHolder[0], preRegisteredSessionHolder[0]); } if (tokenRegistration != null) { tokenRegistration.close(); @@ -1141,6 +1207,14 @@ public CompletableFuture resumeSession(String sessionId, ResumeS } return ensureConnected().thenCompose(connection -> { long totalNanos = System.nanoTime(); + // Validate before registering so a rejected resume leaves any resident + // session and its skill provider in place. + if (options.getMode() == CopilotClientMode.EMPTY && config.getAvailableTools() == null) { + throw new IllegalArgumentException( + "CopilotClient is in Mode = EMPTY but the resume session config did not specify " + + "availableTools. Empty mode requires every session to explicitly opt into " + + "the tools it wants — e.g. setAvailableTools(new ToolSet().addBuiltIn(BuiltInTools.ISOLATED))."); + } // Register the session before the RPC call to avoid missing early events. long setupNanos = System.nanoTime(); CopilotSession replacedSession = sessions.get(sessionId); @@ -1169,15 +1243,8 @@ public CompletableFuture resumeSession(String sessionId, ResumeS request.setEnableGitHubTelemetryForwarding(true); } - // Empty mode: validate availableTools and set toolFilterPrecedence for resume - // path + // Empty mode: set toolFilterPrecedence and isolation defaults for resume path if (options.getMode() == CopilotClientMode.EMPTY) { - if (config.getAvailableTools() == null) { - throw new IllegalArgumentException( - "CopilotClient is in Mode = EMPTY but the resume session config did not specify " - + "availableTools. Empty mode requires every session to explicitly opt into " - + "the tools it wants — e.g. setAvailableTools(new ToolSet().addBuiltIn(BuiltInTools.ISOLATED))."); - } request.setToolFilterPrecedence("excluded"); if (request.getSkipEmbeddingRetrieval() == null) { request.setSkipEmbeddingRetrieval(true); @@ -1237,11 +1304,12 @@ 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(); if (returnedId != null && !returnedId.equals(sessionId)) { - sessions.remove(sessionId); + sessions.remove(sessionId, session); session.setActiveSessionId(returnedId); sessions.put(returnedId, session); } @@ -1259,19 +1327,16 @@ public CompletableFuture resumeSession(String sessionId, ResumeS } else { gitHubTokenProviders.retire(session.getSessionId()); } + ahpHosts.capture(session, request); return session; }); }).exceptionally(ex -> { session.cancelPendingExternalTools(); - sessions.remove(sessionId); - // Also remove the re-keyed entry if the server returned a different ID - String activeId = session.getSessionId(); - if (!sessionId.equals(activeId)) { - sessions.remove(activeId); - } - if (replacedSession != null) { - sessions.putIfAbsent(sessionId, replacedSession); - } + session.clearSkillProvider(); + // A concurrent resume may own the ID now; undo only this attempt. + sessions.compute(sessionId, + (id, current) -> current == null || current == session ? replacedSession : current); + sessions.remove(session.getSessionId(), session); if (tokenRegistration != null) { tokenRegistration.close(); } @@ -1617,6 +1682,7 @@ public CompletableFuture deleteSession(String sessionId) { } CopilotSession session = sessions.remove(sessionId); if (session != null) { + session.clearSkillProvider(); session.releaseGitHubTokenProviderRegistration(); } })); @@ -1766,6 +1832,41 @@ public AutoCloseable onLifecycle(String eventType, SessionLifecycleHandler handl return lifecycleManager.subscribe(eventType, handler); } + /** + * 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. + * + * @param options + * listener options and optional application session factories + * @return a ready listener bound to its original transport + */ + @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, snapshot)).whenComplete((host, error) -> { + if (error != null) { + result.completeExceptionally(error); + } else if (!result.complete(host)) { + host.dispose().whenComplete((ignored, cleanupError) -> { + if (cleanupError != null) { + LOG.log(Level.WARNING, "AHP cancelled startup cleanup failed", cleanupError); + } + }); + } + }); + return result; + } + private CompletableFuture ensureConnected() { if (connectionFuture == null && !options.isAutoStart()) { throw new IllegalStateException("Client not connected. Call start() first."); @@ -1779,9 +1880,11 @@ private CompletableFuture ensureConnected() { * Closes this client using graceful shutdown semantics. *

* This method is intended for {@code try-with-resources} usage and blocks while - * waiting for {@link #stop()} to complete, up to - * {@link #AUTOCLOSEABLE_TIMEOUT_SECONDS} seconds. If shutdown fails or times - * out, the error is logged at {@link Level#FINE} and the method returns. + * waiting for {@link #stop()} to complete. The timeout includes the bounded + * runtime shutdown, natural exit, termination and kill phases, plus + * {@link #AUTOCLOSEABLE_TIMEOUT_SECONDS} for session cleanup. If shutdown fails + * or times out, the error is logged at {@link Level#FINE} and the method + * returns. *

* This method is idempotent. * @@ -1795,7 +1898,8 @@ public void close() { return; disposed = true; try { - stop().get(AUTOCLOSEABLE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + stop().get(AUTOCLOSEABLE_TIMEOUT_SECONDS + RUNTIME_SHUTDOWN_TIMEOUT_SECONDS + PROCESS_EXIT_TIMEOUT_SECONDS + + 2 * FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { LOG.log(Level.FINE, "Error during close", e); } finally { 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..db5d49395d 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<>(); @@ -203,6 +211,7 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); private final AtomicReference autoModeSwitchHandler = new AtomicReference<>(); private final AtomicReference hooksHandler = new AtomicReference<>(); + private final AtomicReference skillProvider = new AtomicReference<>(); private volatile EventErrorHandler eventErrorHandler; private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; private volatile Map>> transformCallbacks; @@ -361,6 +370,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 +760,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 +995,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 +1506,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 +1751,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); } /** @@ -1687,6 +1775,18 @@ void registerMcpAuthHandler(McpAuthHandler handler) { mcpAuthHandler.set(handler); } + void registerSkillProvider(SkillProvider provider) { + skillProvider.set(provider); + } + + SkillProvider getSkillProvider() { + return skillProvider.get(); + } + + void clearSkillProvider() { + skillProvider.set(null); + } + /** * Handles a permission request from the Copilot CLI. *

@@ -2208,6 +2308,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); } @@ -2709,6 +2829,7 @@ public void close() { structuredWaits.forEach(wait -> wait .completeExceptionally(new IllegalStateException("Session closed before structured output completed"))); cancelPendingExternalTools(); + clearSkillProvider(); timeoutScheduler.shutdownNow(); releaseGitHubTokenProviderRegistration(); @@ -2729,7 +2850,7 @@ public void close() { } eventHandlers.clear(); - toolHandlers.clear(); + toolHandlers.set(Map.of()); commandHandlers.clear(); permissionHandler.set(null); userInputHandler.set(null); @@ -2737,6 +2858,7 @@ public void close() { exitPlanModeHandler.set(null); autoModeSwitchHandler.set(null); hooksHandler.set(null); + skillProvider.set(null); if (detachFailure != null) { throw detachFailure; diff --git a/java/sdk/src/main/java/com/github/copilot/InstallationConfirmationAdapter.java b/java/sdk/src/main/java/com/github/copilot/InstallationConfirmationAdapter.java new file mode 100644 index 0000000000..99eddbe545 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/InstallationConfirmationAdapter.java @@ -0,0 +1,246 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.rpc.InstallationConfirmationRequest; +import com.github.copilot.generated.rpc.InstallationDecision; +import com.github.copilot.generated.rpc.InstallationsConfirmResult; +import com.github.copilot.rpc.InstallationConfirmationContext; +import com.github.copilot.rpc.InstallationConfirmationHandler; + +/** + * Bridges {@code installations.confirm} reverse RPC calls to the configured + * installation confirmation handler. + */ +final class InstallationConfirmationAdapter { + + private static final Logger LOG = Logger.getLogger(InstallationConfirmationAdapter.class.getName()); + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + private static final int INVALID_PARAMS = -32602; + private static final int INTERNAL_ERROR = -32603; + private static final int REQUEST_CANCELLED = -32800; + + private final InstallationConfirmationHandler handler; + private final Executor executor; + private final CompletableFuture connectionClosed = new CompletableFuture<>(); + private final Map pending = new ConcurrentHashMap<>(); + + InstallationConfirmationAdapter(InstallationConfirmationHandler handler, Executor executor) { + this.handler = handler; + this.executor = executor; + } + + void registerHandlers(JsonRpcClient rpc) { + rpc.registerMethodHandler("installations.confirm", (rpcId, params) -> handleConfirm(rpc, rpcId, params)); + } + + void closePending() { + connectionClosed.complete(null); + pending.values().forEach(PendingConfirmation::closeConnection); + pending.clear(); + } + + private void handleConfirm(JsonRpcClient rpc, String rpcId, JsonNode params) { + if (rpcId == null) { + return; + } + + Object responseId = parseRpcId(rpcId); + try { + InstallationConfirmationRequest request = MAPPER.treeToValue(params, InstallationConfirmationRequest.class); + if (!isValidRequest(request)) { + sendError(rpc, responseId, INVALID_PARAMS, "Invalid installation confirmation review"); + return; + } + Long numericId = parseNumericRequestId(rpcId); + var pendingConfirmation = new PendingConfirmation(numericId, responseId, request); + if (numericId != null && pending.putIfAbsent(numericId, pendingConfirmation) != null) { + sendError(rpc, responseId, INTERNAL_ERROR, "Duplicate installation confirmation request id"); + return; + } + runAsync(() -> runHandler(rpc, pendingConfirmation)); + } catch (Exception error) { + sendError(rpc, responseId, INVALID_PARAMS, "Invalid installation confirmation review"); + } + } + + /** + * Cancels the confirmation named by a {@code $/cancelRequest} notification. + */ + void handleCancel(JsonRpcClient rpc, JsonNode params) { + if (params == null || !params.has("id") || !params.get("id").canConvertToLong() + || !params.get("id").isIntegralNumber()) { + return; + } + + PendingConfirmation confirmation = pending.get(params.get("id").asLong()); + if (confirmation == null || !confirmation.cancel()) { + return; + } + + sendError(rpc, confirmation.responseId, REQUEST_CANCELLED, "Installation confirmation request cancelled"); + } + + private void runHandler(JsonRpcClient rpc, PendingConfirmation confirmation) { + if (confirmation.retired() || connectionClosed.isDone()) { + return; + } + + CompletableFuture decisionFuture; + try { + decisionFuture = handler.confirm(confirmation.request, confirmation.context); + } catch (Exception error) { + completeError(rpc, confirmation, "Installation confirmation handler failed: " + message(error)); + return; + } + if (decisionFuture == null) { + completeError(rpc, confirmation, "Installation confirmation handler returned a null future"); + return; + } + + decisionFuture.whenComplete((decision, error) -> { + if (confirmation.retired() || connectionClosed.isDone()) { + return; + } + if (error != null) { + Throwable cause = error instanceof CompletionException && error.getCause() != null + ? error.getCause() + : error; + completeError(rpc, confirmation, "Installation confirmation handler failed: " + message(cause)); + return; + } + if (decision == null) { + completeError(rpc, confirmation, "Invalid installation confirmation decision"); + return; + } + completeResult(rpc, confirmation, decision); + }); + } + + private void completeResult(JsonRpcClient rpc, PendingConfirmation confirmation, InstallationDecision decision) { + var result = new InstallationsConfirmResult(confirmation.request.confirmationId(), + confirmation.request.reviewFingerprint(), decision); + if (!confirmation.retire()) { + return; + } + try { + rpc.sendResponse(confirmation.responseId, result); + } catch (IOException error) { + LOG.log(Level.FINE, "Failed to send installation confirmation response", error); + } + } + + private void completeError(JsonRpcClient rpc, PendingConfirmation confirmation, String message) { + if (!confirmation.retire()) { + return; + } + sendError(rpc, confirmation.responseId, INTERNAL_ERROR, message); + } + + private void sendError(JsonRpcClient rpc, Object responseId, int code, String message) { + try { + rpc.sendErrorResponse(responseId, code, message); + } catch (IOException error) { + LOG.log(Level.FINE, "Failed to send installation confirmation error", error); + } + } + + private void runAsync(Runnable task) { + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException error) { + LOG.log(Level.WARNING, "Executor rejected installation confirmation task; running inline", error); + task.run(); + } + } + + private static Object parseRpcId(String rpcId) { + try { + return Long.valueOf(rpcId); + } catch (NumberFormatException ignored) { + return rpcId; + } + } + + private static Long parseNumericRequestId(String rpcId) { + try { + return Long.valueOf(rpcId); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static String message(Throwable error) { + return error.getMessage() != null ? error.getMessage() : error.toString(); + } + + private static boolean isValidRequest(InstallationConfirmationRequest request) { + return request != null && request.confirmationId() != null && request.operationId() != null + && request.expiresAt() != null && request.reviewFingerprint() != null && request.review() != null; + } + + private final class PendingConfirmation { + + private final Long numericId; + private final Object responseId; + private final InstallationConfirmationRequest request; + private final CompletableFuture cancelled = new CompletableFuture<>(); + private final InstallationConfirmationContext context = new InstallationConfirmationContext(cancelled); + private final AtomicBoolean retired = new AtomicBoolean(); + + PendingConfirmation(Long numericId, Object responseId, InstallationConfirmationRequest request) { + this.numericId = numericId; + this.responseId = responseId; + this.request = request; + } + + boolean cancel() { + if (!retire()) { + return false; + } + cancelled.complete(null); + return true; + } + + boolean closeConnection() { + if (!retire()) { + return false; + } + cancelled.complete(null); + return true; + } + + boolean retire() { + if (!retired.compareAndSet(false, true)) { + return false; + } + if (numericId != null) { + pending.remove(numericId, this); + } + return true; + } + + boolean retired() { + return retired.get(); + } + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java b/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java index fc10946829..e1d8650087 100644 --- a/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java +++ b/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java @@ -213,12 +213,20 @@ public void sendResponse(Object id, Object result) throws IOException { * Sends a JSON-RPC error response to a server request. */ public void sendErrorResponse(Object id, int code, String message) throws IOException { + sendErrorResponse(id, code, message, null); + } + + /** + * Sends a JSON-RPC error response to a server request with optional data. + */ + public void sendErrorResponse(Object id, int code, String message, Object data) throws IOException { var response = new JsonRpcResponse(); response.setJsonrpc("2.0"); response.setId(id); var error = new JsonRpcError(); error.setCode(code); error.setMessage(message); + error.setData(data); response.setError(error); sendMessage(response); } diff --git a/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java b/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java index 0dd8a5dc98..888ba538da 100644 --- a/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java +++ b/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java @@ -8,9 +8,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Map; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,6 +53,7 @@ *
  • Lifecycle events
  • * */ +@AllowCopilotExperimental final class RpcHandlerDispatcher { private static final Logger LOG = Logger.getLogger(RpcHandlerDispatcher.class.getName()); @@ -57,6 +63,7 @@ final class RpcHandlerDispatcher { private final LifecycleEventDispatcher lifecycleDispatcher; private final Executor executor; private final GitHubTokenProviderRegistry gitHubTokenProviders; + private final Map> skillProviderCancellations = new ConcurrentHashMap<>(); /** * Creates a dispatcher with session registry and lifecycle dispatcher. @@ -101,6 +108,144 @@ void registerHandlers(JsonRpcClient rpc) { (requestId, params) -> handleProviderTokenGetToken(rpc, requestId, params)); rpc.registerMethodHandler("gitHubToken.getToken", (requestId, params) -> handleGitHubTokenGetToken(rpc, requestId, params)); + rpc.registerMethodHandler("skillProvider.list", + (requestId, params) -> handleSkillProviderList(rpc, requestId, params)); + rpc.registerMethodHandler("skillProvider.read", + (requestId, params) -> handleSkillProviderRead(rpc, requestId, params)); + } + + // The generated skillProvider.* records are package-private because the + // methods are internal, so these handlers read params as JSON. + private void handleSkillProviderList(JsonRpcClient rpc, String requestId, JsonNode params) { + handleSkillProviderRequest(rpc, requestId, params, "skillProvider.list", "listSkills", + (provider, request) -> provider.listSkills(), + skills -> Map.of("skills", skills != null ? skills : Collections.emptyList())); + } + + private void handleSkillProviderRead(JsonRpcClient rpc, String requestId, JsonNode params) { + handleSkillProviderRequest(rpc, requestId, params, "skillProvider.read", "readSkill", + (provider, request) -> provider.readSkill(textParam(request, "name")), markdown -> { + // Write an explicit null: the runtime requires the field and reads null as "not + // found". + var result = MAPPER.createObjectNode(); + if (markdown == null) { + result.putNull("markdown"); + } else { + result.put("markdown", markdown); + } + return result; + }); + } + + private static String textParam(JsonNode params, String field) { + JsonNode value = params != null ? params.get(field) : null; + return value != null && !value.isNull() ? value.asText() : null; + } + + private void handleSkillProviderRequest(JsonRpcClient rpc, String requestId, JsonNode params, String method, + String operation, BiFunction> call, + Function toResult) { + final long requestIdLong = parseRequestId(requestId, method); + if (requestIdLong == -1) { + return; + } + // Registered on the reader thread so a later $/cancelRequest always finds it. + var cancellation = new CompletableFuture(); + skillProviderCancellations.put(requestIdLong, cancellation); + runAsync(() -> { + String failure = "Skill provider " + operation + " failed"; + String sessionId = null; + try { + sessionId = textParam(params, "sessionId"); + SkillProvider provider = findSkillProvider(sessionId); + if (provider == null) { + skillProviderCancellations.remove(requestIdLong, cancellation); + sendSkillProviderError(rpc, requestIdLong, "No skill provider for session: " + sessionId, sessionId, + null); + return; + } + CompletableFuture future = cancellation.isDone() + ? CompletableFuture.failedFuture(new CancellationException()) + : call.apply(provider, params); + if (future == null) { + future = CompletableFuture + .failedFuture(new IllegalStateException(operation + " returned a null future")); + } + final CompletableFuture providerFuture = future; + final String callSessionId = sessionId; + cancellation.thenRun(() -> providerFuture.cancel(true)); + providerFuture.whenComplete((value, error) -> { + skillProviderCancellations.remove(requestIdLong, cancellation); + if (cancellation.isDone()) { + sendSkillProviderCancelled(rpc, requestIdLong); + } else if (error != null) { + sendSkillProviderError(rpc, requestIdLong, failure, callSessionId, error); + } else { + sendSkillProviderResult(rpc, requestIdLong, method, failure, callSessionId, + () -> toResult.apply(value)); + } + }); + } catch (Exception error) { + skillProviderCancellations.remove(requestIdLong, cancellation); + sendSkillProviderError(rpc, requestIdLong, failure, sessionId, error); + } + }); + } + + /** + * Cancels the skill provider call named by a {@code $/cancelRequest} + * notification, if it is still in flight. + */ + void handleCancelRequest(JsonNode params) { + JsonNode id = params != null ? params.get("id") : null; + if (id == null || !id.isIntegralNumber() || !id.canConvertToLong()) { + return; + } + CompletableFuture cancellation = skillProviderCancellations.get(id.asLong()); + if (cancellation != null) { + cancellation.complete(null); + } + } + + /** Cancels every in-flight skill provider call when the connection closes. */ + void cancelSkillProviderCalls() { + skillProviderCancellations.values().forEach(cancellation -> cancellation.complete(null)); + } + + private SkillProvider findSkillProvider(String sessionId) { + CopilotSession session = sessionId != null ? sessions.get(sessionId) : null; + return session != null ? session.getSkillProvider() : null; + } + + private void sendSkillProviderResult(JsonRpcClient rpc, long requestId, String method, String failure, + String sessionId, Supplier result) { + try { + rpc.sendResponse(requestId, result.get()); + } catch (IOException error) { + LOG.log(Level.SEVERE, "Error sending " + method + " response", error); + } catch (RuntimeException error) { + sendSkillProviderError(rpc, requestId, failure, sessionId, error); + } + } + + private void sendSkillProviderCancelled(JsonRpcClient rpc, long requestId) { + try { + rpc.sendErrorResponse(requestId, -32800, "Request cancelled"); + } catch (IOException error) { + LOG.log(Level.FINE, "Error sending skill provider cancellation", error); + } + } + + private void sendSkillProviderError(JsonRpcClient rpc, long requestId, String message, String sessionId, + Throwable error) { + if (error != null) { + LOG.log(Level.WARNING, message + " (sessionId=" + sessionId + ")", error); + } + try { + rpc.sendErrorResponse(requestId, -32603, message); + } catch (IOException sendError) { + LOG.log(Level.SEVERE, "Error sending skill provider error", sendError); + } } private void handleGitHubTokenGetToken(JsonRpcClient rpc, String requestId, JsonNode params) { 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 04e3a48792..5ba90631e1 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. * @@ -177,6 +215,9 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess config.getEnableHostGitOperations().ifPresent(request::setEnableHostGitOperations); config.getEnableSessionStore().ifPresent(request::setEnableSessionStore); config.getEnableSkills().ifPresent(request::setEnableSkills); + if (config.getSkillProvider() != null) { + request.setHasSkillProvider(true); + } if (config.getEmbeddingCacheStorage() != null) { request.setEmbeddingCacheStorage(config.getEmbeddingCacheStorage()); } @@ -256,6 +297,8 @@ 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()); @@ -295,6 +338,9 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo config.getEnableHostGitOperations().ifPresent(request::setEnableHostGitOperations); config.getEnableSessionStore().ifPresent(request::setEnableSessionStore); config.getEnableSkills().ifPresent(request::setEnableSkills); + if (config.getSkillProvider() != null) { + request.setHasSkillProvider(true); + } if (config.getEmbeddingCacheStorage() != null) { request.setEmbeddingCacheStorage(config.getEmbeddingCacheStorage()); } @@ -395,6 +441,9 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } + if (config.getSkillProvider() != null) { + session.registerSkillProvider(config.getSkillProvider()); + } if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } @@ -447,6 +496,9 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } + if (config.getSkillProvider() != null) { + session.registerSkillProvider(config.getSkillProvider()); + } if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } diff --git a/java/sdk/src/main/java/com/github/copilot/SkillProvider.java b/java/sdk/src/main/java/com/github/copilot/SkillProvider.java new file mode 100644 index 0000000000..d2907113f2 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/SkillProvider.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Provides session-scoped skills to the Copilot runtime. + *

    + * A skill provider is registered through {@code SessionConfig} or + * {@code ResumeSessionConfig}. The runtime may invoke provider callbacks + * concurrently, so implementations should be thread-safe and avoid relying on + * callback ordering. + *

    + * When the runtime cancels a call, for example after its time limit or when the + * session disconnects, the SDK cancels the returned future with + * {@link CompletableFuture#cancel(boolean) cancel(true)} and the runtime + * ignores any later result. Cancellation does not interrupt running work; + * observe it with {@link CompletableFuture#isCancelled()} or a completion + * callback to stop early. + * + * @apiNote This API is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +public interface SkillProvider { + + /** + * Lists the skills currently available for the session. + * + * @return a future resolving to the provider's skill descriptors + */ + CompletableFuture> listSkills(); + + /** + * Reads the Markdown content for one skill. + * + * @param name + * the skill invocation name + * @return a future resolving to the skill Markdown, or {@code null} when the + * skill is not found + */ + CompletableFuture readSkill(String name); +} diff --git a/java/sdk/src/main/java/com/github/copilot/SkillProviderDescriptor.java b/java/sdk/src/main/java/com/github/copilot/SkillProviderDescriptor.java new file mode 100644 index 0000000000..04ac45845d --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/SkillProviderDescriptor.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Catalog metadata for one skill supplied by a {@link SkillProvider}. + *

    + * The runtime lists these descriptors without fetching skill content, then + * calls {@link SkillProvider#readSkill(String)} when the skill is used. + * + * @param name + * the invocation and display name + * @param description + * the description shown in skill catalogs + * @param userInvocable + * whether users may invoke the skill directly, or {@code null} for + * the default ({@code true}) + * @param disableModelInvocation + * whether model invocation is disabled, or {@code null} for the + * default ({@code false}) + * @param argumentHint + * an optional freeform argument hint for slash-command catalogs + * @apiNote This API is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SkillProviderDescriptor(@JsonProperty("name") String name, + @JsonProperty("description") String description, @JsonProperty("userInvocable") Boolean userInvocable, + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + @JsonProperty("argumentHint") String argumentHint) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java index fa00ea1820..fbbcb56ff7 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java @@ -64,6 +64,7 @@ public class CopilotClientOptions { private String logLevel = "info"; private CopilotClientMode mode = CopilotClientMode.COPILOT_CLI; private ExtensionLaunchProvider extensionLaunchProvider; + private InstallationConfirmationHandler installationConfirmationHandler; private Supplier>> onListModels; private CopilotRequestHandler requestHandler; private Function> onGitHubTelemetry; @@ -187,6 +188,36 @@ public CopilotClientOptions setExtensionLaunchProvider(ExtensionLaunchProvider e return this; } + /** + * Gets the connection-level installation confirmation handler. + * + * @return the handler, or {@code null} if not set + */ + @JsonIgnore + @CopilotExperimental + public InstallationConfirmationHandler getInstallationConfirmationHandler() { + return installationConfirmationHandler; + } + + /** + * Sets the connection-level installation confirmation handler. + *

    + * When provided, the client installs the {@code installations.confirm} + * reverse-RPC handler. This does not enable installation capabilities or call a + * runtime registration RPC. + * + * @param installationConfirmationHandler + * the handler (must not be {@code null}) + * @return this options instance for method chaining + */ + @CopilotExperimental + public CopilotClientOptions setInstallationConfirmationHandler( + InstallationConfirmationHandler installationConfirmationHandler) { + this.installationConfirmationHandler = Objects.requireNonNull(installationConfirmationHandler, + "installationConfirmationHandler must not be null"); + return this; + } + /** * Gets the extra CLI arguments. *

    @@ -896,6 +927,7 @@ public CopilotClientOptions clone() { copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; copy.executor = this.executor; copy.extensionLaunchProvider = this.extensionLaunchProvider; + copy.installationConfirmationHandler = this.installationConfirmationHandler; copy.gitHubToken = this.gitHubToken; copy.logLevel = this.logLevel; copy.onListModels = this.onListModels; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index d8ab62af95..50672b3724 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -205,6 +205,10 @@ public final class CreateSessionRequest { @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableSkills; + @JsonProperty("hasSkillProvider") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean hasSkillProvider; + @JsonProperty("embeddingCacheStorage") @JsonInclude(JsonInclude.Include.NON_NULL) private String embeddingCacheStorage; @@ -956,6 +960,23 @@ public void clearEnableSkills() { this.enableSkills = null; } + /** Gets skill-provider availability flag. @return the flag */ + public Boolean getHasSkillProvider() { + return hasSkillProvider; + } + + /** Sets skill-provider availability flag. @param hasSkillProvider the flag */ + public void setHasSkillProvider(boolean hasSkillProvider) { + this.hasSkillProvider = hasSkillProvider; + } + + /** + * Clears the hasSkillProvider setting, reverting to the default behavior. + */ + public void clearHasSkillProvider() { + this.hasSkillProvider = null; + } + /** Gets embedding cache storage mode. @return the mode */ public String getEmbeddingCacheStorage() { return embeddingCacheStorage; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/InstallationConfirmationContext.java b/java/sdk/src/main/java/com/github/copilot/rpc/InstallationConfirmationContext.java new file mode 100644 index 0000000000..2887422d1b --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/InstallationConfirmationContext.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import com.github.copilot.CopilotExperimental; + +/** + * Cancellation signal for one installation confirmation request. + *

    + * This signal retires the human review. It does not cancel outbound + * installation or OAuth RPCs. + * + * @apiNote This type is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +public final class InstallationConfirmationContext { + + private final CompletableFuture cancelled; + + /** + * Creates an installation confirmation context. + * + * @param cancelled + * completed when the runtime cancels this review or the original + * connection closes + */ + public InstallationConfirmationContext(CompletableFuture cancelled) { + this.cancelled = Objects.requireNonNull(cancelled, "cancelled must not be null"); + } + + /** + * Gets the cancellation signal for this review. + *

    + * This future completes when the runtime retires this review request, including + * through JSON-RPC {@code $/cancelRequest}, runtime-enforced expiry, or loss of + * the original connection. + *

    + * Completing the returned future does not affect SDK state. + * + * @return the cancellation signal + */ + public CompletableFuture getCancelled() { + return cancelled.copy(); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/InstallationConfirmationHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/InstallationConfirmationHandler.java new file mode 100644 index 0000000000..72175a17b2 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/InstallationConfirmationHandler.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.concurrent.CompletableFuture; + +import com.github.copilot.CopilotExperimental; +import com.github.copilot.generated.rpc.InstallationConfirmationRequest; +import com.github.copilot.generated.rpc.InstallationDecision; + +/** + * Handles connection-global installation confirmation requests from the + * runtime. + *

    + * Match the request's operation and policy session against the exact original + * action on this connection before presenting the review. Refuse unknown + * actions or incomplete reviews. The SDK echoes the original challenge and + * review fingerprint, so the handler returns only an explicit decision. + * + * @apiNote This type is experimental and may change in a future version. + * @since 1.0.0 + */ +@FunctionalInterface +@CopilotExperimental +public interface InstallationConfirmationHandler { + + /** + * Handles one installation confirmation review. + * + * @param request + * the generated request from the runtime + * @param context + * cancellation signal for this review + * @return a future that resolves with the user's explicit decision + */ + CompletableFuture confirm(InstallationConfirmationRequest request, + InstallationConfirmationContext context); +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java b/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java index 6755d959b3..05bdab83fe 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java @@ -25,6 +25,9 @@ public final class ManagedSettingsPermissions { @JsonProperty("allow") private List allow; + @JsonProperty("limitTo") + private List limitTo; + /** @return the bypass-permissions policy, or {@code null} when unset */ public String getDisableBypassPermissionsMode() { return disableBypassPermissionsMode; @@ -88,4 +91,25 @@ public ManagedSettingsPermissions setAllow(List rules) { this.allow = rules == null ? null : new ArrayList<>(rules); return this; } + + /** + * @return the closed-world host boundary, or {@code null} when unset + */ + public List getLimitTo() { + return limitTo; + } + + /** + * Every network request must match this list, and multiple managed layers + * intersect their lists. A present empty list denies all hosts. + * + * @param rules + * host-only {@code Domain(hostname)}, {@code Domain(IP)}, or + * {@code Domain(*.example.com)} boundary rules + * @return this policy + */ + public ManagedSettingsPermissions setLimitTo(List rules) { + this.limitTo = rules == null ? null : new ArrayList<>(rules); + return this; + } } 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/PermissionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequest.java index fc49332b8a..c700d93a44 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequest.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonAnySetter; @@ -45,6 +46,9 @@ public class PermissionRequest { @JsonDeserialize(using = ManagedApprovalRequiredDeserializer.class) private Boolean managedApprovalRequired; + @JsonProperty("readOnlyDirectories") + private List readOnlyDirectories; + private Map extensionData; @JsonAnySetter @@ -146,6 +150,28 @@ public void setManagedApprovalRequired(Boolean managedApprovalRequired) { this.managedApprovalRequired = managedApprovalRequired; } + /** + * Gets the canonical directory candidates that may be approved for read-only + * access. + * + * @return the read-only directory candidates, or {@code null} when this request + * does not offer directory-scoped read authority + */ + public List getReadOnlyDirectories() { + return readOnlyDirectories; + } + + /** + * Sets the canonical directory candidates that may be approved for read-only + * access. + * + * @param readOnlyDirectories + * the read-only directory candidates + */ + public void setReadOnlyDirectories(List readOnlyDirectories) { + this.readOnlyDirectories = readOnlyDirectories; + } + /** * Gets additional extension data for the request. * diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResult.java b/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResult.java index 6546291cf7..7d3fc1ddda 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResult.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResult.java @@ -44,6 +44,9 @@ public final class PermissionRequestResult { @JsonProperty("feedback") private String feedback; + @JsonProperty("directories") + private List directories; + /** * Optional provenance describing how and where this decision was made. Never * serialized inside the result — the SDK forwards it as a sibling of @@ -63,6 +66,20 @@ public static PermissionRequestResult approveOnce() { return new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED); } + /** + * Creates a result that approves read access to the selected directories for + * the rest of the session. + * + * @param directories + * the canonical directory candidates selected from the permission + * request + * @return a new session-scoped read-only result + */ + public static PermissionRequestResult approveReadOnlyForSession(List directories) { + return new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVE_READ_ONLY_FOR_SESSION) + .setDirectories(directories); + } + /** * Creates a result that rejects the request, optionally forwarding feedback to * the LLM. @@ -180,6 +197,28 @@ public PermissionRequestResult setFeedback(String feedback) { return this; } + /** + * Gets the directories selected for a read-only approval. + * + * @return the selected canonical directories, or {@code null} + */ + public List getDirectories() { + return directories; + } + + /** + * Sets the directories selected for a read-only approval. + * + * @param directories + * the canonical directory candidates selected from the permission + * request + * @return this result for method chaining + */ + public PermissionRequestResult setDirectories(List directories) { + this.directories = directories; + return this; + } + /** * Gets the optional provenance describing how and where this decision was made. *

    diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResultKind.java b/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResultKind.java index 95476c36f6..1e33124f5c 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResultKind.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/PermissionRequestResultKind.java @@ -36,6 +36,10 @@ public final class PermissionRequestResultKind { /** The permission was approved for this one instance. */ public static final PermissionRequestResultKind APPROVED = new PermissionRequestResultKind("approve-once"); + /** Read access to selected directories was approved for the session. */ + public static final PermissionRequestResultKind APPROVE_READ_ONLY_FOR_SESSION = new PermissionRequestResultKind( + "approve-read-only-for-session"); + /** The permission was denied interactively by the user. */ public static final PermissionRequestResultKind REJECTED = new PermissionRequestResultKind("reject"); 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 9f4bf818d0..6555f965c9 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 @@ -15,6 +15,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.copilot.CopilotExperimental; +import com.github.copilot.SkillProvider; import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.rpc.DiagnosticsConfiguration; import com.github.copilot.generated.rpc.SessionLimitsConfig; @@ -57,6 +58,8 @@ public class ResumeSessionConfig { private Boolean enableFileChangeTracking; private SessionLimitsConfig sessionLimits; private Boolean enableExperimentalMode; + private Boolean continuePendingWork; + private Boolean allowTranscriptRecovery; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; private Boolean coauthorEnabled; @@ -81,6 +84,7 @@ public class ResumeSessionConfig { private Boolean enableHostGitOperations; private Boolean enableSessionStore; private Boolean enableSkills; + private SkillProvider skillProvider; private String embeddingCacheStorage; private boolean disableResume; private boolean streaming; @@ -141,6 +145,72 @@ public ResumeSessionConfig setModel(String model) { return this; } + /** + * Gets whether pending work should continue after resuming the session. + * + * @return the explicit choice, or empty to use the runtime default + */ + @JsonIgnore + public Optional getContinuePendingWork() { + return Optional.ofNullable(continuePendingWork); + } + + /** + * Sets whether to continue pending work when resuming the session. + * + * @param continuePendingWork + * whether to continue pending work + * @return this config for method chaining + */ + public ResumeSessionConfig setContinuePendingWork(boolean continuePendingWork) { + this.continuePendingWork = continuePendingWork; + return this; + } + + /** + * Clears the pending-work choice so the runtime default applies. + * + * @return this config for method chaining + */ + public ResumeSessionConfig clearContinuePendingWork() { + this.continuePendingWork = null; + 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. * @@ -1243,6 +1313,35 @@ public ResumeSessionConfig clearEnableSkills() { return this; } + /** + * Gets the session-scoped skill provider for the resumed session. + * + * @return the skill provider, or {@code null} when not configured + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + @JsonIgnore + public SkillProvider getSkillProvider() { + return skillProvider; + } + + /** + * Sets the session-scoped skill provider for the resumed session. + *

    + * Skill provider registrations are not persisted; re-supply the provider when + * resuming. + * + * @param skillProvider + * the skill provider to expose to the runtime + * @return this config for method chaining + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + public ResumeSessionConfig setSkillProvider(SkillProvider skillProvider) { + this.skillProvider = skillProvider; + return this; + } + /** * Gets the embedding cache storage mode. * @@ -1343,6 +1442,18 @@ public boolean isDisableResume() { return disableResume; } + /** + * Controls whether persisted pending work may continue on resume. + * + * @param continuePendingWork + * explicit setting, or {@code null} for the default + * @return this config instance + */ + public ResumeSessionConfig setContinuePendingWork(Boolean continuePendingWork) { + this.continuePendingWork = continuePendingWork; + return this; + } + /** * Sets whether to disable the session.resume event. *

    @@ -2141,6 +2252,8 @@ public ResumeSessionConfig clone() { copy.enableFileChangeTracking = this.enableFileChangeTracking; 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; @@ -2162,6 +2275,7 @@ public ResumeSessionConfig clone() { copy.enableHostGitOperations = this.enableHostGitOperations; copy.enableSessionStore = this.enableSessionStore; copy.enableSkills = this.enableSkills; + copy.skillProvider = this.skillProvider; copy.embeddingCacheStorage = this.embeddingCacheStorage; copy.disableResume = this.disableResume; copy.streaming = this.streaming; 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 0f53bf5ce5..20cfe6e123 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 @@ -39,6 +39,12 @@ public final class ResumeSessionRequest { @JsonProperty("model") private String model; + @JsonProperty("continuePendingWork") + private Boolean continuePendingWork; + + @JsonProperty("allowTranscriptRecovery") + private Boolean allowTranscriptRecovery; + @JsonProperty("reasoningEffort") private String reasoningEffort; @@ -141,6 +147,10 @@ public final class ResumeSessionRequest { @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableSkills; + @JsonProperty("hasSkillProvider") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean hasSkillProvider; + @JsonProperty("embeddingCacheStorage") @JsonInclude(JsonInclude.Include.NON_NULL) private String embeddingCacheStorage; @@ -289,6 +299,43 @@ public void setModel(String model) { this.model = model; } + /** + * Gets the pending-work continuation choice. + * + * @return the choice, or {@code null} if omitted + */ + public Boolean getContinuePendingWork() { + return continuePendingWork; + } + + /** + * Sets the pending-work continuation choice. + * + * @param continuePendingWork + * the choice + */ + public void setContinuePendingWork(boolean continuePendingWork) { + this.continuePendingWork = continuePendingWork; + } + + /** Restores the runtime's default pending-work continuation behavior. */ + 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; @@ -721,6 +768,23 @@ public void clearEnableSkills() { this.enableSkills = null; } + /** Gets skill-provider availability flag. @return the flag */ + public Boolean getHasSkillProvider() { + return hasSkillProvider; + } + + /** Sets skill-provider availability flag. @param hasSkillProvider the flag */ + public void setHasSkillProvider(boolean hasSkillProvider) { + this.hasSkillProvider = hasSkillProvider; + } + + /** + * Clears the hasSkillProvider setting, reverting to the default behavior. + */ + public void clearHasSkillProvider() { + this.hasSkillProvider = null; + } + /** Gets embedding cache storage mode. @return the mode */ public String getEmbeddingCacheStorage() { return embeddingCacheStorage; 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/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index 836b972ae2..884b7d3e9f 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.copilot.CopilotExperimental; +import com.github.copilot.SkillProvider; import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.rpc.DiagnosticsConfiguration; import com.github.copilot.generated.rpc.SessionLimitsConfig; @@ -101,6 +102,7 @@ public class SessionConfig { private Boolean enableHostGitOperations; private Boolean enableSessionStore; private Boolean enableSkills; + private SkillProvider skillProvider; private String embeddingCacheStorage; private ModelCapabilitiesOverride modelCapabilities; private Consumer onEvent; @@ -1759,6 +1761,35 @@ public SessionConfig clearEnableSkills() { return this; } + /** + * Gets the session-scoped skill provider. + * + * @return the skill provider, or {@code null} when not configured + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + @JsonIgnore + public SkillProvider getSkillProvider() { + return skillProvider; + } + + /** + * Sets the session-scoped skill provider. + *

    + * Re-supply this provider when resuming a session. Skill providers are not + * supported for cloud sessions. + * + * @param skillProvider + * the skill provider to expose to the runtime + * @return this config instance for method chaining + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + public SessionConfig setSkillProvider(SkillProvider skillProvider) { + this.skillProvider = skillProvider; + return this; + } + /** * Gets the embedding cache storage mode. * @@ -2366,6 +2397,7 @@ public SessionConfig clone() { copy.enableHostGitOperations = this.enableHostGitOperations; copy.enableSessionStore = this.enableSessionStore; copy.enableSkills = this.enableSkills; + copy.skillProvider = this.skillProvider; copy.embeddingCacheStorage = this.embeddingCacheStorage; copy.modelCapabilities = this.modelCapabilities; copy.onEvent = this.onEvent; 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 new file mode 100644 index 0000000000..2ffaf50442 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/AhpHostTest.java @@ -0,0 +1,446 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.JsonNode; +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; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import java.io.IOException; +import java.net.ServerSocket; +import java.util.HashMap; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +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.ValueSource; + +@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()) + .filter(method -> method.getDeclaringClass() == ServerHostApi.class).map(method -> method.getName()) + .collect(Collectors.toSet()); + assertEquals(Set.of("start", "dispose", "publishSession"), methods); + } + + @Test + void cancelledStartupDisposesAfterStartCompletes() throws Exception { + try (var server = new FakeRuntime(); var client = server.client()) { + server.finishStart = new CompletableFuture<>(); + var pending = client.startAhpHost(localOptions()); + String hostId = await(server.startEntered); + assertTrue(pending.cancel(false)); + assertEquals(0, server.disposals.get()); + server.finishStart.complete(null); + assertEquals(hostId, await(server.disposedHost)); + assertTrue(pending.isCancelled()); + assertEquals(1, server.disposals.get()); + } + } + + @ParameterizedTest + @CsvSource({"false,false", "false,true", "true,false", "true,true"}) + void preservesFactoryConfigurationAndReleasesExactObject(boolean resume, boolean enabled) throws Exception { + try (var server = new FakeRuntime(); var client = server.client()) { + var created = new CompletableFuture(); + var released = new CompletableFuture(); + var options = localOptions().setCreateSession(request -> client + .createSession(request.config().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .thenApply(session -> { + created.complete(session); + return session; + })) + .setResumeSession(request -> client + .resumeSession(request.sessionId(), + request.config().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .thenApply(session -> { + created.complete(session); + return session; + })) + .setOnSessionReleased(session -> { + released.complete(session); + return CompletableFuture.completedFuture(null); + }); + var host = await(client.startAhpHost(options)); + Map config = new HashMap<>(Map.of("sessionId", "requested", "workingDirectory", + "/workspace/project", "additionalDirectories", List.of(), "configDir", "/workspace/config", + "mcpOAuthTokenStorage", "memory", "gitHubToken", "unit-test-token", "featureFlags", + Map.of("custom_flag_name", true), "enableExperimentalMode", true, "enableMcpApps", enabled)); + config.put("streaming", enabled); + if (resume) { + config.put("suppressResumeEvent", enabled); + config.put("continuePendingWork", false); + } + assertEquals("requested", + await(server.materialize(host, "handoff", resume, config)).get("sessionId").asText()); + server.release(host, "handoff"); + server.release(host, "handoff"); + assertSame(await(created), await(released)); + assertTrue(server.sessionRequests.get(0).get("additionalDirectories").isEmpty()); + await(host.dispose()); + } + } + + @ParameterizedTest + @CsvSource({"streaming,false", "streaming,true", "enableMcpApps,false", "enableMcpApps,true", + "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(localOptions().setResumeSession(request -> { + switch (setting) { + case "streaming" -> request.config().setStreaming(!expected); + case "enableMcpApps" -> request.config().setEnableMcpApps(!expected); + case "suppressResumeEvent" -> request.config().setDisableResume(!expected); + default -> throw new AssertionError(setting); + } + return client.resumeSession(request.sessionId(), + request.config().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); + }))); + var error = assertThrows(Exception.class, () -> await( + server.materialize(host, "changed", true, Map.of("sessionId", "changed", setting, expected)))); + assertTrue(error.getMessage().contains("config.")); + await(host.dispose()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + 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(localOptions().setCreateSession(request -> { + if (changed) { + request.config().setSessionLimits(new SessionLimitsConfig(credits.doubleValue() + 1)); + } + return client + .createSession(request.config().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); + }).setResumeSession(request -> { + if (changed) { + request.config().setSessionLimits(new SessionLimitsConfig(credits.doubleValue() + 1)); + } + return client.resumeSession(request.sessionId(), + request.config().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); + }))); + var pending = server.materialize(host, "numeric", resume, + Map.of("sessionId", "numeric", "sessionLimits", Map.of("maxAiCredits", credits))); + if (changed) { + var error = assertThrows(Exception.class, () -> await(pending)); + assertTrue(error.getMessage().contains("config.sessionLimits")); + } else { + assertEquals("numeric", await(pending).get("sessionId").asText()); + } + await(host.dispose()); + } + } + } + } + + @Test + void cancellationUnblocksRpcAndReleasesLateResultAfterOwnerStops() throws Exception { + try (var server = new FakeRuntime(); var client = server.client()) { + var late = new CompletableFuture(); + var entered = new CompletableFuture>(); + var released = new CompletableFuture(); + var original = await(client.createSession(new SessionConfig().setSessionId("requested") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))); + var host = await(client.startAhpHost(localOptions().setCreateSession(request -> { + entered.complete(request.cancellation()); + return late; + }).setOnSessionReleased(session -> { + released.complete(session); + return CompletableFuture.completedFuture(null); + }))); + var pending = server.materialize(host, "pending", false, Map.of("sessionId", "requested")); + var cancellation = await(entered); + assertThrows(Exception.class, + () -> await(server.materialize(host, "pending", false, Map.of("sessionId", "requested")))); + assertFalse(pending.isDone()); + server.release(host, "pending"); + assertThrows(Exception.class, () -> await(pending)); + await(cancellation.toCompletableFuture()); + await(client.stop()); + late.complete(original); + assertSame(original, await(released)); + } + } + + @Test + void retainedResumeDoesNotReconfigureOriginal() throws Exception { + try (var server = new FakeRuntime(); var client = server.client()) { + var original = await(client.createSession(new SessionConfig().setSessionId("retained") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))); + var host = await(client.startAhpHost( + localOptions().setResumeSession(request -> CompletableFuture.completedFuture(original)))); + assertEquals("retained", + await(server.materialize(host, "resume", true, + Map.of("sessionId", "retained", "workingDirectory", "/new/default"))).get("sessionId") + .asText()); + assertEquals(1, server.sessionRequests.size()); + await(host.dispose()); + } + } + + @Test + void rejectsForeignObjectsAndChangedHostSettings() throws Exception { + try (var server = new FakeRuntime(); + var foreignServer = new FakeRuntime(); + var client = server.client(); + var foreign = foreignServer.client()) { + var wrongOwner = await(foreign.createSession(new SessionConfig().setSessionId("requested") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))); + var released = new CompletableFuture(); + var host = await(client.startAhpHost( + localOptions().setCreateSession(request -> CompletableFuture.completedFuture(wrongOwner)) + .setOnSessionReleased(session -> { + released.complete(session); + return CompletableFuture.completedFuture(null); + }))); + var error = assertThrows(Exception.class, + () -> await(server.materialize(host, "foreign", false, Map.of("sessionId", "requested")))); + assertTrue(error.getMessage().contains("owning client")); + assertSame(wrongOwner, await(released)); + await(host.dispose()); + 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")); + await(changed.dispose()); + } + } + + @Test + 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(localOptions().setOnExit(info -> { + exits.incrementAndGet(); + exited.complete(null); + return CompletableFuture.completedFuture(null); + }))); + assertNull(host.getPid()); + assertEquals("ahp-session:/resident", await(host.publishSession("resident")).sessionUri()); + await(CompletableFuture.allOf(host.dispose(), host.dispose())); + await(exited); + assertEquals(2, server.disposals.get()); + assertEquals(1, exits.get()); + await(client.stop()); + assertThrows(Exception.class, () -> await(host.dispose())); + assertEquals(2, server.disposals.get()); + } + } + + private static final class FakeRuntime implements AutoCloseable { + final ServerSocket listener = new ServerSocket(0); + final CompletableFuture connected = new CompletableFuture<>(); + 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; + + FakeRuntime() throws IOException { + accept = new Thread(() -> { + try { + var rpc = JsonRpcClient.fromSocket(listener.accept(), transport -> { + 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); + respond(transport, id, Map.of("sessionId", + params.path("sessionId").asText(UUID.randomUUID().toString()))); + }); + } + for (String method : List.of("session.disconnect", "session.options.update")) { + transport.registerMethodHandler(method, (id, params) -> respond(transport, id, Map.of())); + } + transport.registerMethodHandler("host.start", (id, params) -> { + startEntered.complete(params.get("hostId").asText()); + 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, + Map.of("sessionId", params.get("sessionId").asText(), "sessionUri", + "ahp-session:/" + params.get("sessionId").asText()))); + transport.registerMethodHandler("host.dispose", (id, params) -> { + disposals.incrementAndGet(); + try { + transport.notify("host.exited", + Map.of("hostId", params.get("hostId").asText(), "reason", "disposed")); + } catch (IOException error) { + throw new IllegalStateException(error); + } + respond(transport, id, Map.of()); + disposedHost.complete(params.get("hostId").asText()); + }); + }); + connected.complete(rpc); + } catch (IOException error) { + connected.completeExceptionally(error); + } + }, "ahp-test-runtime"); + accept.setDaemon(true); + accept.start(); + } + + CopilotClient client() { + return new CopilotClient(new CopilotClientOptions().setCliUrl("127.0.0.1:" + listener.getLocalPort())); + } + + CompletableFuture materialize(AhpHost host, String handoff, boolean resume, + Map config) throws Exception { + return await(connected).invoke("host.materializeSession", + Map.of("hostId", host.getHostId(), "handoffId", handoff, "resume", resume, "config", config), + JsonNode.class); + } + + void release(AhpHost host, String handoff) throws Exception { + await(connected).notify("host.sessionReleased", Map.of("hostId", host.getHostId(), "handoffId", handoff)); + } + + static void respond(JsonRpcClient rpc, String id, Object value) { + try { + rpc.sendResponse(Long.valueOf(id), value); + } catch (IOException error) { + throw new IllegalStateException(error); + } + } + + @Override + public void close() throws Exception { + if (connected.getNow(null) != null) { + connected.join().close(); + } + listener.close(); + accept.join(5000); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/AhpTestClient.java b/java/sdk/src/test/java/com/github/copilot/AhpTestClient.java new file mode 100644 index 0000000000..b98a96d585 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/AhpTestClient.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.fasterxml.jackson.databind.JsonNode; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +final class AhpTestClient implements AutoCloseable { + private final Process process; + private final BufferedReader output; + private final BufferedWriter input; + + AhpTestClient(Path sdkRoot) throws Exception { + process = new ProcessBuilder("node", "--import", "tsx", "test/e2e/harness/ahpTestDriver.ts") + .directory(sdkRoot.resolve("nodejs").toFile()).redirectError(ProcessBuilder.Redirect.INHERIT).start(); + output = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); + input = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8)); + try { + if (!read().path("ready").asBoolean()) { + throw new IOException("Standard AHP test client did not become ready"); + } + } catch (Exception error) { + try { + close(); + } catch (Exception cleanup) { + error.addSuppressed(cleanup); + } + throw error; + } + } + + synchronized JsonNode request(Map command) throws Exception { + input.write(JsonRpcClient.getObjectMapper().writeValueAsString(command)); + input.newLine(); + input.flush(); + JsonNode response = read(); + if (response.has("error")) { + throw new IOException(response.get("error").asText()); + } + return response.required("result"); + } + + private JsonNode read() throws Exception { + return CompletableFuture.supplyAsync(() -> { + try { + String line = output.readLine(); + if (line == null) { + throw new IOException("Standard AHP test client closed output unexpectedly"); + } + return JsonRpcClient.getObjectMapper().readTree(line); + } catch (IOException error) { + throw new UncheckedIOException(error); + } + }).get(60, TimeUnit.SECONDS); + } + + @Override + public void close() throws Exception { + try { + input.close(); + if (!process.waitFor(10, TimeUnit.SECONDS)) { + throw new IOException("Standard AHP test client did not stop"); + } + if (process.exitValue() != 0) { + throw new IOException("Standard AHP test client exited with code " + process.exitValue()); + } + } finally { + if (process.isAlive()) { + process.destroyForcibly(); + if (!process.waitFor(10, TimeUnit.SECONDS)) { + throw new IOException("Could not reap standard AHP test client"); + } + } + output.close(); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/AuthHostE2ETest.java b/java/sdk/src/test/java/com/github/copilot/AuthHostE2ETest.java new file mode 100644 index 0000000000..c298d05657 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/AuthHostE2ETest.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import com.github.copilot.rpc.CopilotClientOptions; + +class AuthHostE2ETest { + + @ParameterizedTest + @CsvSource({"'', fallback.ghe.example, https://fallback.ghe.example", + "tenant.ghe.example, fallback.ghe.example, https://tenant.ghe.example", "'', '', https://github.com"}) + void usesConfiguredGitHubHostForAuthentication(String copilotHost, String ghHost, String expectedHost) + throws Exception { + try (var ctx = E2ETestContext.create()) { + ctx.configureForTest("client", "should_get_authenticated_status"); + Map environment = ctx.getEnvironment(); + environment.put("COPILOT_GH_HOST", copilotHost); + environment.put("GH_HOST", ghHost); + + try (var client = ctx.createClient(new CopilotClientOptions().setEnvironment(environment) + .setGitHubToken(environment.get("GH_TOKEN")))) { + client.start().get(30, TimeUnit.SECONDS); + var status = client.getAuthStatus().get(30, TimeUnit.SECONDS); + assertTrue(status.isAuthenticated(), status.getStatusMessage()); + assertEquals(expectedHost, status.getHost()); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java index b41f99adb7..462fbc17cd 100644 --- a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.io.TempDir; import com.github.copilot.ffi.NativeRuntimeLoader; +import com.github.copilot.rpc.CopilotClientMode; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.TelemetryConfig; @@ -67,6 +68,21 @@ void explicitCliPathOverridesEnvironment() throws Exception { assertEquals(explicit.toString(), manager.resolveCliLaunch("inherited-copilot-runtime").executable()); } + @Test + void cliModeOptsIntoProcessLoggingWithoutChangingEmptyModeEnvironment() { + for (var mode : CopilotClientMode.values()) { + var options = new CopilotClientOptions().setMode(mode) + .setEnvironment(Map.of("COPILOT_RUNTIME_PROCESS_FILE_LOGGING", "opposite")); + var process = new ProcessBuilder("copilot-runtime"); + new CliServerManager(options).configureProcessEnvironment(process); + if (mode == CopilotClientMode.EMPTY) { + assertEquals("opposite", process.environment().get("COPILOT_RUNTIME_PROCESS_FILE_LOGGING")); + } else { + assertEquals("1", process.environment().get("COPILOT_RUNTIME_PROCESS_FILE_LOGGING")); + } + } + } + // ===== parseCliUrl tests ===== @Test diff --git a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java index 8f937fcb99..fba9a6413b 100644 --- a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -323,6 +323,17 @@ void sessionConfigGranularMultitenancyFieldsCopied() { assertFalse(cloned.getEnableSkills().orElse(true)); } + @Test + @AllowCopilotExperimental + void sessionConfigSkillProviderCloned() { + SkillProvider provider = testSkillProvider(); + SessionConfig original = new SessionConfig().setSkillProvider(provider); + + SessionConfig cloned = original.clone(); + + assertSame(provider, cloned.getSkillProvider()); + } + @Test void resumeSessionConfigEnableSessionTelemetryCopied() { ResumeSessionConfig original = new ResumeSessionConfig(); @@ -362,6 +373,17 @@ void resumeSessionConfigGranularMultitenancyFieldsCopied() { assertTrue(cloned.getEnableSkills().orElse(false)); } + @Test + @AllowCopilotExperimental + void resumeSessionConfigSkillProviderCloned() { + SkillProvider provider = testSkillProvider(); + ResumeSessionConfig original = new ResumeSessionConfig().setSkillProvider(provider); + + ResumeSessionConfig cloned = original.clone(); + + assertSame(provider, cloned.getSkillProvider()); + } + @Test void clonePreservesNullFields() { CopilotClientOptions opts = new CopilotClientOptions(); @@ -548,4 +570,19 @@ void resumeSessionConfigCloneCopiesModeSwitchHandlers() { assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); } + + @AllowCopilotExperimental + private static SkillProvider testSkillProvider() { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + } } 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 7101f56d3b..f4b635b6f7 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java @@ -6,19 +6,34 @@ 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.CloudSessionOptions; import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.CopilotClientMode; +import com.github.copilot.rpc.CreateSessionRequest; +import com.github.copilot.rpc.CreateSessionResponse; import com.github.copilot.rpc.DeleteSessionResponse; import com.github.copilot.rpc.GitHubTokenProviderResult; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.PingResponse; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.ResumeSessionResponse; import com.github.copilot.rpc.SessionConfig; import com.github.copilot.rpc.SessionLifecycleEvent; import com.github.copilot.rpc.SessionLifecycleEventTypes; import com.github.copilot.rpc.ToolDefinition; +import java.io.OutputStream; import java.lang.reflect.Field; +import java.io.BufferedReader; +import java.io.FilterOutputStream; +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; @@ -29,6 +44,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.*; @@ -50,31 +66,6 @@ static void setup() { cliPath = TestUtil.findCliPath(); } - @Test - void testStopRequestsRuntimeShutdownForOwnedProcess() throws Exception { - var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false)); - var rpc = mock(JsonRpcClient.class); - when(rpc.invoke(eq("runtime.shutdown"), any(), eq(Void.class))) - .thenReturn(CompletableFuture.completedFuture(null)); - var process = mock(Process.class); - when(process.isAlive()).thenReturn(true); - when(process.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true); - - setConnectionFuture(client, rpc, process); - - client.stop().get(); - - verify(rpc).invoke(eq("runtime.shutdown"), eq(Map.of()), eq(Void.class)); - verify(rpc).close(); - // The runtime never self-exits after runtime.shutdown (it keeps its - // JSON-RPC server alive to send the response and leaves termination to - // the caller), so stop() terminates the owned process. The mocked - // process exits on the first SIGTERM (waitFor returns true), so we - // never escalate to destroyForcibly(). - verify(process).destroy(); - verify(process, never()).destroyForcibly(); - } - @Test void testStopDoesNotThrowWhenRuntimeShutdownFails() throws Exception { var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false)); @@ -83,6 +74,7 @@ void testStopDoesNotThrowWhenRuntimeShutdownFails() throws Exception { .thenReturn(CompletableFuture.failedFuture(new RuntimeException("shutdown failed"))); var process = mock(Process.class); when(process.isAlive()).thenReturn(true); + when(process.getOutputStream()).thenReturn(OutputStream.nullOutputStream()); when(process.destroyForcibly()).thenReturn(process); when(process.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true); @@ -191,6 +183,137 @@ void testDeleteSessionReleasesGitHubTokenProvider() throws Exception { } } + @Test + @AllowCopilotExperimental + void cloudSessionWithSkillProviderIsRejectedBeforeConnectingOrCallingProvider() throws Exception { + var providerCalls = new AtomicInteger(); + SkillProvider provider = new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + providerCalls.incrementAndGet(); + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + providerCalls.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + }; + + try (var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false))) { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCloud(new CloudSessionOptions()).setSkillProvider(provider); + + ExecutionException error = assertThrows(ExecutionException.class, () -> client.createSession(config).get()); + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + assertEquals("Skill providers are not supported for cloud sessions.", error.getCause().getMessage()); + assertEquals(0, providerCalls.get()); + } + } + + @Test + @AllowCopilotExperimental + void emptyModeCreateWithSkillProviderSendsFlagsAndRegistersBeforeRpc() throws Exception { + var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setMode(CopilotClientMode.EMPTY) + .setCopilotHome(System.getProperty("java.io.tmpdir"))); + var rpc = mock(JsonRpcClient.class); + setConnectionFuture(client, rpc, null); + SkillProvider provider = testSkillProvider(); + var createRequest = new AtomicReference(); + + when(rpc.invoke(eq("session.create"), any(), eq(CreateSessionResponse.class))).thenAnswer(invocation -> { + CreateSessionRequest request = invocation.getArgument(1); + createRequest.set(request); + assertTrue(request.getHasSkillProvider()); + assertFalse(request.getEnableSkills()); + assertSame(provider, registeredSkillProvider(client, "skill-provider-session")); + return CompletableFuture + .completedFuture(new CreateSessionResponse("skill-provider-session", null, null, null)); + }); + when(rpc.invoke(eq("session.options.update"), any(), eq(Void.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + try { + var config = new SessionConfig().setSessionId("skill-provider-session") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setAvailableTools(List.of()) + .setSkillProvider(provider); + + CopilotSession session = client.createSession(config).get(); + + assertSame(provider, session.getSkillProvider()); + assertNotNull(createRequest.get()); + assertTrue(createRequest.get().getHasSkillProvider()); + assertFalse(createRequest.get().getEnableSkills()); + } finally { + client.forceStop().get(); + } + } + + @Test + @AllowCopilotExperimental + void emptyModeResumeValidationFailureKeepsResidentSkillProvider() throws Exception { + var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setMode(CopilotClientMode.EMPTY) + .setCopilotHome(System.getProperty("java.io.tmpdir"))); + var rpc = mock(JsonRpcClient.class); + setConnectionFuture(client, rpc, null); + SkillProvider resident = testSkillProvider(); + when(rpc.invoke(eq("session.create"), any(), eq(CreateSessionResponse.class))).thenReturn( + CompletableFuture.completedFuture(new CreateSessionResponse("resident-session", null, null, null))); + when(rpc.invoke(eq("session.options.update"), any(), eq(Void.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + try { + client.createSession(new SessionConfig().setSessionId("resident-session") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setAvailableTools(List.of()) + .setSkillProvider(resident)).get(); + + var resumeConfig = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillProvider(testSkillProvider()); + var error = assertThrows(ExecutionException.class, + () -> client.resumeSession("resident-session", resumeConfig).get()); + + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + assertSame(resident, registeredSkillProvider(client, "resident-session")); + verify(rpc, never()).invoke(eq("session.resume"), any(), any()); + } finally { + client.forceStop().get(); + } + } + + @Test + @AllowCopilotExperimental + void failedResumeDoesNotEvictAConcurrentResume() throws Exception { + var client = new CopilotClient( + new CopilotClientOptions().setAutoStart(false).setCopilotHome(System.getProperty("java.io.tmpdir"))); + var rpc = mock(JsonRpcClient.class); + setConnectionFuture(client, rpc, null); + when(rpc.invoke(eq("session.create"), any(), eq(CreateSessionResponse.class))).thenReturn( + CompletableFuture.completedFuture(new CreateSessionResponse("shared-session", null, null, null))); + when(rpc.invoke(eq("session.options.update"), any(), eq(Void.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + var firstResume = new CompletableFuture(); + when(rpc.invoke(eq("session.resume"), any(), eq(ResumeSessionResponse.class))).thenReturn(firstResume, + CompletableFuture.completedFuture(new ResumeSessionResponse("shared-session", null, null, null))); + + try { + client.createSession(new SessionConfig().setSessionId("shared-session") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(testSkillProvider())).get(); + var failing = client.resumeSession("shared-session", new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(testSkillProvider())); + SkillProvider winner = testSkillProvider(); + client.resumeSession("shared-session", new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(winner)).get(); + + firstResume.completeExceptionally(new RuntimeException("resume failed")); + + assertThrows(ExecutionException.class, failing::get); + assertSame(winner, registeredSkillProvider(client, "shared-session")); + } finally { + client.forceStop().get(); + } + } + @Test void testClientConstruction() { var client = new CopilotClient(); @@ -278,6 +401,135 @@ 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); + } + }); + process.stdout.write('started\\n'); + """); + var options = new CopilotClientOptions().setCliPath(script.toString()).setUseStdio(true); + var manager = spy(new CliServerManager(options)); + var child = new AtomicReference(); + var failedOutput = new AtomicReference(); + var writeFailure = new IOException("Controlled startup write failure"); + doAnswer(invocation -> { + CliServerManager.ProcessInfo info = (CliServerManager.ProcessInfo) invocation.callRealMethod(); + child.set(info.process()); + // Closing fd 0 does not reliably fail the first parent write on Windows. + var output = new FilterOutputStream(info.process().getOutputStream()) { + @Override + public void write(int value) throws IOException { + throw writeFailure; + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + throw writeFailure; + } + }; + failedOutput.set(output); + var transportProcess = mock(Process.class, org.mockito.AdditionalAnswers.delegatesTo(info.process())); + doReturn(output).when(transportProcess).getOutputStream(); + return new CliServerManager.ProcessInfo(transportProcess, info.port()); + }).when(manager).startCliServer(); + doAnswer(invocation -> { + Process process = invocation.getArgument(0); + 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. + assertTrue(child.get().isAlive(), "Startup must drain stderr before killing the controlled child."); + 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(); + } + assertSame(writeFailure, original, "Startup must preserve the actual failed transport write."); + assertNotSame(cause, original, "The startup error must retain its original transport failure."); + assertFalse(child.get().isAlive(), "Failed startup retained its child process."); + } finally { + if (failedOutput.get() != null) { + failedOutput.get().close(); + } + 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"); @@ -707,4 +959,29 @@ private static void setConnectionFuture(CopilotClient client, JsonRpcClient rpc, field.setAccessible(true); field.set(client, CompletableFuture.completedFuture(connection)); } + + @AllowCopilotExperimental + private static SkillProvider testSkillProvider() { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + } + + @AllowCopilotExperimental + @SuppressWarnings("unchecked") + private static SkillProvider registeredSkillProvider(CopilotClient client, String sessionId) throws Exception { + Field sessionsField = CopilotClient.class.getDeclaredField("sessions"); + sessionsField.setAccessible(true); + Map sessions = (Map) sessionsField.get(client); + CopilotSession session = sessions.get(sessionId); + return session != null ? session.getSkillProvider() : null; + } } 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/E2ETestContext.java b/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java index a6bec65d2c..8a0e335758 100644 --- a/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java +++ b/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java @@ -119,6 +119,7 @@ public static E2ETestContext create() throws IOException, InterruptedException { // individual_pro user at context creation. Map defaultUser = new HashMap<>(); defaultUser.put("login", "e2e-test-user"); + defaultUser.put("id", 12345); defaultUser.put("copilot_plan", "individual_pro"); defaultUser.put("endpoints", Map.of("api", proxyUrl, "telemetry", "https://localhost:1/telemetry")); defaultUser.put("analytics_tracking_id", "e2e-test-tracking-id"); @@ -395,12 +396,12 @@ public Map getEnvironment() { env.put("REQUESTS_CA_BUNDLE", caFile); env.put("CURL_CA_BUNDLE", caFile); env.put("GIT_SSL_CAINFO", caFile); - env.put("GH_TOKEN", DEFAULT_GITHUB_TOKEN); - env.put("GITHUB_TOKEN", DEFAULT_GITHUB_TOKEN); env.put("GH_ENTERPRISE_TOKEN", ""); env.put("GITHUB_ENTERPRISE_TOKEN", ""); } + env.put("GH_TOKEN", DEFAULT_GITHUB_TOKEN); + env.put("GITHUB_TOKEN", DEFAULT_GITHUB_TOKEN); return env; } 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/InstallationConfirmationTest.java b/java/sdk/src/test/java/com/github/copilot/InstallationConfirmationTest.java new file mode 100644 index 0000000000..8394209215 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/InstallationConfirmationTest.java @@ -0,0 +1,631 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.generated.rpc.InstallationConfirmationRequest; +import com.github.copilot.generated.rpc.InstallationDecision; +import com.github.copilot.generated.rpc.InstallationsConfirmResult; +import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.InstallationConfirmationContext; +import com.github.copilot.rpc.InstallationConfirmationHandler; +import com.github.copilot.rpc.RuntimeConnection; + +@AllowCopilotExperimental +class InstallationConfirmationTest { + + private static final int TIMEOUT_SECONDS = 15; + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + @Test + void typedReviewReceivedWithOriginalOperationAndSessionPreserved() throws Exception { + var observedRequest = new CompletableFuture(); + var observedContext = new CompletableFuture(); + InstallationConfirmationHandler handler = (request, context) -> { + observedRequest.complete(request); + observedContext.complete(context); + return CompletableFuture.completedFuture(InstallationDecision.CONFIRM); + }; + + try (var peer = new TestPeer(handler)) { + var result = peer.confirm(request("a")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + var request = observedRequest.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + var context = observedContext.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + assertEquals("operation-a", request.operationId()); + assertEquals("original-session", request.policySessionId()); + assertEquals("challenge-a", request.confirmationId()); + assertEquals("fingerprint-a", request.reviewFingerprint()); + assertFalse(context.getCancelled().isDone()); + assertEquals(InstallationDecision.CONFIRM, result.decision()); + assertEquals(0, peer.extensionRegistrationCount()); + } + } + + @Test + void challengeAndFingerprintAreEchoed() throws Exception { + try (var peer = new TestPeer( + (request, context) -> CompletableFuture.completedFuture(InstallationDecision.DECLINE))) { + var result = peer.confirm(request("echo")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + assertEquals("challenge-echo", result.confirmationId()); + assertEquals("fingerprint-echo", result.reviewFingerprint()); + assertEquals(InstallationDecision.DECLINE, result.decision()); + } + } + + @Test + void cancelDecisionIsEchoed() throws Exception { + try (var peer = new TestPeer( + (request, context) -> CompletableFuture.completedFuture(InstallationDecision.CANCEL))) { + var result = peer.confirm(request("cancel")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + assertEquals("challenge-cancel", result.confirmationId()); + assertEquals("fingerprint-cancel", result.reviewFingerprint()); + assertEquals(InstallationDecision.CANCEL, result.decision()); + } + } + + @Test + void concurrentOutOfOrderDecisionsAndAnotherRpcWhilePending() throws Exception { + var aDecision = new CompletableFuture(); + var bDecision = new CompletableFuture(); + var sawA = new CompletableFuture(); + var sawB = new CompletableFuture(); + InstallationConfirmationHandler handler = (request, context) -> { + if ("operation-a".equals(request.operationId())) { + sawA.complete(null); + return aDecision; + } + sawB.complete(null); + return bDecision; + }; + + try (var peer = new TestPeer(handler)) { + var first = peer.confirm(request("a")); + var second = peer.confirm(request("b")); + sawA.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + sawB.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + var tokenError = assertRpcError(peer.runtime.invoke("gitHubToken.getToken", + Map.of("registrationId", "unknown", "host", "github.com", "reason", "test"), Object.class)); + assertEquals(-32603, tokenError.getCode()); + + bDecision.complete(InstallationDecision.DECLINE); + var secondResult = second.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertEquals("challenge-b", secondResult.confirmationId()); + assertEquals(InstallationDecision.DECLINE, secondResult.decision()); + + aDecision.complete(InstallationDecision.CONFIRM); + var firstResult = first.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertEquals("challenge-a", firstResult.confirmationId()); + assertEquals(InstallationDecision.CONFIRM, firstResult.decision()); + } + } + + @Test + void cancelRequestRetiresOnlyCancelledReviewAndDropsLateDecision() throws Exception { + var lateDecision = new CompletableFuture(); + var contexts = new ConcurrentHashMap>(); + InstallationConfirmationHandler handler = (request, context) -> { + contextFuture(contexts, request.operationId()).complete(context); + if ("operation-a".equals(request.operationId())) { + return lateDecision; + } + return CompletableFuture.completedFuture(InstallationDecision.DECLINE); + }; + + try (var peer = new TestPeer(handler)) { + var first = peer.confirm(request("a")); + var context = contextFuture(contexts, "operation-a").get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + peer.cancel(1L); + var error = assertRpcError(first); + assertEquals(-32800, error.getCode()); + assertTrue(context.getCancelled().isDone()); + + lateDecision.complete(InstallationDecision.CONFIRM); + var second = peer.confirm(request("b")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertEquals("challenge-b", second.confirmationId()); + assertEquals(InstallationDecision.DECLINE, second.decision()); + } + } + + @Test + void cancelRequestAndDecisionReadyTogetherRejectsDecision() throws Exception { + var returnGate = new CompletableFuture(); + var observedContext = new CompletableFuture(); + InstallationConfirmationHandler handler = (request, context) -> { + observedContext.complete(context); + returnGate.join(); + return CompletableFuture.completedFuture(InstallationDecision.CONFIRM); + }; + + try (var peer = new TestPeer(handler)) { + var response = peer.confirm(request("a")); + var context = observedContext.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + peer.cancel(1L); + var error = assertRpcError(response); + assertEquals(-32800, error.getCode()); + assertTrue(context.getCancelled().isDone()); + + returnGate.complete(null); + } finally { + returnGate.complete(null); + } + } + + @Test + void staleUnknownOrStringCancelIdDoesNotTouchSuccessor() throws Exception { + var bDecision = new CompletableFuture(); + var contexts = new ConcurrentHashMap>(); + InstallationConfirmationHandler handler = (request, context) -> { + contextFuture(contexts, request.operationId()).complete(context); + if ("operation-b".equals(request.operationId())) { + return bDecision; + } + return CompletableFuture.completedFuture(InstallationDecision.DECLINE); + }; + + try (var peer = new TestPeer(handler)) { + var first = peer.confirm(request("a")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertEquals(InstallationDecision.DECLINE, first.decision()); + peer.cancel(1L); + + var second = peer.confirm(request("b")); + var context = contextFuture(contexts, "operation-b").get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + peer.cancel(999L); + peer.cancel("2"); + assertFalse(context.getCancelled().isDone()); + + bDecision.complete(InstallationDecision.CONFIRM); + var secondResult = second.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertEquals("challenge-b", secondResult.confirmationId()); + assertEquals(InstallationDecision.CONFIRM, secondResult.decision()); + } + } + + @Test + void connectionCloseCancelsAllReviewsAndDoesNotAffectAnotherConnection() throws Exception { + var decisions = new ConcurrentHashMap>(); + decisions.put("operation-a", new CompletableFuture<>()); + decisions.put("operation-b", new CompletableFuture<>()); + var contexts = new ConcurrentHashMap>(); + try (var first = new TestPeer((request, context) -> { + contextFuture(contexts, request.operationId()).complete(context); + return decisions.get(request.operationId()); + }); + var second = new TestPeer( + (request, context) -> CompletableFuture.completedFuture(InstallationDecision.DECLINE))) { + var firstResponse = first.confirm(request("a")); + var secondResponse = first.confirm(request("b")); + var firstContext = contextFuture(contexts, "operation-a").get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + var secondContext = contextFuture(contexts, "operation-b").get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + first.closeRuntimeConnection(); + firstContext.getCancelled().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + secondContext.getCancelled().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertFutureFails(firstResponse); + assertFutureFails(secondResponse); + + decisions.get("operation-a").complete(InstallationDecision.CONFIRM); + decisions.get("operation-b").complete(InstallationDecision.DECLINE); + var secondResult = second.confirm(request("c")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertEquals("challenge-c", secondResult.confirmationId()); + assertEquals(InstallationDecision.DECLINE, secondResult.decision()); + } + } + + @Test + void stoppingClientRetiresPendingReviews() throws Exception { + var pending = new CompletableFuture(); + var observedContext = new CompletableFuture(); + try (var peer = new TestPeer((request, context) -> { + observedContext.complete(context); + return pending; + })) { + var response = peer.confirm(request("a")); + var context = observedContext.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + peer.client.forceStop().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + context.getCancelled().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertTrue(context.getCancelled().isDone()); + assertFutureFails(response); + + pending.complete(InstallationDecision.CONFIRM); + } + } + + @Test + void missingHandlerInvalidReviewHandlerErrorsAndUnknownDecisionsNeverApprove() throws Exception { + try (var missing = new TestPeer(null)) { + var error = assertRpcError(missing.confirm(request("a"))); + assertEquals(-32601, error.getCode()); + } + + var handlerCalled = new AtomicBoolean(); + try (var invalid = new TestPeer((request, context) -> { + handlerCalled.set(true); + return CompletableFuture.completedFuture(InstallationDecision.CONFIRM); + })) { + var malformed = MAPPER.createObjectNode(); + malformed.put("confirmationId", "challenge-invalid"); + malformed.put("reviewFingerprint", "fingerprint-invalid"); + malformed.put("review", "not-an-object"); + var error = assertRpcError( + invalid.runtime.invoke("installations.confirm", malformed, InstallationsConfirmResult.class)); + assertEquals(-32602, error.getCode()); + assertFalse(handlerCalled.get()); + } + + try (var failing = new TestPeer((request, context) -> { + throw new IllegalStateException("refused"); + })) { + var error = assertRpcError(failing.confirm(request("a"))); + assertEquals(-32603, error.getCode()); + } + + try (var invalidDecision = new TestPeer( + (request, context) -> CompletableFuture.completedFuture(InstallationDecision.fromValue("unknown")))) { + var error = assertRpcError(invalidDecision.confirm(request("a"))); + assertEquals(-32603, error.getCode()); + } + } + + @Test + void optionalLegacySessionIsNotInferred() throws Exception { + var observedSession = new CompletableFuture(); + try (var peer = new TestPeer((request, context) -> { + observedSession.complete(request.policySessionId()); + return CompletableFuture.completedFuture(InstallationDecision.DECLINE); + })) { + var incoming = requestNode("legacy"); + incoming.remove("policySessionId"); + var result = peer.runtime.invoke("installations.confirm", incoming, InstallationsConfirmResult.class) + .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + assertNull(observedSession.get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + assertEquals(InstallationDecision.DECLINE, result.decision()); + } + } + + @Test + void clonePreservesInstallationConfirmationHandler() { + InstallationConfirmationHandler handler = (request, context) -> CompletableFuture + .completedFuture(InstallationDecision.CONFIRM); + var clone = new CopilotClientOptions().setInstallationConfirmationHandler(handler).clone(); + + assertSame(handler, clone.getInstallationConfirmationHandler()); + } + + private static CompletableFuture contextFuture( + ConcurrentHashMap> contexts, + String operationId) { + return contexts.computeIfAbsent(operationId, ignored -> new CompletableFuture<>()); + } + + private static InstallationConfirmationRequest request(String operation) throws IOException { + return MAPPER.treeToValue(requestNode(operation), InstallationConfirmationRequest.class); + } + + private static ObjectNode requestNode(String operation) throws IOException { + ObjectNode node = (ObjectNode) MAPPER.readTree(Files.readString(fixturePath())); + node.put("operationId", "operation-" + operation); + node.put("confirmationId", "challenge-" + operation); + node.put("reviewFingerprint", "fingerprint-" + operation); + return node; + } + + private static Path fixturePath() { + Path cwd = Path.of("").toAbsolutePath(); + for (Path path = cwd; path != null; path = path.getParent()) { + Path sdkCandidate = path.resolve("rust/tests/fixtures/installation_confirmation.json"); + if (Files.exists(sdkCandidate)) { + return sdkCandidate; + } + Path repoCandidate = path.resolve("src/sdk/rust/tests/fixtures/installation_confirmation.json"); + if (Files.exists(repoCandidate)) { + return repoCandidate; + } + } + throw new IllegalStateException("Could not locate installation confirmation fixture from " + cwd); + } + + private static JsonRpcException assertRpcError(CompletableFuture future) { + var error = assertThrows(ExecutionException.class, () -> future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + var cause = unwrap(error.getCause()); + assertInstanceOf(JsonRpcException.class, cause); + return (JsonRpcException) cause; + } + + private static void assertFutureFails(CompletableFuture future) { + assertThrows(ExecutionException.class, () -> future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + } + + private static Throwable unwrap(Throwable error) { + if (error instanceof CompletionException && error.getCause() != null) { + return error.getCause(); + } + return error; + } + + private static final class TestPeer implements AutoCloseable { + + private final JsonRpcClient runtime; + private final CopilotClient client; + private final InputStream clientInput; + private final OutputStream runtimeOutput; + private final InputStream runtimeInput; + private final OutputStream clientOutput; + private final AtomicInteger extensionRegistrations = new AtomicInteger(); + + TestPeer(InstallationConfirmationHandler handler) throws Exception { + InputStream openedClientInput = null; + OutputStream openedRuntimeOutput = null; + InputStream openedRuntimeInput = null; + OutputStream openedClientOutput = null; + try { + var runtimeToClient = new LoopbackPipe(); + openedClientInput = runtimeToClient.input(); + openedRuntimeOutput = runtimeToClient.output(); + var clientToRuntime = new LoopbackPipe(); + openedRuntimeInput = clientToRuntime.input(); + openedClientOutput = clientToRuntime.output(); + + clientInput = openedClientInput; + runtimeOutput = openedRuntimeOutput; + runtimeInput = openedRuntimeInput; + clientOutput = openedClientOutput; + + runtime = JsonRpcClient.fromStreams(runtimeInput, runtimeOutput); + runtime.registerMethodHandler("connect", (id, params) -> respond(runtime, id, + Map.of("ok", true, "protocolVersion", 3, "version", "installation-confirmation-test"))); + runtime.registerMethodHandler("runtime.shutdown", (id, params) -> respond(runtime, id, Map.of())); + runtime.registerMethodHandler("registerExtensionLaunchProvider", (id, params) -> { + extensionRegistrations.incrementAndGet(); + respond(runtime, id, Map.of()); + }); + + var options = new CopilotClientOptions().setConnection(RuntimeConnection.forInProcess()); + if (handler != null) { + options.setInstallationConfirmationHandler(handler); + } + client = new CopilotClient(options); + client.setInProcessTransportFactory( + ignored -> new CopilotClient.InProcessTransport(clientInput, clientOutput, () -> { + })); + client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception | Error error) { + closeQuietly(openedClientOutput); + closeQuietly(openedRuntimeInput); + closeQuietly(openedRuntimeOutput); + closeQuietly(openedClientInput); + throw error; + } + } + + CompletableFuture confirm(InstallationConfirmationRequest request) { + return runtime.invoke("installations.confirm", request, InstallationsConfirmResult.class); + } + + void cancel(Object id) throws IOException { + runtime.notify("$/cancelRequest", Map.of("id", id)); + } + + int extensionRegistrationCount() { + return extensionRegistrations.get(); + } + + void closeRuntimeConnection() { + runtime.close(); + } + + private static void respond(JsonRpcClient rpc, String id, Object result) { + if (id == null) { + return; + } + try { + rpc.sendResponse(parseRpcId(id), result); + } catch (IOException e) { + throw new IllegalStateException("Failed to send fake runtime response", e); + } + } + + private static Object parseRpcId(String id) { + try { + return Long.valueOf(id); + } catch (NumberFormatException ignored) { + return id; + } + } + + @Override + public void close() { + try { + client.forceStop().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + runtime.close(); + closeQuietly(clientOutput); + closeQuietly(runtimeInput); + closeQuietly(runtimeOutput); + closeQuietly(clientInput); + } + + private static void closeQuietly(Closeable stream) { + if (stream == null) { + return; + } + try { + stream.close(); + } catch (IOException ignored) { + } + } + } + + /** + * PipedInputStream treats a short-lived writer thread as a broken pipe, which + * makes these cross-thread JSON-RPC tests flaky with virtual-thread executors. + */ + private static final class LoopbackPipe { + private static final int EOF = -1; + + private final LinkedBlockingQueue bytes = new LinkedBlockingQueue<>(); + private final AtomicBoolean inputClosed = new AtomicBoolean(); + private final AtomicBoolean outputClosed = new AtomicBoolean(); + private final InputStream input = new Input(); + private final OutputStream output = new Output(); + + InputStream input() { + return input; + } + + OutputStream output() { + return output; + } + + private final class Input extends InputStream { + @Override + public int read() throws IOException { + if (inputClosed.get()) { + return EOF; + } + try { + int value = bytes.take(); + if (value == EOF) { + requeueEof(); + } + return value; + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while reading loopback pipe", error); + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException("b"); + } + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return 0; + } + + int first = read(); + if (first == EOF) { + return EOF; + } + b[off] = (byte) first; + int count = 1; + while (count < len) { + Integer value = bytes.poll(); + if (value == null) { + break; + } + if (value == EOF) { + requeueEof(); + break; + } + b[off + count] = (byte) (value & 0xff); + count++; + } + return count; + } + + @Override + public void close() { + if (inputClosed.compareAndSet(false, true)) { + signalEof(); + } + } + + /** Keeps end of stream visible to later reads. */ + private void requeueEof() throws IOException { + if (!bytes.offer(EOF)) { + throw new IOException("Could not preserve end of loopback pipe"); + } + } + } + + /** Enqueues end of stream, failing loudly rather than losing it. */ + private void signalEof() { + try { + bytes.put(EOF); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while closing loopback pipe", error); + } + } + + private final class Output extends OutputStream { + @Override + public void write(int b) throws IOException { + if (outputClosed.get()) { + throw new IOException("Pipe closed"); + } + if (inputClosed.get()) { + throw new IOException("Pipe input closed"); + } + try { + bytes.put(b & 0xff); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while writing loopback pipe", error); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException("b"); + } + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + for (int i = 0; i < len; i++) { + write(b[off + i]); + } + } + + @Override + public void close() { + if (outputClosed.compareAndSet(false, true)) { + signalEof(); + } + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/LegacyRequestCompatibilityTest.java b/java/sdk/src/test/java/com/github/copilot/LegacyRequestCompatibilityTest.java new file mode 100644 index 0000000000..16b09e9bf3 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/LegacyRequestCompatibilityTest.java @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * 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.Arrays; +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; +import com.github.copilot.generated.rpc.CatalogClientContract; +import com.github.copilot.generated.rpc.CatalogSearchParams; +import com.github.copilot.generated.rpc.CatalogSearchRequest; +import com.github.copilot.generated.rpc.McpPlanInstallParams; +import com.github.copilot.generated.rpc.McpPlanInstallRequest; +import com.github.copilot.generated.rpc.McpPlanScope; +import com.github.copilot.generated.rpc.RpcCaller; +import com.github.copilot.generated.rpc.ServerRpc; + +/** + * Existing callers of requests that gained fields keep their records, while new + * callers use the extensible request class on the same wire method. + */ +class LegacyRequestCompatibilityTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final class RecordingCaller implements RpcCaller { + record Call(String method, JsonNode params) { + } + + final List calls = new ArrayList<>(); + + @Override + public CompletableFuture invoke(String method, Object params, Class resultType) { + calls.add(new Call(method, MAPPER.valueToTree(params))); + return CompletableFuture.completedFuture(null); + } + } + + private static CatalogClientContract contract() { + return new CatalogClientContract(3L, List.of("mcp-install-planning")); + } + + private static Map candidate() { + return Map.of("kind", "candidate", "candidateHandle", "candidate", "searchId", "search"); + } + + @Test + void existingRecordsKeepTheirLegacyComponents() { + assertEquals(List.of("contract", "source", "scope"), + Arrays.stream(McpPlanInstallParams.class.getRecordComponents()).map(component -> component.getName()) + .toList()); + assertEquals(List.of("contract", "query", "limit", "kinds", "page"), + Arrays.stream(CatalogSearchParams.class.getRecordComponents()).map(component -> component.getName()) + .toList()); + } + + @Test + void existingRecordCallsOmitTheAddedFields() { + var caller = new RecordingCaller(); + var server = new ServerRpc(caller); + + server.mcp.planInstall(new McpPlanInstallParams(contract(), candidate(), McpPlanScope.USER)); + server.catalog.search(new CatalogSearchParams(contract(), "catalogue query", 4L, null, null)); + + assertEquals(2, caller.calls.size()); + var plan = caller.calls.get(0); + assertEquals("mcp.planInstall", plan.method()); + assertEquals("user", plan.params().path("scope").asText()); + assertFalse(plan.params().has("policySessionId")); + var search = caller.calls.get(1); + assertEquals("catalog.search", search.method()); + assertEquals(4, search.params().path("limit").asInt()); + assertFalse(search.params().has("policySessionId")); + } + + @Test + void extensibleRequestsUseTheSameWireMethodWithFlatParams() { + var caller = new RecordingCaller(); + var server = new ServerRpc(caller); + + server.mcp.planInstall( + new McpPlanInstallRequest(contract(), candidate()).setScope(McpPlanScope.USER).setPolicySessionId("s")); + server.catalog.search(new CatalogSearchRequest(contract(), "catalogue query").setPolicySessionId("s")); + + var plan = caller.calls.get(0); + assertEquals("mcp.planInstall", plan.method()); + assertEquals(MAPPER.valueToTree(Map.of("contract", + Map.of("protocolVersion", 3L, "requiredCapabilities", List.of("mcp-install-planning")), "source", + candidate(), "scope", "user", "policySessionId", "s")), plan.params()); + var search = caller.calls.get(1); + assertEquals("catalog.search", search.method()); + assertEquals("catalogue query", search.params().path("query").asText()); + assertEquals("s", search.params().path("policySessionId").asText()); + assertFalse(search.params().has("limit")); + } + + @Test + void extensibleRequestsRequireTheirMandatoryInputs() { + var server = new ServerRpc(new RecordingCaller()); + + assertThrows(NullPointerException.class, () -> new McpPlanInstallRequest(null, candidate())); + assertThrows(NullPointerException.class, () -> new McpPlanInstallRequest(contract(), null)); + assertThrows(NullPointerException.class, () -> new CatalogSearchRequest(contract(), null)); + assertThrows(NullPointerException.class, () -> server.mcp.planInstall((McpPlanInstallRequest) null)); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java b/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java index d6341b26c5..5c8a1a137d 100644 --- a/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java @@ -24,7 +24,8 @@ class ManagedSettingsTest { void forwardsManagedSettingsOnCreateAndResume() throws Exception { var permissions = new ManagedSettingsPermissions() .setDisableBypassPermissionsMode(DisableBypassPermissionsModes.DISABLE).setDeny(List.of("Shell(rm *)")) - .setAsk(List.of("Domain(publish.example)")).setAllow(List.of("Read(**)")); + .setAsk(List.of("Domain(publish.example)")).setAllow(List.of("Read(**)")) + .setLimitTo(List.of("Domain(github.com)")); var managedSettings = new ManagedSettings().setPermissions(permissions); var create = SessionRequestBuilder.buildCreateRequest( @@ -39,6 +40,7 @@ void forwardsManagedSettingsOnCreateAndResume() throws Exception { assertTrue(json.contains("\"enableManagedSettings\":true")); assertTrue(json.contains("\"managedSettings\":{\"permissions\"")); assertTrue(json.contains("\"disableBypassPermissionsMode\":\"disable\"")); + assertTrue(json.contains("\"limitTo\":[\"Domain(github.com)\"]")); } @Test @@ -55,7 +57,8 @@ void preservesExplicitEmptyPermissionArrays() throws Exception { // absent (null) list imposes no such restriction. Jackson NON_NULL must // emit an explicit empty array as `[]` and omit null fields, so the two // remain distinguishable on the wire. - var permissions = new ManagedSettingsPermissions().setDeny(List.of()).setAsk(List.of()).setAllow(List.of()); + var permissions = new ManagedSettingsPermissions().setDeny(List.of()).setAsk(List.of()).setAllow(List.of()) + .setLimitTo(List.of()); var managedSettings = new ManagedSettings().setPermissions(permissions); var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setManagedSettings(managedSettings), "managed-empty"); @@ -64,6 +67,7 @@ void preservesExplicitEmptyPermissionArrays() throws Exception { assertTrue(json.contains("\"deny\":[]"), json); assertTrue(json.contains("\"ask\":[]"), json); assertTrue(json.contains("\"allow\":[]"), json); + assertTrue(json.contains("\"limitTo\":[]"), json); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/McpServerLegacyConstructorCompatibilityTest.java b/java/sdk/src/test/java/com/github/copilot/McpServerLegacyConstructorCompatibilityTest.java new file mode 100644 index 0000000000..7fe34688c2 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/McpServerLegacyConstructorCompatibilityTest.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.rpc.McpServer; +import com.github.copilot.generated.rpc.McpServerMetadata; +import com.github.copilot.generated.rpc.McpServerSource; +import com.github.copilot.generated.rpc.McpServerStatus; + +class McpServerLegacyConstructorCompatibilityTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void releasedConstructorOmitsTheOwnedMarker() { + var metadata = new McpServerMetadata("use it"); + var server = new McpServer("server", McpServerStatus.CONNECTED, McpServerSource.USER, "plugin", "1.0.0", + "Server", "failed", metadata); + + assertEquals("server", server.name()); + assertEquals(metadata, server.serverMetadata()); + assertNull(server.owned()); + assertFalse(MAPPER.valueToTree(server).has("owned")); + } + + @Test + void ownedMarkerDeserialisesThroughTheCanonicalRecord() throws Exception { + var server = MAPPER.readValue(""" + {"name":"owned","status":"stopped","owned":{"installationId":"installation"}} + """, McpServer.class); + + assertEquals("installation", server.owned().installationId()); + JsonNode json = MAPPER.valueToTree(server); + assertEquals("installation", json.at("/owned/installationId").asText()); + } +} 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/OwnedMcpLegacyRequestCompatibilityTest.java b/java/sdk/src/test/java/com/github/copilot/OwnedMcpLegacyRequestCompatibilityTest.java new file mode 100644 index 0000000000..5b59eaeae7 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/OwnedMcpLegacyRequestCompatibilityTest.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.rpc.RpcCaller; +import com.github.copilot.generated.rpc.SessionMcpEnableParams; +import com.github.copilot.generated.rpc.SessionMcpEnableRequest; +import com.github.copilot.generated.rpc.SessionMcpOauthLoginParams; +import com.github.copilot.generated.rpc.SessionMcpOauthLoginRequest; +import com.github.copilot.generated.rpc.SessionRpc; + +class OwnedMcpLegacyRequestCompatibilityTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final class RecordingCaller implements RpcCaller { + record Call(String method, JsonNode params) { + } + + final List calls = new ArrayList<>(); + + @Override + public CompletableFuture invoke(String method, Object params, Class resultType) { + calls.add(new Call(method, MAPPER.valueToTree(params))); + return CompletableFuture.completedFuture(null); + } + } + + @Test + void existingRecordsKeepTheirReleasedComponents() { + assertEquals(List.of("sessionId", "serverName"), + Arrays.stream(SessionMcpEnableParams.class.getRecordComponents()).map(c -> c.getName()).toList()); + assertEquals( + List.of("sessionId", "serverName", "forceReauth", "clientName", "callbackSuccessMessage", "clientId", + "clientSecret", "publicClient", "grantType"), + Arrays.stream(SessionMcpOauthLoginParams.class.getRecordComponents()).map(c -> c.getName()).toList()); + } + + @Test + void existingRecordCallsOmitOwnedIdentity() { + var caller = new RecordingCaller(); + var session = new SessionRpc(caller, "owned-session"); + + session.mcp.enable(new SessionMcpEnableParams(null, "manual")); + session.mcp.oauth + .login(new SessionMcpOauthLoginParams(null, "manual", null, null, null, null, null, null, null)); + + assertEquals("session.mcp.enable", caller.calls.get(0).method()); + assertFalse(caller.calls.get(0).params().has("expectedInstallationId")); + assertEquals("session.mcp.oauth.login", caller.calls.get(1).method()); + assertFalse(caller.calls.get(1).params().has("expectedInstallationId")); + assertFalse(caller.calls.get(1).params().has("loginId")); + } + + @Test + void ownedIdentityUsesTheSameWireMethodWithFlatParams() { + var caller = new RecordingCaller(); + var session = new SessionRpc(caller, "owned-session"); + var installation = "a".repeat(32); + + session.mcp.enable(new SessionMcpEnableRequest("owned").setExpectedInstallationId(installation)); + session.mcp.oauth.login(new SessionMcpOauthLoginRequest("owned").setExpectedInstallationId(installation) + .setLoginId("b".repeat(32))); + + var enable = caller.calls.get(0); + assertEquals("session.mcp.enable", enable.method()); + assertEquals("owned-session", enable.params().path("sessionId").asText()); + assertEquals("owned", enable.params().path("serverName").asText()); + assertEquals(installation, enable.params().path("expectedInstallationId").asText()); + var login = caller.calls.get(1); + assertEquals("session.mcp.oauth.login", login.method()); + assertEquals("owned-session", login.params().path("sessionId").asText()); + assertEquals(installation, login.params().path("expectedInstallationId").asText()); + assertEquals("b".repeat(32), login.params().path("loginId").asText()); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/PackagedConsumerIT.java b/java/sdk/src/test/java/com/github/copilot/PackagedConsumerIT.java new file mode 100644 index 0000000000..29cb03f487 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/PackagedConsumerIT.java @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import javax.tools.ToolProvider; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PackagedConsumerIT { + + @Test + void runsStandaloneConsumerWithManifestClasspath(@TempDir Path directory) throws Exception { + Path build = Path.of(System.getProperty("project.build.directory")); + Path library = Files.createDirectory(directory.resolve("lib")); + var dependencies = new ArrayList(); + dependencies.add(build.resolve(System.getProperty("project.build.finalName") + ".jar")); + try (var jars = Files.list(build.resolve("consumer-dependencies"))) { + dependencies.addAll(jars.filter(path -> path.toString().endsWith(".jar")).sorted().toList()); + } + for (Path dependency : dependencies) { + Files.copy(dependency, library.resolve(dependency.getFileName())); + } + + Path source = directory.resolve("Consumer.java"); + Files.writeString(source, """ + import com.github.copilot.CopilotClient; + import com.github.copilot.ConnectionState; + import com.github.copilot.rpc.MessageOptions; + import com.fasterxml.jackson.databind.ObjectMapper; + import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + + public class Consumer { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + var mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + var json = mapper.writeValueAsString(new MessageOptions().setPrompt("consumer")); + if (!mapper.readTree(json).get("prompt").asText().equals("consumer") + || client.getState() != ConnectionState.DISCONNECTED) { + throw new AssertionError("Packaged SDK consumer failed"); + } + } + System.out.println("consumer-ok"); + } + } + """); + + String classpath = String.join(File.pathSeparator, + dependencies.stream().map(path -> library.resolve(path.getFileName()).toString()).toList()); + var diagnostics = new ByteArrayOutputStream(); + int compilation = ToolProvider.getSystemJavaCompiler().run(null, diagnostics, diagnostics, "--release", "17", + "-proc:none", "-classpath", classpath, source.toString()); + assertEquals(0, compilation, diagnostics.toString(StandardCharsets.UTF_8)); + + var manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "Consumer"); + manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, + String.join(" ", dependencies.stream().map(path -> "lib/" + path.getFileName()).toList())); + Path consumerJar = directory.resolve("consumer.jar"); + try (var jar = new JarOutputStream(Files.newOutputStream(consumerJar), manifest)) { + jar.putNextEntry(new JarEntry("Consumer.class")); + Files.copy(directory.resolve("Consumer.class"), jar); + jar.closeEntry(); + } + Files.delete(directory.resolve("Consumer.class")); + + Path java = Path.of(System.getProperty("java.home"), "bin", + System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java"); + Path output = directory.resolve("output.log"); + ProcessBuilder builder = new ProcessBuilder(java.toString(), "-jar", consumerJar.toString()) + .directory(directory.toFile()).redirectErrorStream(true).redirectOutput(output.toFile()); + builder.environment().remove("COPILOT_SDK_DEFAULT_CONNECTION"); + Process process = builder.start(); + try { + boolean exited = process.waitFor(30, TimeUnit.SECONDS); + String text = Files.readString(output); + assertTrue(exited, "Consumer JVM did not exit. Output:\n" + text); + assertEquals(0, process.exitValue(), text); + assertTrue(text.contains("consumer-ok"), text); + } finally { + if (process.isAlive()) { + process.destroyForcibly().waitFor(30, TimeUnit.SECONDS); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultKindTest.java b/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultKindTest.java index 0bd08f47d7..9da404f379 100644 --- a/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultKindTest.java +++ b/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultKindTest.java @@ -23,6 +23,8 @@ public class PermissionRequestResultKindTest { @Test void wellKnownKinds_haveExpectedValues() { assertEquals("approve-once", PermissionRequestResultKind.APPROVED.getValue()); + assertEquals("approve-read-only-for-session", + PermissionRequestResultKind.APPROVE_READ_ONLY_FOR_SESSION.getValue()); assertEquals("reject", PermissionRequestResultKind.REJECTED.getValue()); assertEquals("user-not-available", PermissionRequestResultKind.USER_NOT_AVAILABLE.getValue()); assertEquals("no-result", PermissionRequestResultKind.NO_RESULT.getValue()); @@ -115,8 +117,8 @@ void permissionRequestResult_setKindWithString_backwardCompatible() { void jsonRoundTrip_allWellKnownKinds() throws Exception { ObjectMapper mapper = new ObjectMapper(); PermissionRequestResultKind[] kinds = {PermissionRequestResultKind.APPROVED, - PermissionRequestResultKind.REJECTED, PermissionRequestResultKind.USER_NOT_AVAILABLE, - PermissionRequestResultKind.NO_RESULT,}; + PermissionRequestResultKind.APPROVE_READ_ONLY_FOR_SESSION, PermissionRequestResultKind.REJECTED, + PermissionRequestResultKind.USER_NOT_AVAILABLE, PermissionRequestResultKind.NO_RESULT,}; for (PermissionRequestResultKind kind : kinds) { var result = new PermissionRequestResult().setKind(kind); String json = mapper.writeValueAsString(result); diff --git a/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultTest.java b/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultTest.java index c1ca9191b0..30dd266971 100644 --- a/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultTest.java +++ b/java/sdk/src/test/java/com/github/copilot/PermissionRequestResultTest.java @@ -6,6 +6,8 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.List; + import org.junit.jupiter.api.Test; import com.github.copilot.generated.PermissionRequestedEvent; @@ -32,6 +34,18 @@ void testApproveOnce() { assertNull(result.getFeedback()); } + @Test + void testApproveReadOnlyForSession() throws Exception { + var result = PermissionRequestResult.approveReadOnlyForSession(List.of("/outside/reference")); + var json = MAPPER.readTree(MAPPER.writeValueAsString(result)); + + assertEquals("approve-read-only-for-session", result.getKind()); + assertEquals(List.of("/outside/reference"), result.getDirectories()); + assertEquals("approve-read-only-for-session", json.get("kind").asText()); + assertEquals("/outside/reference", json.get("directories").get(0).asText()); + assertFalse(json.has("locationKey")); + } + @Test void testRejectWithFeedback() { var result = PermissionRequestResult.reject("Not allowed"); @@ -81,11 +95,13 @@ void testPermissionRequestExposesManagedApprovalRequired() throws Exception { { "kind": "read", "path": "/workspace/file.txt", - "managedApprovalRequired": true + "managedApprovalRequired": true, + "readOnlyDirectories": ["/outside/reference"] } """, PermissionRequest.class); assertTrue(request.getManagedApprovalRequired()); + assertEquals(List.of("/outside/reference"), request.getReadOnlyDirectories()); } @Test 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/RpcHandlerDispatcherTest.java b/java/sdk/src/test/java/com/github/copilot/RpcHandlerDispatcherTest.java index 44bff36444..a92a4b561d 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcHandlerDispatcherTest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcHandlerDispatcherTest.java @@ -16,8 +16,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -43,6 +48,7 @@ * identified by JaCoCo: unknown sessions, missing fields, error paths, and edge * cases for each handler method. */ +@AllowCopilotExperimental class RpcHandlerDispatcherTest { private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); @@ -509,6 +515,213 @@ void userInputRequestHandlerFails() throws Exception { assertEquals(-32603, response.get("error").get("code").asInt()); } + // ===== skillProvider.* tests ===== + + @Test + void skillProviderListAndReadSuccess() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider( + () -> CompletableFuture.completedFuture( + List.of(new SkillProviderDescriptor("docs", "Documentation", null, null, "topic"))), + name -> CompletableFuture.completedFuture("# " + name))); + + ObjectNode listParams = MAPPER.createObjectNode(); + listParams.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "60", listParams); + + JsonNode listResponse = readResponse(); + JsonNode descriptor = listResponse.at("/result/skills/0"); + assertEquals("docs", descriptor.path("name").asText()); + assertEquals("Documentation", descriptor.path("description").asText()); + assertEquals("topic", descriptor.path("argumentHint").asText()); + assertFalse(descriptor.has("userInvocable")); + assertFalse(descriptor.has("disableModelInvocation")); + + ObjectNode readParams = MAPPER.createObjectNode(); + readParams.put("sessionId", "s1"); + readParams.put("name", "docs"); + invokeHandler("skillProvider.read", "61", readParams); + + JsonNode readResponse = readResponse(); + assertEquals("# docs", readResponse.at("/result/markdown").asText()); + } + + @Test + void skillProviderNullListReturnsEmptyList() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(null), + name -> CompletableFuture.completedFuture("# " + name))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "62", params); + + JsonNode response = readResponse(); + assertTrue(response.at("/result/skills").isArray()); + assertEquals(0, response.at("/result/skills").size()); + } + + @Test + void skillProviderReadNullReturnsNullMarkdown() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), + name -> CompletableFuture.completedFuture(null))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("name", "missing"); + invokeHandler("skillProvider.read", "63", params); + + JsonNode response = readResponse(); + assertNull(response.get("error")); + assertTrue(response.at("/result").has("markdown")); + assertTrue(response.at("/result/markdown").isNull()); + } + + @Test + void skillProviderFailuresUseStableMessagesWithoutData() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider( + () -> CompletableFuture.failedFuture(new IllegalStateException("secret list failure")), name -> { + throw new IllegalStateException("secret read failure"); + })); + + ObjectNode listParams = MAPPER.createObjectNode(); + listParams.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "64", listParams); + JsonNode listResponse = readResponse(); + assertEquals(-32603, listResponse.at("/error/code").asInt()); + assertEquals("Skill provider listSkills failed", listResponse.at("/error/message").asText()); + assertFalse(listResponse.path("error").has("data")); + + ObjectNode readParams = MAPPER.createObjectNode(); + readParams.put("sessionId", "s1"); + readParams.put("name", "docs"); + invokeHandler("skillProvider.read", "65", readParams); + JsonNode readResponse = readResponse(); + assertEquals(-32603, readResponse.at("/error/code").asInt()); + assertEquals("Skill provider readSkill failed", readResponse.at("/error/message").asText()); + assertFalse(readResponse.path("error").has("data")); + } + + @Test + void skillProviderErrorsForUnknownMissingOrClearedProviderHaveNoData() throws Exception { + ObjectNode unknownParams = MAPPER.createObjectNode(); + unknownParams.put("sessionId", "missing-session"); + invokeHandler("skillProvider.list", "66", unknownParams); + JsonNode unknownResponse = readResponse(); + assertEquals(-32603, unknownResponse.at("/error/code").asInt()); + assertEquals("No skill provider for session: missing-session", unknownResponse.at("/error/message").asText()); + assertFalse(unknownResponse.path("error").has("data")); + + createSession("s1"); + ObjectNode noProviderParams = MAPPER.createObjectNode(); + noProviderParams.put("sessionId", "s1"); + noProviderParams.put("name", "docs"); + invokeHandler("skillProvider.read", "67", noProviderParams); + JsonNode noProviderResponse = readResponse(); + assertEquals(-32603, noProviderResponse.at("/error/code").asInt()); + assertEquals("No skill provider for session: s1", noProviderResponse.at("/error/message").asText()); + assertFalse(noProviderResponse.path("error").has("data")); + + CopilotSession session = sessions.get("s1"); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), + name -> CompletableFuture.completedFuture("# " + name))); + session.clearSkillProvider(); + invokeHandler("skillProvider.list", "68", noProviderParams); + JsonNode clearedResponse = readResponse(); + assertEquals(-32603, clearedResponse.at("/error/code").asInt()); + assertEquals("No skill provider for session: s1", clearedResponse.at("/error/message").asText()); + assertFalse(clearedResponse.path("error").has("data")); + } + + @Test + void skillProviderCallIsCancelledByCancelRequest() throws Exception { + CopilotSession session = createSession("s1"); + var entered = new CountDownLatch(1); + var pending = new CompletableFuture>(); + session.registerSkillProvider(skillProvider(() -> { + entered.countDown(); + return pending; + }, name -> CompletableFuture.completedFuture(null))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "69", params); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + dispatcher.handleCancelRequest(MAPPER.createObjectNode().put("id", 69)); + + JsonNode response = readResponse(); + assertEquals(69, response.path("id").asInt()); + assertEquals(-32800, response.at("/error/code").asInt()); + assertTrue(pending.isCancelled()); + } + + @Test + void skillProviderCallCancelledBeforeDispatchIsNotStarted() throws Exception { + CopilotSession session = createSession("s1"); + var calls = new AtomicInteger(); + session.registerSkillProvider(skillProvider(() -> { + calls.incrementAndGet(); + return CompletableFuture.completedFuture(List.of()); + }, name -> CompletableFuture.completedFuture(null))); + var blockedExecutor = new CompletableFuture(); + var cancellingDispatcher = new RpcHandlerDispatcher(sessions, lifecycleEvents::add, + task -> blockedExecutor.thenRun(task), gitHubTokenProviders); + cancellingDispatcher.registerHandlers(rpc); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "70", params); + cancellingDispatcher.handleCancelRequest(MAPPER.createObjectNode().put("id", 70)); + blockedExecutor.complete(null); + + assertEquals(-32800, readResponse().at("/error/code").asInt()); + assertEquals(0, calls.get()); + } + + @Test + void skillProviderCallsAreCancelledWhenTheConnectionCloses() throws Exception { + CopilotSession session = createSession("s1"); + var entered = new CountDownLatch(1); + var pending = new CompletableFuture(); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), name -> { + entered.countDown(); + return pending; + })); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("name", "docs"); + invokeHandler("skillProvider.read", "71", params); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + dispatcher.cancelSkillProviderCalls(); + + assertEquals(-32800, readResponse().at("/error/code").asInt()); + assertTrue(pending.isCancelled()); + } + + @Test + void cancelRequestForAnotherRequestLeavesSkillProviderCallRunning() throws Exception { + CopilotSession session = createSession("s1"); + var entered = new CountDownLatch(1); + var pending = new CompletableFuture(); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), name -> { + entered.countDown(); + return pending; + })); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("name", "docs"); + invokeHandler("skillProvider.read", "72", params); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + dispatcher.handleCancelRequest(MAPPER.createObjectNode().put("id", 73)); + pending.complete("# docs"); + + assertEquals("# docs", readResponse().at("/result/markdown").asText()); + } + // ===== hooks.invoke tests ===== @Test @@ -649,4 +862,19 @@ void systemMessageTransformWithKnownSessionNoCallbacks() throws Exception { assertNotNull(resultSections); assertEquals("Original content", resultSections.get("identity").get("content").asText()); } + + private static SkillProvider skillProvider(Supplier>> listSkills, + Function> readSkill) { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return listSkills.get(); + } + + @Override + public CompletableFuture readSkill(String name) { + return readSkill.apply(name); + } + }; + } } 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 43f07d2315..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/RpcSurfaceParityE2ETest.java +++ /dev/null @@ -1,1178 +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 = 420; - private static final String EXPECTED_RPC_SIGNATURE_SHA256 = "b060cc39689fb59c658ddc1d073e23c3fc16f08abf719e9102a7f54b26a99166"; - private static final Map EXPECTED_METHODS_BY_DECLARING_TYPE = Map.ofEntries( - Map.entry("RpcCaller", 2), Map.entry("ServerAccountApi", 6), Map.entry("ServerAgentRegistryApi", 1), - Map.entry("ServerAgentsApi", 2), Map.entry("ServerCatalogApi", 2), Map.entry("ServerCommandsApi", 1), - Map.entry("ServerExtensionsApi", 3), Map.entry("ServerHooksApi", 1), Map.entry("ServerInstructionsApi", 2), - Map.entry("ServerLlmInferenceApi", 3), Map.entry("ServerManagedSettingsApi", 2), - Map.entry("ServerMcpApi", 2), Map.entry("ServerMcpConfigApi", 7), 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", 2), Map.entry("ServerSkillsConfigApi", 2), - Map.entry("ServerToolsApi", 1), Map.entry("ServerUserSettingsApi", 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("SessionEventLogApi", 4), Map.entry("SessionExtensionsApi", 5), - Map.entry("SessionFactoryApi", 13), Map.entry("SessionFactoryJournalApi", 2), - Map.entry("SessionFleetApi", 1), Map.entry("SessionGitHubAuthApi", 10), Map.entry("SessionHistoryApi", 10), - Map.entry("SessionInstructionsApi", 2), Map.entry("SessionLimitPredictionApi", 2), - Map.entry("SessionManagedSettingsApi", 1), Map.entry("SessionLspApi", 1), Map.entry("SessionMcpApi", 18), - Map.entry("SessionDiagnosticsApi", 2), Map.entry("SessionMcpAppsApi", 6), - Map.entry("SessionMcpHeadersApi", 1), Map.entry("SessionMcpOauthApi", 5), - 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", 2), 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.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(104, 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.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.factory.cancel(params(SessionFactoryCancelParams.class, "{}")); - rpc.factory.getRunDetail(params(SessionFactoryGetRunDetailParams.class, "{}")); - rpc.factory.getRunProgress(params(SessionFactoryGetRunProgressParams.class, "{}")); - rpc.factory.listRuns(params(SessionFactoryListRunsParams.class, "{}")); - rpc.factory.pauseAtCheckpoint(params(SessionFactoryPauseAtCheckpointParams.class, "{}")); - rpc.factory.resumeFromTool(params(SessionFactoryResumeFromToolParams.class, "{}")); - rpc.factory.runFromTool(params(SessionFactoryRunFromToolParams.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()); - - 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.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 factoryAndMcpRpcsSerializeRequestsAndProjectStateTransitions() 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.factory.run(params(SessionFactoryRunParams.class, """ - {"name":"rpc-factory","args":{"input":42}, - "options":{"limits":{"maxAiCredits":2.5,"maxConcurrentSubagents":2, - "maxTotalSubagents":4,"timeoutSeconds":30}, - "logPhaseNames":true,"notifyOnComplete":false}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("factory-run-1", run.runId()); - assertEquals(FactoryRunStatus.RUNNING, run.status()); - assertEquals(1L, run.attempt()); - - var resumed = rpc.factory.resume(params(SessionFactoryResumeParams.class, """ - {"runId":"factory-run-1","limits":{"maxTotalSubagents":8}, - "notifyOnComplete":true,"logPhaseNames":false} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("rpc-factory", resumed.factoryName()); - assertEquals(2L, resumed.run().attempt()); - - assertEquals(FactoryRunStatus.RUNNING, - rpc.factory.getRun(new SessionFactoryGetRunParams(null, "factory-run-1")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).status()); - assertEquals(FactoryRunStatus.PAUSED, - rpc.factory.pause(new SessionFactoryPauseParams(null, "factory-run-1")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).status()); - - rpc.factory.log(params(SessionFactoryLogParams.class, """ - {"runId":"factory-run-1","executionToken":"execution-token-1", - "lines":[{"kind":"log","seq":7,"text":"Factory progress"}]} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var agent = rpc.factory.agent(params(SessionFactoryAgentParams.class, """ - {"runId":"factory-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.factory.journal.get( - new SessionFactoryJournalGetParams(null, "factory-run-1", "execution-token-1", "checkpoint")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(journal.hit()); - assertEquals(7, ((Map) journal.resultJson()).get("checkpoint")); - rpc.factory.journal.put(params(SessionFactoryJournalPutParams.class, """ - {"runId":"factory-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.factory.run", "session.factory.resume", "session.factory.getRun", - "session.factory.pause", "session.factory.log", "session.factory.agent", - "session.factory.journal.get", "session.factory.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.factory.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.abort" -> json(""" - {"success":true,"error":null} - """); - case "session.interruptMainTurn" -> json(""" - {"interrupted":true} - """); - case "session.log" -> json(""" - {"eventId":"11111111-2222-3333-4444-555555555555"} - """); - case "session.factory.run", "session.factory.getRun" -> json(""" - {"runId":"factory-run-1","status":"running","attempt":1, - "result":{"value":"running"},"snapshot":{"step":1}} - """); - case "session.factory.resume" -> json(""" - {"factoryName":"rpc-factory","run":{"runId":"factory-run-1","status":"running", - "attempt":2,"snapshot":{"step":3}}} - """); - case "session.factory.pause" -> json(""" - {"runId":"factory-run-1","status":"paused","attempt":1, - "reason":"caller requested pause","snapshot":{"step":2}} - """); - case "session.factory.agent" -> json(""" - {"result":{"answer":"agent-result"}} - """); - case "session.factory.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()); - } - 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 new file mode 100644 index 0000000000..43b286da48 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/RuntimeHostIT.java @@ -0,0 +1,251 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +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; +import com.github.copilot.rpc.SessionHooks; +import com.github.copilot.rpc.SystemMessageConfig; +import com.github.copilot.rpc.ToolDefinition; +import java.util.HashMap; +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.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@AllowCopilotExperimental +@Timeout(120) +@EnabledIfEnvironmentVariable(named = "COPILOT_RUNTIME_HOST_E2E", matches = "1") +class RuntimeHostIT { + private static final String TOOL_PROMPT = "Use the magic_number tool with seed 'hello' and tell me the result"; + private static final String COMPOSED_PROMPT = "Call magic_number with seed 'hello' and client_echo with text 'ping', then report both results"; + private static final String MARKER = "APPLICATION_OWNED_AHP_PROMPT"; + + public record MagicInput(String seed) { + } + + private static T await(CompletableFuture future) throws Exception { + return future.get(30, TimeUnit.SECONDS); + } + + private static final class Application { + final E2ETestContext context; + final CopilotClient client; + volatile CopilotSession session; + final AtomicInteger creates = new AtomicInteger(); + final AtomicInteger resumes = new AtomicInteger(); + final AtomicInteger toolCalls = new AtomicInteger(); + final AtomicInteger exits = new AtomicInteger(); + final CopyOnWriteArrayList hookSessions = new CopyOnWriteArrayList<>(); + final CopyOnWriteArrayList releases = new CopyOnWriteArrayList<>(); + final CompletableFuture released = new CompletableFuture<>(); + final CompletableFuture exited = new CompletableFuture<>(); + + Application(E2ETestContext context, CopilotClient client) { + this.context = context; + this.client = client; + } + + List tools() { + return List + .of(ToolDefinition.create( + "magic_number", "Returns a magic number", Map.of("type", "object", "properties", + Map.of("seed", Map.of("type", "string")), "required", List.of("seed")), + invocation -> { + assertEquals("hello", invocation.getArgumentsAs(MagicInput.class).seed()); + toolCalls.incrementAndGet(); + return CompletableFuture.completedFuture("MAGIC_hello_42"); + })); + } + + SessionHooks hooks() { + return new SessionHooks().setOnPreToolUse((input, invocation) -> { + hookSessions.add(invocation.getSessionId()); + return CompletableFuture.completedFuture(null); + }); + } + + SystemMessageConfig prompt() { + return new SystemMessageConfig().setMode(SystemMessageMode.APPEND).setContent(MARKER); + } + + SessionConfig configure(SessionConfig config) { + return config.setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(tools()).setHooks(hooks()) + .setSystemMessage(prompt()); + } + + ResumeSessionConfig configure(ResumeSessionConfig config) { + return config.setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(tools()).setHooks(hooks()) + .setSystemMessage(prompt()); + } + + AhpHostOptions options() { + 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 { + assertEquals(1, toolCalls.get()); + assertTrue(hookSessions.contains(sessionId)); + JsonNode exchanges = JsonRpcClient.getObjectMapper().valueToTree(context.getExchanges()); + assertTrue(exchanges.toString().contains(MARKER)); + assertTrue(exchanges.toString().contains("\"name\":\"magic_number\"")); + } + + void assertReleased() throws Exception { + await(released); + assertEquals(1, releases.size()); + assertSame(session, releases.get(0)); + } + } + + 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) { + command.put("token", host.getToken()); + } + if (clientId != null) { + command.put("clientId", clientId); + } + return ahp.request(command).get("clientId").asText(); + } + + private static void stopped(AhpTestClient ahp, AhpHost host, String clientId) throws Exception { + ahp.request(Map.of("op", "stopped", "clientId", clientId, "url", host.getUrl())); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void createsOrPublishesExactApplicationSession(boolean publish) throws Exception { + try (var context = E2ETestContext.create()) { + context.configureForTest("multi_client", "both_clients_see_tool_request_and_completion_events"); + try (var owner = context.createClient(); var ahp = new AhpTestClient(context.getRepoRoot())) { + var app = new Application(context, owner); + if (publish) { + app.session = await(owner.createSession( + app.configure(new SessionConfig().setWorkingDirectory(context.getWorkDir().toString())))); + } + try (var host = await(owner.startAhpHost(app.options()))) { + assertNull(host.getPid()); + String clientId = connect(ahp, host, null); + String sessionId; + if (publish) { + sessionId = app.session.getSessionId(); + var publication = await(host.publishSession(sessionId)); + assertEquals(sessionId, publication.sessionId()); + assertEquals("ahp-session:/" + sessionId, publication.sessionUri()); + ahp.request(Map.of("op", "attach", "clientId", clientId, "sessionId", sessionId)); + } else { + sessionId = ahp.request(Map.of("op", "create", "clientId", clientId, "workDir", + context.getWorkDir().toString())).get("sessionId").asText(); + assertEquals(sessionId, app.session.getSessionId()); + assertEquals(1, app.creates.get()); + } + var response = ahp.request( + Map.of("op", "turn", "clientId", clientId, "sessionId", sessionId, "prompt", TOOL_PROMPT)); + assertTrue(response.get("text").asText().contains("MAGIC_hello_42")); + app.assertCallbacks(sessionId); + await(CompletableFuture.allOf(host.dispose(), host.dispose())); + stopped(ahp, host, clientId); + if (publish) { + assertEquals(0, app.creates.get()); + assertEquals(0, app.resumes.get()); + assertTrue(app.releases.isEmpty()); + } else { + app.assertReleased(); + } + await(app.exited); + assertEquals(1, app.exits.get()); + assertFalse(await(app.session.getMessages()).isEmpty()); + assertEquals("pong: still alive", await(owner.ping("still alive")).message()); + } + } + } + } + + @Test + void resumesAfterRuntimeRestartAndComposesTools() throws Exception { + try (var context = E2ETestContext.create(); var ahp = new AhpTestClient(context.getRepoRoot())) { + context.configureForTest("runtime_host", "app_resume_callback_composes_tools_after_history"); + String clientId; + String sessionId; + CopilotSession original; + try (var firstOwner = context.createClient()) { + var app = new Application(context, firstOwner); + try (var host = await(firstOwner.startAhpHost(app.options()))) { + clientId = connect(ahp, host, null); + sessionId = ahp.request(Map.of("op", "create", "clientId", clientId, "workDir", + context.getWorkDir().toString(), "clientTools", true)).get("sessionId").asText(); + original = app.session; + assertTrue(ahp.request(Map.of("op", "turn", "clientId", clientId, "sessionId", sessionId, "prompt", + "What is 2+2?")).get("text").asText().contains("4")); + await(host.dispose()); + stopped(ahp, host, clientId); + app.assertReleased(); + } + ahp.request(Map.of("op", "close", "clientId", clientId)); + } + try (var resumedOwner = context.createClient()) { + var app = new Application(context, resumedOwner); + try (var host = await(resumedOwner.startAhpHost(app.options()))) { + assertNull(host.getPid()); + connect(ahp, host, clientId); + var attached = ahp.request( + Map.of("op", "attach", "clientId", clientId, "sessionId", sessionId, "clientTools", true)); + assertEquals(1, attached.get("history").size()); + assertEquals("What is 2+2?", attached.get("history").get(0).get("message").get("text").asText()); + assertEquals(0, app.creates.get()); + assertEquals(1, app.resumes.get()); + assertEquals(sessionId, app.session.getSessionId()); + assertNotSame(original, app.session); + var response = ahp.request(Map.of("op", "turn", "clientId", clientId, "sessionId", sessionId, + "prompt", COMPOSED_PROMPT, "clientTools", true)); + assertTrue(response.get("text").asText().contains("MAGIC_hello_42")); + assertTrue(response.get("text").asText().contains("CLIENT_ECHO_ping")); + assertEquals(1, response.get("clientToolCalls").asInt()); + app.assertCallbacks(sessionId); + await(host.dispose()); + stopped(ahp, host, clientId); + app.assertReleased(); + assertFalse(await(app.session.getMessages()).isEmpty()); + } + } + } + } +} 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 74d178e076..978346e486 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 // ========================================================================= @@ -60,6 +111,7 @@ void testBuildCreateRequestNullConfig() { assertNull(request.getModel()); assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); + assertNull(request.getHasSkillProvider()); } @Test @@ -111,6 +163,36 @@ void askUserVariantDefaultsToOmittedLegacyBehavior() throws Exception { assertFalse(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).has("askUserVariant")); } + @Test + @AllowCopilotExperimental + void skillProviderFlagIsForwardedAndSerializedForCreateAndResume() throws Exception { + SkillProvider provider = testSkillProvider(); + var mapper = JsonRpcClient.getObjectMapper(); + + var createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setSkillProvider(provider), + "skill-provider-create"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("skill-provider-resume", + new ResumeSessionConfig().setSkillProvider(provider)); + + assertTrue(createRequest.getHasSkillProvider()); + assertTrue(resumeRequest.getHasSkillProvider()); + assertTrue(mapper.readTree(mapper.writeValueAsBytes(createRequest)).path("hasSkillProvider").asBoolean()); + assertTrue(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).path("hasSkillProvider").asBoolean()); + } + + @Test + void skillProviderFlagIsOmittedWhenUnset() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "skill-provider-create"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("skill-provider-resume", + new ResumeSessionConfig()); + + assertNull(createRequest.getHasSkillProvider()); + assertNull(resumeRequest.getHasSkillProvider()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(createRequest)).has("hasSkillProvider")); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).has("hasSkillProvider")); + } + @Test void diagnosticsAreForwardedAndOmittedForCreateAndColdResume() throws Exception { var mapper = JsonRpcClient.getObjectMapper(); @@ -345,8 +427,73 @@ void testBuildResumeRequestNullConfig() { ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", null); assertEquals("sid-1", request.getSessionId()); assertNull(request.getModel()); + assertNull(request.getContinuePendingWork()); assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); + assertNull(request.getHasSkillProvider()); + } + + @Test + void continuePendingWorkIsForwardedAndSerializedOnResume() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + for (boolean enabled : new boolean[]{true, false}) { + var config = new ResumeSessionConfig().setContinuePendingWork(enabled); + assertEquals(enabled, config.getContinuePendingWork().orElseThrow()); + + var request = SessionRequestBuilder.buildResumeRequest("sid-pending", config.clone()); + assertEquals(enabled, request.getContinuePendingWork()); + var serialized = mapper.readTree(mapper.writeValueAsBytes(request)); + assertTrue(serialized.has("continuePendingWork")); + assertEquals(enabled, serialized.get("continuePendingWork").booleanValue()); + } + } + + @Test + void continuePendingWorkIsOmittedWhenUnsetOrCleared() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var config = new ResumeSessionConfig(); + assertTrue(config.getContinuePendingWork().isEmpty()); + var defaultRequest = SessionRequestBuilder.buildResumeRequest("sid-pending-default", config); + assertNull(defaultRequest.getContinuePendingWork()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(defaultRequest)).has("continuePendingWork")); + + config.setContinuePendingWork(true).clearContinuePendingWork(); + var clearedRequest = SessionRequestBuilder.buildResumeRequest("sid-pending-cleared", config); + assertTrue(config.getContinuePendingWork().isEmpty()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(clearedRequest)).has("continuePendingWork")); + } + + @Test + void nullableContinuePendingWorkPreservesExplicitValuesAndClearsDefault() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var config = new ResumeSessionConfig(); + for (Boolean enabled : new Boolean[]{true, false}) { + assertSame(config, config.setContinuePendingWork(enabled)); + var request = SessionRequestBuilder.buildResumeRequest("sid-pending", config.clone()); + assertEquals(enabled, request.getContinuePendingWork()); + assertEquals(enabled, + mapper.readTree(mapper.writeValueAsBytes(request)).path("continuePendingWork").booleanValue()); + } + + assertSame(config, config.setContinuePendingWork((Boolean) null)); + assertTrue(config.getContinuePendingWork().isEmpty()); + var request = SessionRequestBuilder.buildResumeRequest("sid-pending-default", config.clone()); + assertNull(request.getContinuePendingWork()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(request)).has("continuePendingWork")); + } + + @Test + void continuePendingWorkIsOmittedWhenRequestIsCleared() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + for (boolean enabled : new boolean[]{true, false}) { + var request = new ResumeSessionRequest(); + request.setContinuePendingWork(enabled); + assertEquals(enabled, request.getContinuePendingWork()); + + request.clearContinuePendingWork(); + assertNull(request.getContinuePendingWork()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(request)).has("continuePendingWork")); + } } @Test @@ -735,6 +882,17 @@ void configureSessionWithNullConfig_returnsEarly() { assertDoesNotThrow(() -> SessionRequestBuilder.configureSession(session, (SessionConfig) null)); } + @Test + @AllowCopilotExperimental + void configureSessionWithSkillProvider_registersProvider() { + CopilotSession session = new CopilotSession("session-1", null); + SkillProvider provider = testSkillProvider(); + + SessionRequestBuilder.configureSession(session, new SessionConfig().setSkillProvider(provider)); + + assertSame(provider, session.getSkillProvider()); + } + @Test void configureSessionWithCommands_registersCommands() { CopilotSession session = new CopilotSession("session-1", null); @@ -807,6 +965,17 @@ void configureResumedSessionWithOnEvent_registersEventHandler() { SessionRequestBuilder.configureSession(session, config); } + @Test + @AllowCopilotExperimental + void configureResumedSessionWithSkillProvider_registersProvider() { + CopilotSession session = new CopilotSession("session-1", null); + SkillProvider provider = testSkillProvider(); + + SessionRequestBuilder.configureSession(session, new ResumeSessionConfig().setSkillProvider(provider)); + + assertSame(provider, session.getSkillProvider()); + } + @Test void testBuildCreateRequestWithDefaultAgent() { var defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); @@ -1185,4 +1354,19 @@ void githubMcpToolConfigIsMappedAndSerializedForCreateAndResume() throws Excepti mapper.writeValueAsString(SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "session-2")) .contains("\"githubMcpToolConfig\"")); } + + @AllowCopilotExperimental + private static SkillProvider testSkillProvider() { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + } } 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/SkillProviderTest.java b/java/sdk/src/test/java/com/github/copilot/SkillProviderTest.java new file mode 100644 index 0000000000..7a4e6f3329 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SkillProviderTest.java @@ -0,0 +1,421 @@ +/*--------------------------------------------------------------------------------------------- + * 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.util.List; +import java.util.Locale; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +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.generated.SessionEvent; +import com.github.copilot.generated.ToolExecutionCompleteEvent; +import com.github.copilot.generated.rpc.Skill; +import com.github.copilot.generated.rpc.SkillSource; +import com.github.copilot.rpc.CloudSessionOptions; +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.SessionConfig; + +@AllowCopilotExperimental +class SkillProviderTest { + + 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 should_load_provider_skill_lazily_through_skill_tool() throws Exception { + ctx.configureForTest("skill_provider", "should_load_provider_skill_lazily_through_skill_tool"); + + var provider = new TestSkillProvider( + List.of(skill("provider-lookup", "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + try { + Skill listed = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills().stream() + .filter(s -> "provider-lookup".equals(s.name())).findFirst().orElse(null); + assertNotNull(listed, "Expected provider-lookup to be listed"); + assertEquals(SkillSource.SDK, listed.source()); + assertTrue(listed.enabled()); + assertEquals("", listed.path() == null ? "" : listed.path()); + assertEquals(List.of(), provider.reads()); + + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the provider-lookup skill, then reply with its verification word."), + 60_000).get(90, TimeUnit.SECONDS); + + assertEquals(List.of("provider-lookup"), provider.reads()); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("TANGERINE_QUARTZ_19")); + } finally { + session.close(); + } + } + } + + @Test + void should_load_provider_and_file_based_skills_together() throws Exception { + ctx.configureForTest("skill_provider", "should_load_provider_and_file_based_skills_together"); + + Path skillsDir = ctx.getWorkDir().resolve("file-skills"); + Files.createDirectories(skillsDir.resolve("file-notes")); + Files.writeString(skillsDir.resolve("file-notes").resolve("SKILL.md"), + "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n"); + + var provider = new TestSkillProvider(List.of(skill("provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDir.toString())).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + try { + List skills = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + Skill fileSkill = findSkill(skills, "file-notes"); + Skill providerSkill = findSkill(skills, "provider-audit"); + assertNotNull(fileSkill, "Expected file-notes to be listed"); + assertNotEquals(SkillSource.SDK, fileSkill.source()); + assertNotNull(fileSkill.path()); + assertNotNull(providerSkill, "Expected provider-audit to be listed"); + assertEquals(SkillSource.SDK, providerSkill.source()); + + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words."), + 60_000).get(90, TimeUnit.SECONDS); + + assertEquals(List.of("provider-audit"), provider.reads()); + assertTrue(assistantText(message).contains("MAPLE_FALCON_27")); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("COBALT_HERON_58")); + } finally { + session.close(); + } + } + } + + @Test + void should_rebind_skill_provider_on_resume() throws Exception { + ctx.configureForTest("skill_provider", "should_rebind_skill_provider_on_resume"); + + var original = new TestSkillProvider(List.of(skill("rebind-check", "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n"))); + var replacement = new TestSkillProvider(List.of(skill("rebind-check", "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession first = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(original)) + .get(30, TimeUnit.SECONDS); + String sessionId = first.getSessionId(); + AssistantMessageEvent ready = first + .sendAndWait(new MessageOptions() + .setPrompt("Without using any tools or skills, reply with exactly REBIND_READY."), 60_000) + .get(90, TimeUnit.SECONDS); + assertTrue(assistantText(ready).contains("REBIND_READY")); + first.close(); + assertEquals(List.of(), original.reads()); + int originalCallsBeforeResume = original.callCount(); + + CopilotSession session = client + .resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(replacement)) + .get(30, TimeUnit.SECONDS); + + try { + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the rebind-check skill, then reply with its verification word."), + 60_000).get(90, TimeUnit.SECONDS); + + assertEquals(List.of("rebind-check"), replacement.reads()); + assertEquals(originalCallsBeforeResume, original.callCount()); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("BRONZE_BETA_22")); + assertFalse(assistantText(message).contains("AMBER_ALPHA_11")); + } finally { + session.close(); + } + } + } + + @Test + void should_report_provider_read_failure_without_leaking_details() throws Exception { + ctx.configureForTest("skill_provider", "should_report_provider_read_failure_without_leaking_details"); + + String secret = "PROVIDER_SECRET_7F3A9C"; + var provider = new TestSkillProvider(List.of(new ProvidedSkill(new SkillProviderDescriptor("broken-lookup", + "Reports the broken lookup verification word.", null, null, null), () -> { + throw new IllegalStateException("database unavailable: " + secret); + }))); + List events = new CopyOnWriteArrayList<>(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + session.on(events::add); + + try { + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED."), + 60_000).get(90, TimeUnit.SECONDS); + + assertTrue(provider.reads().contains("broken-lookup")); + List failures = failedToolExecutions(events); + assertEquals(1, failures.size()); + assertFalse(JsonRpcClient.getObjectMapper().writeValueAsString(events).contains(secret)); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("LOAD_FAILED")); + } finally { + session.close(); + } + } + } + + @Test + void should_report_missing_provider_skill_as_not_found() throws Exception { + ctx.configureForTest("skill_provider", "should_report_missing_provider_skill_as_not_found"); + + var provider = new TestSkillProvider(List.of(new ProvidedSkill(new SkillProviderDescriptor("vanished-lookup", + "Reports the vanished lookup verification word.", null, null, null), () -> null))); + List events = new CopyOnWriteArrayList<>(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + session.on(events::add); + + try { + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED."), + 60_000).get(90, TimeUnit.SECONDS); + + assertTrue(provider.reads().contains("vanished-lookup")); + List failures = failedToolExecutions(events); + assertEquals(1, failures.size()); + String failureJson = JsonRpcClient.getObjectMapper().writeValueAsString(failures.get(0)) + .toLowerCase(Locale.ROOT); + assertTrue(failureJson.contains("not found"), "Expected not found failure, got: " + failureJson); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("LOAD_FAILED")); + } finally { + session.close(); + } + } + } + + @Test + void should_keep_provider_dormant_when_skills_disabled() throws Exception { + ctx.initializeProxy(); + + var provider = new TestSkillProvider(List.of(skill("dormant-lookup", "Never listed.", "Never read.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setEnableSkills(false).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + try { + session.getRpc().skills.ensureLoaded().get(30, TimeUnit.SECONDS); + List skills = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + + assertEquals(List.of(), sdkSkills(skills)); + assertEquals(List.of(), provider.calls()); + } finally { + session.close(); + } + } + } + + @Test + void should_unbind_provider_when_resumed_without_one() throws Exception { + ctx.initializeProxy(); + + var provider = new TestSkillProvider( + List.of(skill("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession first = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + List before = first.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + assertTrue(before.stream().anyMatch(s -> "unbound-lookup".equals(s.name()))); + int callsBeforeResume = provider.callCount(); + + CopilotSession session = client + .resumeSession(first.getSessionId(), + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + + try { + session.getRpc().skills.reload().get(30, TimeUnit.SECONDS); + List skills = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + + assertEquals(List.of(), sdkSkills(skills)); + assertEquals(callsBeforeResume, provider.callCount()); + } finally { + session.close(); + } + } + } + + @Test + void should_cancel_a_blocked_provider_call_when_the_session_disconnects() throws Exception { + ctx.initializeProxy(); + + var entered = new CountDownLatch(1); + var pending = new CompletableFuture>(); + var provider = new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + entered.countDown(); + return pending; + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + session.getRpc().skills.list(); + assertTrue(entered.await(30, TimeUnit.SECONDS), "provider listSkills was not called"); + + session.close(); + assertThrows(CancellationException.class, () -> pending.get(10, TimeUnit.SECONDS)); + } + } + + @Test + void should_reject_skill_provider_for_cloud_sessions() throws Exception { + ctx.initializeProxy(); + + var provider = new TestSkillProvider(List.of(skill("cloud-lookup", "Never listed.", "Never read.\n"))); + + try (CopilotClient client = ctx.createClient()) { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCloud(new CloudSessionOptions()).setSkillProvider(provider); + + ExecutionException error = assertThrows(ExecutionException.class, + () -> client.createSession(config).get(30, TimeUnit.SECONDS)); + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + assertEquals("Skill providers are not supported for cloud sessions.", error.getCause().getMessage()); + assertEquals(List.of(), provider.calls()); + } + } + + private static ProvidedSkill skill(String name, String description, String markdown) { + return new ProvidedSkill(new SkillProviderDescriptor(name, description, null, null, null), () -> markdown); + } + + private static Skill findSkill(List skills, String name) { + return skills.stream().filter(s -> name.equals(s.name())).findFirst().orElse(null); + } + + private static List sdkSkills(List skills) { + return skills.stream().filter(s -> s.source() == SkillSource.SDK).toList(); + } + + private static List failedToolExecutions(List events) { + return events.stream().filter(ToolExecutionCompleteEvent.class::isInstance) + .map(ToolExecutionCompleteEvent.class::cast) + .filter(event -> !Boolean.TRUE.equals(event.getData().success())).toList(); + } + + private static String assistantText(AssistantMessageEvent response) { + assertNotNull(response, "Expected a response from the assistant"); + assertNotNull(response.getData(), "Expected assistant response data"); + assertNotNull(response.getData().content(), "Expected assistant response content"); + return response.getData().content(); + } + + private record ProvidedSkill(SkillProviderDescriptor descriptor, Supplier reader) { + String read() { + return reader.get(); + } + } + + private static final class TestSkillProvider implements SkillProvider { + private final List calls = new CopyOnWriteArrayList<>(); + private final List skills; + + private TestSkillProvider(List skills) { + this.skills = skills; + } + + List calls() { + return List.copyOf(calls); + } + + int callCount() { + return calls.size(); + } + + List reads() { + return calls.stream().filter(call -> call.startsWith("read:")).map(call -> call.substring("read:".length())) + .toList(); + } + + @Override + public CompletableFuture> listSkills() { + calls.add("list"); + return CompletableFuture.completedFuture(skills.stream().map(ProvidedSkill::descriptor).toList()); + } + + @Override + public CompletableFuture readSkill(String name) { + calls.add("read:" + name); + return CompletableFuture + .completedFuture(skills.stream().filter(skill -> skill.descriptor().name().equals(name)).findFirst() + .map(ProvidedSkill::read).orElse(null)); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/StdioShutdownIT.java b/java/sdk/src/test/java/com/github/copilot/StdioShutdownIT.java new file mode 100644 index 0000000000..e836917e98 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/StdioShutdownIT.java @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.github.copilot.rpc.CopilotClientOptions; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Exercises owned-process shutdown through the public client API. + */ +@Timeout(value = 90, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) +class StdioShutdownIT { + + @Test + void stopWaitsForCleanupAfterStdinEof() throws Exception { + try (var fixture = new Fixture("stop")) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.client.stop().get(60, TimeUnit.SECONDS); + fixture.assertCleanExit(); + } + } + + @Test + void closeWaitsForCleanupAfterStdinEof() throws Exception { + try (var fixture = new Fixture("dispose")) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.client.close(); + fixture.assertCleanExit(); + } + } + + @Test + void forceStopDoesNotWaitForGracefulCleanup() throws Exception { + try (var fixture = new Fixture("force")) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.client.forceStop().get(5, TimeUnit.SECONDS); + fixture.assertExited(); + assertFalse(Files.exists(fixture.marker)); + } + } + + @Test + void stopTerminatesChildThatDoesNotExitAfterEof() throws Exception { + try (var fixture = new Fixture("fallback")) { + fixture.client.start().get(30, TimeUnit.SECONDS); + long started = System.nanoTime(); + fixture.client.stop().get(60, TimeUnit.SECONDS); + assertTrue(System.nanoTime() - started >= TimeUnit.SECONDS.toNanos(10), + "Stop must allow the full graceful exit timeout before terminating"); + fixture.assertCleanExit(); + } + } + + @Test + void closeTerminatesChildThatDoesNotExitAfterEof() throws Exception { + try (var fixture = new Fixture("fallback")) { + fixture.client.start().get(30, TimeUnit.SECONDS); + long started = System.nanoTime(); + fixture.client.close(); + assertTrue(System.nanoTime() - started >= TimeUnit.SECONDS.toNanos(10), + "Close must allow the full graceful exit timeout before terminating"); + fixture.assertCleanExit(); + } + } + + @Test + void failedStartupTerminatesChild() throws Exception { + try (var fixture = new Fixture("start-failure")) { + assertThrows(ExecutionException.class, () -> fixture.client.start().get(30, TimeUnit.SECONDS)); + fixture.assertExited(); + assertFalse(Files.exists(fixture.marker)); + } + } + + private static final class Fixture implements AutoCloseable { + private final Path directory; + private final Path marker; + private final Path pid; + private final CopilotClient client; + + private Fixture(String mode) throws Exception { + directory = Files.createDirectories(Path.of("target", "shutdown-" + UUID.randomUUID()).toAbsolutePath()); + marker = directory.resolve("cleanup.jsonl"); + pid = directory.resolve("pid"); + Path script = Path.of("..", "..", "test", "harness", "stdio-shutdown-runtime.cjs").toAbsolutePath(); + assertTrue(Files.isRegularFile(script), "Shared shutdown fixture must exist"); + String nodePath = TestUtil.findExecutableInPath("node"); + assertNotNull(nodePath, "Node.js was not found in PATH"); + client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setCliPath(nodePath) + .setCliArgs(new String[]{script.toString(), marker.toString(), mode, pid.toString()})); + } + + private void assertCleanExit() throws Exception { + assertEquals("{\"type\":\"span\"}\n", Files.readString(marker)); + assertExited(); + } + + private void assertExited() throws Exception { + assertTrue(Files.isRegularFile(pid), "Child must have started"); + assertFalse( + ProcessHandle.of(Long.parseLong(Files.readString(pid))).map(ProcessHandle::isAlive).orElse(false), + "Child must be reaped before shutdown returns"); + } + + @Override + public void close() throws Exception { + if (Files.isRegularFile(pid)) { + var process = ProcessHandle.of(Long.parseLong(Files.readString(pid))); + if (process.isPresent() && process.get().isAlive()) { + process.get().destroyForcibly(); + process.get().onExit().get(10, TimeUnit.SECONDS); + } + } + client.close(); + try (var paths = Files.walk(directory)) { + for (var path : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(path); + } + } + } + } +} 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..bc83502f87 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,177 @@ 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().toRealPath(), Path.of(start.cwd()).toRealPath()); + 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 +227,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 +242,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/TelemetryExportIT.java b/java/sdk/src/test/java/com/github/copilot/TelemetryExportIT.java new file mode 100644 index 0000000000..5d17eed144 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/TelemetryExportIT.java @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +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.rpc.CopilotClientOptions; +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.RuntimeConnection; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.TelemetryConfig; + +class TelemetryExportIT { + private static final String PROMPT = "Use the task tool in sync mode to ask a task agent to read " + + "subagent-otel.txt with the view tool. Then reply with SUBAGENT_OTEL_DONE."; + + @Test + void shouldExportPerRequestSubagentChatSpans() throws Exception { + try (E2ETestContext ctx = E2ETestContext.create()) { + ctx.configureForTest("telemetry", "should_export_per_request_subagent_chat_spans"); + Path telemetryPath = ctx.getWorkDir().resolve("java-subagent-telemetry.jsonl"); + String sourceName = "java-sdk-subagent-telemetry-e2e"; + Files.writeString(ctx.getWorkDir().resolve("subagent-otel.txt"), "SUBAGENT_OTEL_FILE_CONTENT"); + + var options = new CopilotClientOptions().setConnection(RuntimeConnection.forStdio(ctx.getCliPath())) + .setTelemetry(new TelemetryConfig().setFilePath(telemetryPath.toString()).setExporterType("file") + .setSourceName(sourceName).setCaptureContent(true)); + String sessionId; + try (CopilotClient client = ctx.createClient(options)) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + try { + sessionId = session.getSessionId(); + var response = session.sendAndWait(new MessageOptions().setPrompt(PROMPT), 90_000).get(120, + TimeUnit.SECONDS); + assertNotNull(response); + assertTrue(response.getData().content().contains("SUBAGENT_OTEL_DONE")); + } finally { + session.close(); + } + client.stop().get(30, TimeUnit.SECONDS); + } + + ObjectMapper mapper = new ObjectMapper(); + List spans = new ArrayList<>(); + for (String line : Files.readAllLines(telemetryPath)) { + if (!line.isBlank()) { + JsonNode entry = mapper.readTree(line); + if ("span".equals(property(entry, "type"))) { + spans.add(entry); + } + } + } + assertFalse(spans.isEmpty(), "Expected exported spans"); + for (JsonNode span : spans) { + assertEquals(sourceName, property(span.path("instrumentationScope"), "name")); + assertFalse(span.path("status").path("code").asInt() == 2, "Unexpected error span: " + span); + } + + List invocations = operation(spans, "invoke_agent"); + assertEquals(2, invocations.size(), "Invocation spans: " + invocations); + List roots = invocations.stream().filter(s -> { + String parent = property(s, "parentSpanId"); + return parent == null || parent.isEmpty() || parent.equals("0000000000000000"); + }).toList(); + assertEquals(1, roots.size(), "Root spans: " + roots); + JsonNode root = roots.get(0); + assertEquals(sessionId, attribute(root, "gen_ai.conversation.id")); + String rootId = property(root, "spanId"); + String traceId = property(root, "traceId"); + assertNotNull(rootId); + assertNotNull(traceId); + + List tasks = operation(spans, "execute_tool").stream() + .filter(s -> "task".equals(attribute(s, "gen_ai.tool.name"))).toList(); + assertEquals(1, tasks.size(), "Task spans: " + tasks); + JsonNode task = tasks.get(0); + assertEquals(rootId, property(task, "parentSpanId")); + List children = invocations.stream() + .filter(s -> property(task, "spanId").equals(property(s, "parentSpanId"))).toList(); + assertEquals(1, children.size(), "Child spans: " + children); + JsonNode child = children.get(0); + assertEquals(traceId, property(task, "traceId")); + assertEquals(traceId, property(child, "traceId")); + + List chats = operation(spans, "chat"); + assertEquals(4, chats.size(), "Chat spans: " + chats); + List parentChats = chats.stream().filter(s -> rootId.equals(property(s, "parentSpanId"))) + .toList(); + assertEquals(2, parentChats.size()); + for (JsonNode chat : parentChats) { + assertEquals(traceId, property(chat, "traceId")); + } + List childChats = chats.stream() + .filter(s -> property(child, "spanId").equals(property(s, "parentSpanId"))).toList(); + assertEquals(2, childChats.size(), "Child chats: " + childChats); + for (JsonNode chat : childChats) { + assertEquals(traceId, property(chat, "traceId")); + assertEquals("sub-agent", attribute(chat, "github.copilot.initiator")); + } + List requestingChats = childChats.stream() + .filter(s -> attribute(s, "gen_ai.output.messages").contains("\"view\"")).toList(); + assertEquals(1, requestingChats.size()); + String requestingInput = attribute(requestingChats.get(0), "gen_ai.input.messages"); + assertTrue(requestingInput == null || !requestingInput.contains("SUBAGENT_OTEL_FILE_CONTENT")); + List finals = childChats.stream() + .filter(s -> attribute(s, "gen_ai.output.messages").contains("SUBAGENT_OTEL_CHILD_DONE")).toList(); + assertEquals(1, finals.size(), "Final child chats: " + finals); + assertTrue(attribute(finals.get(0), "gen_ai.input.messages").contains("SUBAGENT_OTEL_FILE_CONTENT")); + } + } + + private static List operation(List spans, String name) { + return spans.stream().filter(s -> name.equals(attribute(s, "gen_ai.operation.name"))).toList(); + } + + private static String attribute(JsonNode span, String name) { + return property(span.path("attributes"), name); + } + + private static String property(JsonNode value, String name) { + JsonNode property = value.path(name); + if (property.isMissingNode() || property.isNull()) { + return null; + } + return property.isTextual() ? property.asText() : property.toString(); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/TestUtil.java b/java/sdk/src/test/java/com/github/copilot/TestUtil.java index 8e90c2f48f..b8ac48748e 100644 --- a/java/sdk/src/test/java/com/github/copilot/TestUtil.java +++ b/java/sdk/src/test/java/com/github/copilot/TestUtil.java @@ -95,7 +95,7 @@ static String preparePinnedCli(Path repoRoot) throws Exception { return output; } - private static String findExecutableInPath(String name) { + static String findExecutableInPath(String name) { var pathValue = System.getenv("PATH"); if (pathValue == null || pathValue.isEmpty()) { return null; 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 5ad0ee2402..e6ec2ba203 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,19 +1058,20 @@ 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); var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount", true); var billing = new ModelBilling(1.0, null, null, promo); - var modelItem = new Model("gpt-5", "GPT-5", capabilities, null, policy, billing, null, null, null, null, null, - null, null, null); + var modelItem = new Model("gpt-5", "GPT-5", capabilities, null, null, policy, billing, null, null, null, null, + null, null, null, null, null); var result = new ModelsListResult(List.of(modelItem)); 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/java/sdk/src/test/prompts/PROMPT-smoke-test.md b/java/sdk/src/test/prompts/PROMPT-smoke-test.md deleted file mode 100644 index 4013002aca..0000000000 --- a/java/sdk/src/test/prompts/PROMPT-smoke-test.md +++ /dev/null @@ -1,135 +0,0 @@ -# Prompt: Generate and Run the copilot-sdk-java Smoke Test - -## Objective - -Create a Maven project that acts as a smoke test for `copilot-sdk-java`. The project must compile, build, and run to completion with exit code 0 as the definition of success. - -## Step 1 — Read the source README - -Read the file `README.md` at the top level of this repository. You will need two sections from it: - -- **"Snapshot Builds"** — provides the Maven GAV (groupId, artifactId, version) and the Maven Central Snapshots repository configuration to use for the dependency under test. -- **"Quick Start"** — provides the exact Java source code for the smoke test program. Use this code verbatim. Do not modify it, fix it, or improve it. If it does not compile or run correctly against the artifact under test, that is itself a smoke test failure and must be reported as such rather than silently corrected. - -## Step 2 — Create the Maven project - -Create the following file layout in a subdirectory named `smoke-test/` at the top level of this repository: - -``` -smoke-test/ - pom.xml - src/main/java/(Class name taken from the code in the "Quick Start" section in the README).java ← verbatim from README "Quick Start" -``` - -### `pom.xml` requirements - -- **groupId**: `com.github` (or any reasonable value) -- **artifactId**: `copilot-sdk-smoketest` -- **version**: `1.0-SNAPSHOT` -- **packaging**: `jar` -- **Java source/target**: (taken from the "Requirements" section in the README) (via `maven.compiler.source` and `maven.compiler.target` properties) -- **`mainClass` property**: (taken from the "Quick Start" section in the README) (the class is in the default package) - -#### Snapshot repository - -Configure the Maven Central Snapshots repository exactly as specified in the "Snapshot Builds" section of `README.md`, and add `always` inside the `` block so that every build fetches the latest snapshot without requiring `-U`: - -```xml - - central-snapshots - https://central.sonatype.com/repository/maven-snapshots/ - - true - always - - -``` - -#### Dependency - -Use the GAV from the "Snapshot Builds" section of `README.md` verbatim — do not substitute the release version from the "Maven" section. - -#### Plugins — REQUIRED configuration - -**Do not use `maven-shade-plugin`.** Use the `Class-Path` manifest approach instead: - -1. **`maven-jar-plugin`** (version **3.4.1** — pin explicitly to suppress Maven version warnings): - - ```xml - - org.apache.maven.plugins - maven-jar-plugin - 3.4.1 - - - - ${mainClass} - true - lib/ - false - - - - - ``` - - **Critical**: `false` is mandatory. Without it, the manifest `Class-Path:` entry uses the timestamped SNAPSHOT filename (e.g. `copilot-sdk-java-0.1.33-20260312.125508-3.jar`) while `copy-dependencies` writes the base SNAPSHOT filename (`copilot-sdk-java-0.1.33-SNAPSHOT.jar`), causing `NoClassDefFoundError` at runtime. - -2. **`maven-dependency-plugin`** (version **3.6.1**): - - ```xml - - org.apache.maven.plugins - maven-dependency-plugin - 3.6.1 - - - copy-dependencies - package - copy-dependencies - - ${project.build.directory}/lib - - - - - ``` - - This copies all runtime dependency JARs into `target/lib/`, which is where the manifest `Class-Path:` points. - -## Step 3 — Build - -```bash -mvn -U clean package -``` - -The `-U` flag forces a fresh snapshot metadata check regardless of local cache. The `always` already handles this for normal invocations, but `-U` is the safest choice for CI. - -Build must succeed with `BUILD SUCCESS` before proceeding. - -## Step 4 — Run - -```bash -java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar -``` - -The JAR must be run from the `smoke-test/` directory so that the relative `lib/` path in the manifest resolves correctly. Do not use `-cp` or `-classpath` — the test specifically validates that `java -jar` works with the manifest `Class-Path:` approach. - -## Step 5 — Verify success - -The smoke test passes if and only if the process exits with code **0**. - -The "Quick Start" code in `README.md` already contains the exit-code logic: it captures the last assistant message and calls `System.exit(0)` if it contains `"4"` (the expected answer to "What is 2+2?"), or `System.exit(-1)` otherwise. - -Check the exit code: -```bash -echo "Exit code: $?" -``` - -Expected: `Exit code: 0` - -## Important API notes (do not apply these as fixes — they are here for diagnostic context only) - -If the build fails with compilation errors such as `cannot find symbol` on methods like `getContent()`, `getCurrentTokens()`, `getTokenLimit()`, or `getMessagesLength()`, this indicates a mismatch between the Quick Start code and the SDK implementation. **Do not silently fix the code.** Report the failure. The purpose of this smoke test is precisely to catch such regressions. - -For reference: the data classes in `copilot-sdk-java` are Java **records**. Record accessor methods have no `get` prefix — they are named `content()`, `currentTokens()`, `tokenLimit()`, and `messagesLength()`. If the README Quick Start uses `getContent()` etc., that is a bug in the README that must be surfaced, not silently corrected. diff --git a/nodejs/README.md b/nodejs/README.md index b89fce6331..8a47d28a12 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 @@ -37,6 +41,139 @@ verify all eight platforms. npm install @github/copilot-sdk ``` +## Runtime-supervised AHP host (experimental) + +`startAhpHost()` exposes copilotd's complete Agent Host Protocol server through the +same runtime used by the SDK: + +```typescript +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); + }, +}); + +// Connect an AHP client using host.url and host.token. +// Treat host.token as a secret; do not log it. + +// Existing SDK sessions and AHP sessions share this runtime. +// Keep client alive while the listener is needed. Leaving this scope disposes +// client, and the runtime stops its listener without a separate host.dispose(). +``` + +The runtime hosts the complete AHP server in-process; the SDK does not launch a +second runtime or relay the host's traffic. The host has its own SDK connection +and belongs to the client connection that started it. Explicit disposal, +connection loss, and runtime shutdown stop the listener and its hosting task +without deleting underlying sessions. Reconnecting does not reclaim a host. +The optional `onExit` callback reports exits at most once. If the owner connection +is lost, it reports that loss rather than claiming that listener cleanup was +acknowledged. + +`host.pid` is absent for in-process listeners. The optional field is retained +for separate host process IDs returned by legacy runtimes, never the runtime PID. +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. + +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. +- `port` defaults to `0`, which selects an available port. +- `requireConnectionToken` defaults to `true`. The runtime generates a random + token unless you supply a nonempty `token`. +- Set `requireConnectionToken: false` to disable token authentication; + `host.token` is then undefined. A supplied `token` cannot be combined with + `requireConnectionToken: false`. + +The listener follows the owning client's lifetime. Call `await host.dispose()` +only when you want to stop it earlier; `await using host` also supports a shorter +scope. Each disposal call forwards to the runtime, which owns idempotent cleanup. +The AHP transport remains owned by the host. + +### Application-owned sessions + +Supply `createSession` to materialize fresh AHP sessions in your application: + +```typescript +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({ + ...config, + onPermissionRequest: approveAll, + systemMessage: { mode: "append", content: "Use the app's greeting tool." }, + tools: [defineTool("greeting", { + description: "Get the application's greeting", + parameters: { type: "object", properties: {} }, + handler: () => "Hello from the application!", + })], + }); + }, + onSessionReleased: async (originalSession) => { + // Optional: the app decides whether to disconnect, keep, or destroy it. + await originalSession.disconnect(); + }, +}); +``` + +Preserve the supplied `config`, including its fresh session identity, workspace, +and selected host settings. Add your prompt and tools where the host has not +explicitly selected those settings; conflicting settings fail rather than +silently advertising configuration that was not applied. Return a normal session +created by this same client. The creation factory does not adopt an arbitrary existing +session or expose unrelated application sessions in the AHP catalog. + +Only the session ID returns through ordinary SDK RPC. The host attaches to **that same +resident session**, adding its own callback/tool registrations without replacing +the application's prompt, tool filters, hooks, or tools. Application tool functions +continue running in the app while their results stream through the existing AHP +projector. No second application connection or function serialization is involved. + +The SDK retains the original returned object until participation ends and invokes +`onSessionReleased` at most once per handoff, including attach failure, hosting-task exit, +and owner disconnection. The SDK never automatically disconnects or destroys the +app object. The creation callback receives an abort signal; materialization +is bounded to 30 seconds and cancellation also releases objects returned late. +Graceful host disposal waits for AHP detach before reporting release. Omitting +`createSession` preserves copilotd-owned creation. + +To restore durable application-owned sessions, also supply `resumeSession`. +It receives `{ sessionId, config, signal }` (`AhpSessionResumeRequest`). +Return the object from this client's `resumeSession(sessionId, { ...config, +onPermissionRequest, ... })`, restoring your tools, hooks, and handlers. +Alternatively, return a retained original session from this client when it +still matches the requested identity and workspace. +Only catalog entries marked as application-owned invoke this callback. +If the callback is missing, restoring such an entry fails instead of falling +back to host-owned creation. +Published resident sessions attach directly, without invoking it or replacing +their current registrations. Resumed sessions follow the same original-object +retention, cancellation, late-result release, and `onSessionReleased` rules. + +The `copilotd-hosting` library runs inside the runtime provider. +Development integrations require a +source-built launcher and provider (`COPILOT_RUNTIME_PROVIDER_LIB`). Older +runtimes without these RPC operations cannot start a host. + ## Run the Sample Try the interactive chat sample (from the repo root): @@ -124,6 +261,7 @@ new CopilotClient(options?: CopilotClientOptions) - `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd). - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`. - `extensionLaunchProvider?: ExtensionLaunchProvider` - Experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can be created. +- `installationConfirmationHandler?: InstallationConfirmationHandler` - Experimental connection-global human review for `installations.confirm`. Receives the typed request and independent request/connection cancellation signals, and returns an explicit decision. Does not enable installation capabilities. - `logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all"` - Log level. When omitted, the runtime uses its own default (currently `"info"`). - `env?: Record` - Environment variables for the runtime process. When omitted, inherits `process.env`. - `gitHubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. @@ -132,9 +270,65 @@ 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`. +#### Installation confirmation (experimental) + +All six SDKs (Node.js, Python, Go, .NET, Java and Rust) provide this receiver with the +same semantics: each review gets one cancellation token, concurrent reviews are +independent, and a decision returned after cancellation is never sent. Without a +configured handler, an `installations.confirm` request is refused, which the +runtime treats as no consent. + +The installation confirmation handler receives the generated +`InstallationConfirmationRequest` and a `CancellationToken`. Match `operationId` +and `policySessionId` against the exact original action on this connection +before presenting the complete review. Refuse unknown operations or incomplete +reviews; missing legacy session metadata is not permission to use the current +session. Return `"confirm"`, `"decline"` or `"cancel"` only after an explicit +human decision. The SDK echoes the original challenge and fingerprint. + +Concurrent reviews remain independent. The token is cancelled when the runtime +retires the request, including runtime-enforced expiry, or when the original +connection closes. Observe it to close pending UI. Late handler results cannot +approve a retired request. This incoming signal does not cancel outbound +installation or OAuth RPCs, and dropping those promises is not cancellation. + +Use `client.rpc.mcp.prepareInstall` before `applyInstall`: preparation returns an +inert runtime-issued `operationId` and original expiry. Register that ID with its +captured session on this exact client before applying. Removal uses +`planUninstall` then `applyUninstall`; the returned `operationId` identifies the +operation, while `planHandle` is the one-use removal input. Never interchange them. +Use `client.rpc.mcp.installations.list` and `recover` for owned inventory. +Inspect or cancel uncertain work through `status` and `cancel` on the original +connection and operation ID, without selecting a replacement session or replaying apply. + +Owned OAuth similarly uses `session.rpc.mcp.oauth.prepareLogin` to obtain +`loginId` before browser, network or cached-reconnect work. Retain that ID with +the original session and `expectedInstallationId` for `login` and `cancelLogin`. +Prepare freezes reauthentication and display options. Cancelling an incoming +confirmation or abandoning a login promise is not a substitute for `cancelLogin`. +Manual MCP OAuth retains its direct `login` path. + +A matching runtime contract and available owned-lifecycle support are required. +Capability negotiation does not promise availability; preserve typed refusals +instead of falling back to raw configuration writes. Generated presence and +transport tests do not establish a working installer, live OAuth or restart safety. + #### Methods ##### `start(): Promise` @@ -144,6 +338,9 @@ Start the CLI server and establish connection. ##### `stop(): Promise` Stop the server and close all sessions. Returns a list of any errors encountered during cleanup. +For an owned stdio runtime, closes stdin and waits up to 10 seconds for host cleanup +(including telemetry export) and process exit before falling back to termination. +This graceful-exit timeout is separate from the shutdown RPC and post-termination wait. ##### `forceStop(): Promise` @@ -172,6 +369,7 @@ Create a new conversation session. - `askUserVariant?: "legacy" | "elicitation"` - Selects the model-facing `ask_user` tool shape when creating or cold-resuming a session. Defaults to `"legacy"`; use `"elicitation"` with `onElicitationRequest`. - `onElicitationRequest?: ElicitationHandler` - Handler for elicitation requests dispatched by the server. Enables this client to present form-based UI dialogs on behalf of the agent or other session participants. See [Elicitation Requests](#elicitation-requests) section. - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. +- `skillProvider?: SkillProvider` - **Experimental.** Serves skills from host storage instead of skill directories. Not persisted; pass it again on resume. See [Skill providers](#skill-providers-experimental). ```typescript const session = await client.createSession({ @@ -188,6 +386,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 }>` @@ -454,6 +658,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. @@ -649,6 +871,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: @@ -798,6 +1036,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 @@ -806,6 +1046,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 @@ -879,6 +1121,46 @@ const session = await client.createSession({ }); ``` +### Skill providers (experimental) + +A skill provider serves skills from your own storage, such as a database, instead of `SKILL.md` +files on disk. Provider skills join the session's skill catalog and load on demand through the +`skill` tool, just like file-based skills. + +```typescript +import type { SkillProvider } from "@github/copilot-sdk"; + +const skillProvider: SkillProvider = { + listSkills: async () => [ + { name: "release-notes", description: "Writes release notes in the team's format." }, + ], + readSkill: async (name, { signal }) => (await db.findSkill(name, { signal }))?.markdown ?? null, +}; + +const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider, +}); +``` + +- `listSkills()` returns the catalog metadata. `readSkill(name)` returns the skill's markdown, or + `null`/`undefined` if the skill no longer exists. The markdown may omit YAML frontmatter; when + frontmatter is present, its fields must agree with the listed metadata, and `allowed-tools` is + read only from frontmatter. +- Each call receives `{ signal }`, an `AbortSignal` that fires when the runtime cancels the call, + for example after its 30-second limit or when the session disconnects. It doesn't fire when the + connection closes or the client is force-stopped; a running call then continues until it returns. +- The provider is never persisted. Pass it again to `resumeSession`; resuming without it unbinds + the provider. +- A provider enables skills unless you set `enableSkills: false`, which keeps it bound but unused. + In `mode: "empty"`, skills stay disabled until you set `enableSkills: true`. +- Errors thrown by the provider are reported to the model as a generic load failure; their + messages are not forwarded. +- The runtime may call the provider concurrently, so both methods must be safe for concurrent use. +- Skill providers are not supported for cloud sessions. + +See [Custom skills](../docs/features/skills.md#skill-providers-experimental) for limits and details. + ### Multiple Sessions ```typescript @@ -1253,6 +1535,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}` }; + }, }, }); ``` @@ -1267,6 +1563,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/docs/extensions.md b/nodejs/docs/extensions.md index 15577bfdd0..5a50d9db3f 100644 --- a/nodejs/docs/extensions.md +++ b/nodejs/docs/extensions.md @@ -78,5 +78,4 @@ An approved extension can pass a granted value to anything it starts, so ask onl - `examples.md` — Practical code examples for tools, hooks, events, and complete extensions - `workflows.md` — Authoring, running, resuming, and observing Dynamic Workflows -- `factories.md`: Authoring, running, resuming, and observing Agent Factories - `agent-author.md` — Step-by-step workflow for agents authoring extensions programmatically diff --git a/nodejs/docs/factories.md b/nodejs/docs/factories.md deleted file mode 100644 index f07230a839..0000000000 --- a/nodejs/docs/factories.md +++ /dev/null @@ -1,296 +0,0 @@ -# Agent Factories - -Agent Factories are extension-authored, session-scoped workflows that coordinate subagents and durable steps. The API is experimental. - -## Define and register a factory - -Use `defineFactory` and pass the returned handle to `joinSession`: - -```js -import { defineFactory, joinSession } from "@github/copilot-sdk/extension"; - -const reviewChanged = defineFactory({ - meta: { - name: "review-changed", - description: - "Review changed files and verify the findings. " + - "args: { files: string[] } — the paths to review.", - phases: [{ title: "Review" }, { title: "Verify" }], - argsSchema: { - type: "object", - required: ["files"], - properties: { - files: { type: "array", items: { type: "string" } }, - }, - }, - }, - run: async (ctx) => { - ctx.phase("Review"); - const reviews = await ctx.parallel( - ctx.args.files.map( - (file) => () => ctx.agent(`Review ${file}`, { label: `Review ${file}` }) - ) - ); - - ctx.phase("Verify"); - const report = await ctx.step("report", () => ({ reviews })); - ctx.log(`Completed factory run ${ctx.runId}`); - return report; - }, -}); - -const session = await joinSession({ factories: [reviewChanged] }); -``` - -Factory metadata contains a stable `name`, a human-readable `description`, declared `phases`, an optional `argsSchema`, and optional `limits`. Phase entries contain a `title` and optional `detail`. - -## Declaring an argument shape - -A factory that reads `ctx.args` should declare `meta.argsSchema`, as the example above does. When the model invokes the factory through the `run_factory` tool, the CLI validates `args` against the declaration **before** the run starts. - -Declaring one turns an expensive failure into a cheap one. With a schema, a malformed call is rejected up front — the model gets a correction hint and retries, and no run row, permission prompt, or credit spend happens. Without one, nothing validates: the run starts, takes a user approval, spends credits, and then dies inside the factory body with a confusing error. Agents can read the declared shape with `factories_manage` using `operation: "inspect"`. - -Enforcement covers structure — types, required properties, and enum or const values. Finer constraints such as `minLength`, `pattern`, or `additionalProperties` are recorded in the declaration but not enforced. The accepted vocabulary is the `FactoryJsonSchema` subset also used for subagent structured output: `type`, `required`, `enum`, `const`, recursive `properties`/`items`, and `anyOf`/`oneOf`/`allOf`. A `type` is one of `null`, `boolean`, `integer`, `number`, `string`, `array`, or `object`, or a non-empty array of those such as `["object", "null"]`. A declaration outside that subset is rejected at registration. - -`argsSchema` is optional and backward compatible. A factory that omits it behaves exactly as before, so **the `description` is then the only thing telling an agent what arguments to supply** — state the expected shape there. - -Validation covers the model's `run_factory` path only. An extension calling `session.factory.run(...)` directly is not validated against `argsSchema`; those arguments are typed through `defineFactory` instead, and that typing does not reach the model. So a factory that reads `ctx.args` should still validate it rather than assume a shape — the declared subset does not enforce every constraint, and it does not run at all on the SDK path. - -`defineFactory` accepts a `run(context)` function returning `Promise`, where `TResult` is `JsonValue | void`. Objects, arrays, strings, numbers, booleans, and `null` are valid results. Returning `undefined` completes the factory with no result. Other non-JSON values are rejected. - -## Factory context - -The `run()` context provides: - -* `ctx.runId`: Stable ID reused across resumed attempts. -* `ctx.args`: Invocation arguments, forwarded verbatim. When the caller omits `args`, this is `{}` rather than `undefined`. -* `ctx.agent(prompt, options?)`: Runs one factory-owned subagent. Options are exactly `label`, `schema`, `model`, `agent`, `reasoningEffort`, and `contextTier`. See [Subagent calls](#subagent-calls). -* `ctx.parallel(thunks)`: Runs thunks concurrently and awaits all of them (a barrier). A thunk that throws becomes `null` in the result array, so one failed item does not lose the rest. Cancellation and hard runtime failures (`ResponseError`, `ConnectionError`) are the exception — those propagate and reject the whole call, because they mean the run itself is in trouble rather than one item having failed. Handle them at run level; do not assume every failure arrives as a `null`. Rejects above 4096 items. -* `ctx.pipeline(items, ...stages)`: Flows each item through every stage without a barrier between stages, so one item can be in a later stage while another is still in an earlier one. Each stage is called as `(previous, item, index)`, where `previous` is the prior stage's result and `item` is the original input. A stage that throws drops that item to `null` and skips its remaining stages, with the same exception for cancellation and hard runtime failures. Rejects above 4096 items. -* `ctx.phase(title)`: Starts a named progress phase. This sets a single run-global value, so calling it from inside concurrent `parallel`/`pipeline` stages races. Call it at run-level transitions and distinguish concurrent work by `label` instead. -* `ctx.log(message)`: Appends a progress line. When a factory bounds its own coverage (top-N, sampling), log what was dropped. -* `ctx.step(key, producer, options?)`: Journals the producer's JSON result under a stable key so a resume replays it without re-running the producer. A journaled (default) producer must return a JSON-serializable value; `undefined` or a non-JSON value is rejected. Pass `{ volatile: true }` to bypass the journal and run the producer every time. - - The key is the *sole* identity: neither the producer body nor its inputs contribute to it. A resume replays the cached value for a matching key even if the producer has since changed, so version the key (`"scan-v2"`) whenever its inputs or meaning change. Journaled producers are best-effort at-least-once and may run again across crashes or concurrent same-key callers, so keep side effects idempotent. -* `ctx.pause(key)`: Pauses at a durable, one-shot checkpoint. The first attempt records the checkpoint, pauses, and throws `AbortError` after cooperative cancellation. When the run resumes, the factory starts again and the same checkpoint returns so execution can continue. Call it only from the main factory flow, not inside `ctx.parallel()` or `ctx.pipeline()`. -* `ctx.session`: The session returned by `joinSession`. It refuses calls that start, resume, or pause a factory run. Call `extensions_manage` with `operation: "guide"` to read more about the session APIs. -* `ctx.signal`: Cooperative cancellation signal for extension work and subprocesses. -* `ctx.factory(...)`: Always rejects because nested factories are not supported. - -Factory-owned subagents are intentionally hidden from `read_agent` and `write_agent`. Use the factory observability APIs instead. - -### Subagent calls - -`ctx.agent(prompt, options?)` spawns one factory-scoped subagent and awaits it. Without a schema it resolves to the subagent's final text. With `options.schema` it resolves to the parsed JSON value. - -**Identical calls are memoized into one subagent.** Each call is journaled by its canonical prompt and options, including `label`. Two calls with the same prompt and the same options return one shared result — even when issued concurrently. To spawn N *independent* subagents, give each a unique `label` or vary the prompt: - -```js -// One subagent, awaited five times — almost certainly not what you want. -await ctx.parallel([1, 2, 3, 4, 5].map(() => () => ctx.agent("Find a bug"))); - -// Five independent subagents. -await ctx.parallel( - [1, 2, 3, 4, 5].map((i) => () => ctx.agent("Find a bug", { label: `finder:${i}` })) -); -``` - -**An ordinary failure resolves to `null` — it does not throw.** A subagent that errors, returns nothing, or (with a schema) produces output that still fails to parse or match after its one retry resolves `null`. Always guard the result before using it, including a bare `await ctx.agent(...)`: - -```js -const finding = await ctx.agent(prompt, { label: "inspector" }); -if (!finding) return { finding: null }; -``` - -Cancellation and hard runtime failures — a reached limit, a durable-state failure — reject instead, aborting the run. When filtering results, prefer `v => v !== null` over `Boolean`, which also discards a valid `false`, `0`, or `""`. - -**`schema` is a structural subset of JSON Schema, not a validator.** Honored: `type`, `required`, `enum`, `const`, recursive `properties`/`items`, and `anyOf`/`oneOf`/`allOf` — where `oneOf` is treated as `anyOf`, meaning at least one branch matches rather than exactly one. Ignored and *not* enforced: `additionalProperties`, `pattern`, `minLength`/`maxLength`, `format`, numeric ranges, and boolean schemas. Do not rely on an ignored keyword to constrain a result. A schema call retries once on a parse or match failure, so it may spawn twice, and both spawns count toward `maxTotalSubagents`. - -### Choosing between pipeline and parallel - -Prefer `pipeline` for multi-stage work. It has no barrier between stages, so each item advances as soon as its own prior stage finishes. - -Reach for a barrier — `parallel` between stages — only when a stage genuinely needs every prior result at once: deduplicating or merging across the full set, an early exit based on the total, or a prompt that compares one result against the others. Needing to map, filter, or flatten is not a reason to use a barrier; do that inside a pipeline stage. Barrier latency is real: if the slowest of N subagents takes three times the fastest, a barrier wastes the rest of the pool's time. - -See [factory-patterns.md](./factory-patterns.md) for composable orchestration patterns built on these primitives. - -## Resource limits - -Limits may be declared in `meta.limits` and overridden per invocation. Every limit is optional and must be positive when present; an omitted limit leaves that dimension unbounded, except that an omitted `maxConcurrentSubagents` falls back to `maxTotalSubagents`, so a declared total cap also bounds concurrency. - -Set a ceiling only from real knowledge of what the factory costs, or because the user named one. A guessed ceiling does not make a run safer: it stops a healthy run partway with `factory_limit_reached`, after that run has already spent credits. An agent authoring or invoking a factory on the user's behalf has no basis for estimating a number, so it should leave `limits` unset and bound the work with the factory's own counters instead. Omitting limits does not remove oversight of a model-initiated run: `run_factory` requests permission first, and that prompt shows the effective limits. SDK-initiated `run` and `resume` do not request permission, so an SDK caller that wants a ceiling sets it deliberately, from the cost it already knows. - -```js -// Only when the cost profile is known, or the user asked for this ceiling. -limits: { maxTotalSubagents: 10 }, -``` - -- `maxConcurrentSubagents`: Positive integer concurrent-subagent cap. Additional subagents wait in a queue. Queueing applies backpressure and does not fail the run. -- `maxTotalSubagents`: Positive integer cumulative admission cap. An attempted subagent beyond the cap ends the attempt with failure kind `maxTotalSubagents`. -- `timeoutSeconds`: Positive finite number of seconds, including positive fractions, capped at `2_147_483.647`. It measures accumulated active-execution time across attempts, including the extension body, subprocess waits, queued-agent waits, and sleeps. Time between attempts is excluded. The timeout is soft because already-running work may take time to stop. Its failure kind is `timeoutSeconds`. -- `maxAiCredits`: Positive finite AI-credit budget for the whole run's factory subagent subtree, including descendants. AI credits are GitHub Copilot's universal usage metric. This is a soft, post-paid ceiling, so completed or parallel turns can settle above it before the run stops. Accounting is fail-closed: an accounting failure stops a budgeted run rather than allowing untracked use. Its failure kind is `maxAiCredits`. - -`maxTotalSubagents`, `timeoutSeconds`, and `maxAiCredits` use reject-and-retry semantics. A rejected attempt ends with run status `error` and `failure.type` set to `factory_limit_reached`. The failed run keeps its ID, arguments, journal, and accounting. Resume the run with a raised limit when additional work is approved. Previously consumed resources still count. - -## Run and resume - -Run by registered name or handle: - -```ts -const run = await session.factory.run("review-changed", { - args: { files: ["src/a.ts"] }, - limits: { maxAiCredits: 3 }, - notifyOnComplete: true, - logPhaseNames: true, -}); - -if (run.status === "completed") { - console.log(run.result); -} else { - console.error(`run ${run.runId} ended as ${run.status}`, run.failure ?? run.error); -} -``` - -The name overload is: - -```ts -session.factory.run( - name: string, - options?: { - args?: JsonValue; - limits?: FactoryLimitOverrides; - notifyOnComplete?: boolean; - logPhaseNames?: boolean; - }, -): Promise; -``` - -Resume by run ID without resending the name or arguments: - -```ts -const run = await session.factory.resume(runId, { - limits: { maxAiCredits: 6 }, - notifyOnComplete: true, - logPhaseNames: true, -}); -``` - -The signature is: - -```ts -session.factory.resume( - runId: string, - options?: { - limits?: FactoryLimitOverrides; - notifyOnComplete?: boolean; - logPhaseNames?: boolean; - }, -): Promise; -``` - -Set `notifyOnComplete` to `true` for factories that are likely to be invoked by an agent, so the originating session is notified when the factory completes. Set it to `false` for factories intended to be invoked programmatically, where the caller awaits the result directly. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs. - -Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome—`completed`, `error`, `halted`, `paused`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. A `paused` envelope means that the current attempt settled, not that the durable run is permanently finished. Resume the same run ID to start another attempt with its journal and accounting intact. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`. - -An agent that no longer has a prior run's ID in context can recover it with `factories_manage` and `operation: "runs"`, which lists the session's factory runs with their IDs and statuses. This matters for resume: a run that reached a limit keeps its journal, so resuming it replays completed work for free, while restarting it from scratch pays for that work twice. - -Pause a running attempt from outside its factory body: - -```ts -const paused = await session.factory.pause(runId); -``` - -Inside a factory body, use a durable checkpoint instead: - -```ts -await ctx.step("prepare", prepareInput); -await ctx.pause("review-ready"); -await ctx.agent("Review the prepared input"); -``` - -The first attempt pauses at `"review-ready"` and ends through cooperative cancellation. On resume, the factory starts from the beginning, reuses the journaled step, returns from the checkpoint, and continues. - -The agent-facing `run_factory` tool has exactly two input branches: - -```ts -{ name: string; args?: JsonValue; limits?: FactoryLimitOverrides } -{ resumeFromRunId: string; limits?: FactoryLimitOverrides } -``` - -## Authoring a factory from inside a session - -The agent-facing `factories_manage` tool writes a factory into a session-scoped extension at runtime with `operation: "author"`. The rules above all apply, plus one constraint that does not affect an extension author. - -**The `run` body is self-contained.** It is emitted verbatim into a generated module as a single async function expression. It closes over nothing: not the conversation that authored it, and not any authoring-time binding. Only its own locals, its `ctx` parameter, and standard Node and JavaScript globals are in scope, so every schema, constant, and helper must be defined *inside* the function. The generated module imports the SDK itself; the expression cannot add static `import` statements or use `require`. Load anything else with a dynamic `await import("...")` in the body. - -```js -async ({ args, agent, phase }) => { - // Defined inside — there is no outer scope to close over. - const VERDICT = { type: "object", properties: { real: { type: "boolean" } }, required: ["real"] }; - - phase("Inspect"); - const finding = await agent(`Name one likely bug in ${args.file ?? "the code"}.`, { - label: "inspector", - }); - if (!finding) return { finding: null, real: false }; - - phase("Verify"); - const verdict = await agent(`Is this a real bug? Claim: ${finding}`, { - label: "verifier", - schema: VERDICT, - }); - return { finding, real: verdict?.real === true }; -}; -``` - -Authoring registers the factory but does not run it. Invoke it afterwards with `run_factory`. Use `factories_manage` with `operation: "list"` to see the factories already registered in the session and `operation: "inspect"` to read one factory's description, phases, declared argument shape, and limits before running it. - -## Observe a run - -The calling session can inspect its own factory runs: - -```ts -const runs = await session.factory.listRuns(); -const runsPage = await session.factory.listRuns({ - afterSeq, - beforeSeq, - limit, -}); -const detail = await session.factory.getRunDetail(runId); -const progressPage = await session.factory.getRunProgress(runId, { - phaseId, - afterSeq, - beforeSeq, - limit, -}); -``` - -- `listRuns()` returns only the runs array from the newest default page of this session's durable factory runs. This overload preserves the original convenience API. -- `listRuns({ afterSeq, beforeSeq, limit })` returns the full page. Its `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` fields let callers continue paging without raw RPC calls. -- `getRunDetail(runId)` returns phases, prompt-safe agent summaries, and the latest progress page. -- `getRunProgress(runId, options?)` pages progress forward, backward, by phase, or from the latest tail. - -`getRun(runId)` reads the latest run envelope. `pause(runId)` pauses a running attempt and returns its `paused` envelope. `cancel(runId)` cancels a run and returns its terminal envelope. - -`waitForRun(runId, options?)` resolves with the current attempt's envelope once it settles into `completed`, `error`, `halted`, `paused`, or `cancelled`. It resolves immediately when the current attempt has already settled: - -```ts -const settled = await session.factory.waitForRun(runId); -if (settled.status === "completed") { - console.log(settled.result); -} -``` - -It watches `factory.run_updated` and re-reads the durable envelope on each invalidation, collapsing a burst of events into a single in-flight read. A low-frequency periodic re-read runs alongside the subscription, so a dropped or missing invalidation degrades into a slightly late resolution rather than an unbounded wait. Pass a `signal` to stop waiting: - -```ts -const controller = new AbortController(); -setTimeout(() => controller.abort(), 30_000); -const settled = await session.factory.waitForRun(runId, { signal: controller.signal }); -``` - -Aborting rejects the wait and has no effect on the run, which keeps executing—use `pause(runId)` or `cancel(runId)` to stop it. The resolved object is a snapshot of that settled attempt. If its status is `paused`, a later resume updates the durable envelope under the same run ID. Call `getRun(runId)` to read the latest envelope. `isFactoryRunTerminal(status)` exposes the same current-attempt settlement test for callers driving their own loop. - -Listen for the ephemeral `factory.run_updated` event. Its `{ runId, revision }` payload is an invalidation signal. Re-read the desired API when a newer monotonic revision arrives. - -Revisions cover durable lifecycle, accounting, phase, agent, and progress changes. Continuous read-time fields can change without a new revision. These include `observedAt`, active-time calculations, live counts, and a live agent's status or prompt-safe activity text. Factory prompts are never exposed by these APIs. A run is visible only through the session that owns it. diff --git a/nodejs/docs/factory-patterns.md b/nodejs/docs/factory-patterns.md deleted file mode 100644 index 8ae403d55a..0000000000 --- a/nodejs/docs/factory-patterns.md +++ /dev/null @@ -1,194 +0,0 @@ -# Agent Factory patterns - -Composable orchestration patterns built on the factory context. Read [factories.md](./factories.md) first for the API and its semantics. The API is experimental. - -Every snippet below assumes the surrounding `async (ctx) => { ... }` run body and destructures the hooks it uses. Three rules apply throughout, because breaking them fails silently: - -- **Give every independent subagent a unique `label`.** Identical prompt-and-options pairs memoize into a single shared subagent. -- **Guard every `agent()` result.** An ordinary failure resolves to `null` rather than throwing. -- **Filter with `v => v !== null`,** not `Boolean`, which also discards a valid `false`, `0`, or `""`. - -## Multi-stage review - -The default shape: fan out across dimensions, and let each dimension verify as soon as its own review lands. No barrier, so a slow dimension never holds up a fast one. - -```js -async ({ pipeline, parallel, agent, phase, log }) => { - const FINDINGS = { - type: "object", - properties: { - findings: { - type: "array", - items: { - type: "object", - properties: { title: { type: "string" } }, - required: ["title"], - }, - }, - }, - required: ["findings"], - }; - const VERDICT = { - type: "object", - properties: { isReal: { type: "boolean" } }, - required: ["isReal"], - }; - const DIMENSIONS = [ - { key: "bugs", prompt: "Review the diff for correctness bugs. Return JSON {findings:[{title}]}." }, - { key: "perf", prompt: "Review the diff for performance issues. Return JSON {findings:[{title}]}." }, - ]; - - phase("Review"); // Run-global: set it before the fan-out, never inside a stage. - const perDimension = await pipeline( - DIMENSIONS, - (d) => agent(d.prompt, { label: `review:${d.key}`, schema: FINDINGS }), - (review, d) => { - if (!review) { - log(`review:${d.key} produced nothing`); - return []; - } - return parallel( - (review.findings ?? []).map((f, i) => () => - agent(`Adversarially verify this finding is real: ${f.title}`, { - label: `verify:${d.key}:${i}`, - schema: VERDICT, - }).then((v) => (v && v.isReal ? f : null)) - ) - ); - } - ); - - return { confirmed: perDimension.flat().filter((v) => v !== null) }; -}; -``` - -## When a barrier is correct - -Deduplicating across every finding needs the whole set in hand, so the barrier earns its cost here. Dedup itself is plain JavaScript, done in the body between the two fan-outs. This excerpt reuses `FINDINGS`, `VERDICT`, and `DIMENSIONS` from the previous example — define them inside your own function. - -```js -const all = await parallel( - DIMENSIONS.map((d) => () => agent(d.prompt, { label: `find:${d.key}`, schema: FINDINGS })) -); -const findings = all.filter((v) => v !== null).flatMap((r) => r.findings ?? []); -const deduped = [...new Map(findings.map((f) => [f.title, f])).values()]; // Needs all of them. -const verified = await parallel( - deduped.map((f, i) => () => agent(`Verify: ${f.title}`, { label: `verify:${i}`, schema: VERDICT })) -); -``` - -## Loop until count - -Accumulate toward a target. Each iteration needs a unique identity — a unique label plus a prompt that excludes what has already been found — a bounded attempt count, and a null guard. - -```js -const BUG = { - type: "object", - properties: { title: { type: "string" } }, - required: ["title"], -}; - -const bugs = []; -let attempt = 0; -while (bugs.length < 10 && attempt < 30) { - const r = await agent( - `Find ONE distinct bug NOT already listed: ${JSON.stringify(bugs.map((b) => b.title))}. Return JSON {title}.`, - { label: `finder:${attempt}`, schema: BUG } - ); - attempt++; - if (r && r.title) bugs.push(r); - log(`${bugs.length}/10 found`); -} -``` - -## Loop until dry - -Keep spawning finders until some number of consecutive rounds surface nothing new. Deduplicate against everything *seen*, not just what was kept, or discarded findings resurface every round. - -```js -const BUGS = { - type: "object", - properties: { - bugs: { - type: "array", - items: { type: "object", properties: { title: { type: "string" } }, required: ["title"] }, - }, - }, - required: ["bugs"], -}; -const VERDICT = { - type: "object", - properties: { real: { type: "boolean" } }, - required: ["real"], -}; - -const seen = new Set(); -const confirmed = []; -const keyOf = (b) => b.title.toLowerCase(); -let dry = 0; -let round = 0; - -while (dry < 2 && round < 20) { - const found = ( - await parallel( - [0, 1, 2].map((i) => () => - agent(`Find bugs (finder ${i}, round ${round}). Return JSON {bugs:[{title}]}.`, { - label: `find:${round}:${i}`, - schema: BUGS, - }) - ) - ) - ) - .filter((v) => v !== null) - .flatMap((r) => r.bugs ?? []); - - const fresh = found.filter((b) => { - const k = keyOf(b); - if (seen.has(k)) return false; - seen.add(k); - return true; - }); - - if (!fresh.length) { - dry++; - round++; - continue; - } - dry = 0; - - const judged = await parallel( - fresh.map((b, i) => () => - parallel( - ["correctness", "security", "repro"].map((lens) => () => - agent(`Judge via ${lens}: is "${b.title}" real? Return JSON {real}.`, { - label: `judge:${round}:${i}:${lens}`, - schema: VERDICT, - }) - ) - ).then((vs) => ({ b, real: vs.filter((v) => v !== null).filter((v) => v.real).length >= 2 })) - ) - ); - - confirmed.push(...judged.filter((v) => v !== null && v.real).map((v) => v.b)); - round++; -} -``` - -## Quality patterns - -Compose these freely. - -- **Adversarial verify.** Spawn several independent skeptics per finding, each prompted to *refute* it and to default to refuted when uncertain. Keep only what a majority fails to refute. -- **Perspective-diverse verify.** Give each verifier a distinct lens — correctness, security, performance, does-it-reproduce — instead of several identical skeptics. The distinct prompts also stop them memoizing into one subagent. -- **Judge panel.** Generate several independent attempts from different angles, score them with parallel judges, then synthesize from the winner while grafting the best ideas from the runners-up. -- **Multi-modal sweep.** Run parallel searchers that each look a different way: by container, by content, by entity, by time. -- **Completeness critic.** End with an agent asking what is missing — an angle not run, a claim unverified, a source unread — and use its answer to seed the next round. -- **No silent caps.** When the factory bounds its own coverage with a top-N, a sampling step, or a no-retry rule, `log()` what was dropped. - -## Scaling - -Match the orchestration to what was asked. A quick check wants a couple of subagents and single-vote verification; a request to be thorough or comprehensive wants a larger finder pool, a three-to-five vote adversarial pass, and a synthesis stage. - -There is no in-script budget object. Scale with your own counters, as in the loop patterns above, and treat any declared limits as the safety ceiling rather than the control mechanism. Only `agent()` spawns are throttled, by `maxConcurrentSubagents` falling back to `maxTotalSubagents`; with neither declared there is no built-in concurrency cap, so bound a wide fan-out with the factory's own counters. Do not invent a ceiling to compensate, and see [Resource limits](./factories.md#resource-limits) for when declaring one is appropriate. `parallel` itself is `Promise.all`, so non-agent work in a thunk runs fully concurrently regardless. - -These patterns are not exhaustive. Compose novel harnesses — tournament brackets, self-repair loops, staged escalation — when the task calls for it. diff --git a/nodejs/docs/workflows.md b/nodejs/docs/workflows.md index 821778fcd8..872cbcb0fe 100644 --- a/nodejs/docs/workflows.md +++ b/nodejs/docs/workflows.md @@ -2,8 +2,6 @@ Dynamic Workflows are extension-authored, session-scoped workflows that coordinate subagents and durable steps. The API is experimental. -Use Dynamic Workflows for new extension-authored orchestration. Existing Agent Factory extensions remain supported during the transition, but one `joinSession` call must register either `workflows` or `factories`, never both. - ## Define and register a workflow Use `defineWorkflow` and pass the returned handle to `joinSession`: @@ -242,7 +240,7 @@ if (settled.status === "completed") { } ``` -It watches the runtime's `factory.run_updated` compatibility event and re-reads the durable envelope on each invalidation, collapsing a burst of events into a single in-flight read. A low-frequency periodic re-read runs alongside the subscription, so a dropped or missing invalidation degrades into a slightly late resolution rather than an unbounded wait. Pass a `signal` to stop waiting: +It watches `workflow.run_updated` and re-reads the durable envelope on each invalidation, collapsing a burst of events into a single in-flight read. A low-frequency periodic re-read runs alongside the subscription, so a dropped or missing invalidation degrades into a slightly late resolution rather than an unbounded wait. Pass a `signal` to stop waiting: ```ts const controller = new AbortController(); @@ -252,6 +250,6 @@ const settled = await session.workflow.waitForRun(runId, { signal: controller.si Aborting rejects the wait and has no effect on the run, which keeps executing—use `pause(runId)` or `cancel(runId)` to stop it. The resolved object is a snapshot of that settled attempt. If its status is `paused`, a later resume updates the durable envelope under the same run ID. Call `getRun(runId)` to read the latest envelope. `isWorkflowRunTerminal(status)` exposes the same current-attempt settlement test for callers driving their own loop. -Listen for the ephemeral `factory.run_updated` compatibility event. Its `{ runId, revision }` payload is an invalidation signal. Re-read the desired API when a newer monotonic revision arrives. +Listen for the ephemeral `workflow.run_updated` event. Its `{ runId, revision }` payload is an invalidation signal. Re-read the desired API when a newer monotonic revision arrives. Revisions cover durable lifecycle, accounting, phase, agent, and progress changes. Continuous read-time fields can change without a new revision. These include `observedAt`, active-time calculations, live counts, and a live agent's status or prompt-safe activity text. Workflow prompts are never exposed by these APIs. A run is visible only through the session that owns it. 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-lock.json b/nodejs/package-lock.json index 4214cc8cd7..937a26efa1 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -14,6 +14,7 @@ "zod": "4.3.6" }, "devDependencies": { + "@microsoft/agent-host-protocol-v09": "npm:@microsoft/agent-host-protocol@0.9.0", "@platformatic/vfs": "^0.3.0", "@types/node": "^25.2.0", "@types/semver": "7.8.0", @@ -1006,6 +1007,13 @@ "url": "https://liberapay.com/Koromix" } }, + "node_modules/@microsoft/agent-host-protocol-v09": { + "name": "@microsoft/agent-host-protocol", + "version": "0.9.0", + "integrity": "sha512-XY6zOqLSqlNianiWbEpZkzKRB2AKwcDPk9yAbp+afxyxhWzTfpJKy+03T5rIipJDnqf45rXiJ5JV/dzUA8JHLw==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.3", "integrity": "sha1-l+PUXXQk3F2h1OMvO/OykvbBtEw=", diff --git a/nodejs/package.json b/nodejs/package.json index f6187b73d8..0684757516 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.89-3", + "copilotCliVersion": "1.0.93-2", "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", @@ -71,6 +70,7 @@ "zod": "4.3.6" }, "devDependencies": { + "@microsoft/agent-host-protocol-v09": "npm:@microsoft/agent-host-protocol@0.9.0", "@platformatic/vfs": "^0.3.0", "@types/node": "^25.2.0", "@types/semver": "7.8.0", @@ -97,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..7cd973a833 --- /dev/null +++ b/nodejs/scripts/package-release.ts @@ -0,0 +1,379 @@ +/*--------------------------------------------------------------------------------------------- + * 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", + "linuxmusl-arm64": "linuxmusl.arm64", + "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..4a164d7e92 --- /dev/null +++ b/nodejs/scripts/publish-release.ts @@ -0,0 +1,391 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +type GithubRelease = { + tag_name: string; + draft: boolean; + prerelease: boolean; + published_at: string | null; +}; + +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: GithubRelease[], + 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; + } + let releases: GithubRelease[]; + if (identity.channel === "latest") { + try { + releases = [JSON.parse(gh(["api", `repos/${repository}/releases/latest`]))]; + } catch (error) { + if (!/\(HTTP 404\)/.test(String((error as { stderr?: string }).stderr))) throw error; + releases = []; + } + } else { + const output = gh([ + "api", + "--paginate", + "--jq", + ".[] | {tag_name, draft, prerelease, published_at}", + `repos/${repository}/releases?per_page=100`, + ]); + releases = output ? output.split("\n").map((line) => JSON.parse(line)) : []; + } + 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 a2c01fa144..3f1def373f 100644 --- a/nodejs/src/cliVersion.ts +++ b/nodejs/src/cliVersion.ts @@ -1,3 +1,3 @@ -export const COPILOT_CLI_VERSION = "1.0.89-3"; +export const COPILOT_CLI_VERSION = "1.0.93-2"; export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index f4f911c273..af1cce1f51 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -17,7 +17,9 @@ import { existsSync } from "node:fs"; import { isIPv6, Socket } from "node:net"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { - createMessageConnection, + type CancellationToken, + createMessageConnection as createRpcMessageConnection, + type DataCallback, ErrorCodes, type Message, MessageConnection, @@ -36,17 +38,23 @@ import type { GitHubTelemetryNotification, GitHubTokenAcquireRequest, GitHubTokenAcquireResult, + InstallationConfirmationRequest, OpenCanvasInstance, SessionUpdateOptionsParams, TaskKind, } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; +import { AhpHost, type AhpHostExit, type AhpHostOptions } from "./host.js"; import type { FfiRuntimeHost } from "./ffiRuntimeHost.js"; import { ensureRuntimeBundle } from "./runtimeArtifacts.js"; import { COPILOT_CLI_VERSION } from "./cliVersion.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { createCopilotRequestAdapter } from "./copilotRequestHandler.js"; +import { + createInstallationConfirmationAdapter, + type InstallationConfirmationHandler, +} from "./installationConfirmation.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; import { getTraceContext } from "./telemetry.js"; import { toJsonSchema } from "./schema.js"; @@ -90,14 +98,13 @@ import type { SystemMessageCustomizeConfig, TelemetryConfig, TraceContextProvider, + TranscriptRecovery, TypedSessionLifecycleHandler, } from "./types.js"; import { defaultJoinSessionPermissionHandler } from "./types.js"; -import type { FactoryHandle } from "./factory.js"; import type { WorkflowHandle } from "./workflow.js"; interface ExtensionOrchestrationContributions { - factories?: FactoryHandle[]; workflows?: WorkflowHandle[]; } @@ -107,6 +114,66 @@ 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 }; + +function createMessageConnection( + reader: StreamMessageReader, + writer: StreamMessageWriter +): DrainingMessageConnection { + let dispatch: DataCallback | undefined; + let finishDrain: (() => void) | undefined; + let drained: Promise | undefined; + let disposed = false; + const barrier = { jsonrpc: "2.0", method: "$/sdkDrain" }; + const connection = createRpcMessageConnection( + { + onError: reader.onError, + onClose: reader.onClose, + onPartialMessage: reader.onPartialMessage, + listen: (callback) => { + dispatch = callback; + return reader.listen(callback); + }, + dispose: () => reader.dispose(), + }, + writer, + undefined, + { + messageStrategy: { + handleMessage: (message, next) => { + if (message === barrier) { + finishDrain?.(); + } else { + next(message); + } + }, + }, + } + ); + connection.onDispose(() => { + disposed = true; + finishDrain?.(); + }); + return Object.assign(connection, { + drain: () => { + if (!drained) { + drained = new Promise((resolve) => { + finishDrain = resolve; + if (disposed || !dispatch) { + resolve(); + } else { + // A local queue barrier preserves every message parsed before EOF. + // It is never sent on the wire or exposed to notification handlers. + dispatch(barrier); + } + }); + } + return drained; + }, + }); +} async function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { let timeout: ReturnType | undefined; @@ -396,8 +463,8 @@ function getBundledRuntimePath(): Promise { * A {@link StreamMessageWriter} that suppresses write failures while the client * is tearing down its transport. * - * During `stop()`/`forceStop()` the runtime's end of the pipe can close while - * vscode-jsonrpc still has an in-flight write — most commonly the + * During `stop()`/`forceStop()` or transport EOF the runtime's end of the pipe + * can close while vscode-jsonrpc still has an in-flight write — most commonly the * auto-generated response to a server→client request (tool/hook/userInput/LLM * inference handler) that resolved just before teardown. That write rejects * with `ERR_STREAM_DESTROYED`, and because the response write is internal to @@ -425,10 +492,11 @@ export class CopilotClient { private cliStartTimeout: ReturnType | null = null; private cliProcess: ChildProcess | null = null; private ffiHost: FfiRuntimeHost | null = null; - private connection: MessageConnection | null = null; + private connection: DrainingMessageConnection | null = null; 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"; @@ -436,6 +504,20 @@ export class CopilotClient { /** Shared in-flight start; concurrent callers await it instead of spawning another CLI. */ private startPromise: Promise | null = null; private sessions: Map = new Map(); + private hostExitCallbacks = new Map void>(); + private hostSessionFactories = new Map(); + private hostHandoffs = new Map< + string, + { + hostId: string; + requestedSessionId?: string; + controller: AbortController; + configs: Map; + session?: CopilotSession; + released: boolean; + onReleased?: AhpHostOptions["onSessionReleased"]; + } + >(); private stderrBuffer: string = ""; // Captures CLI stderr for error messages /** Resolved connection mode chosen in the constructor. */ private connectionConfig: InternalRuntimeConnection; @@ -477,6 +559,7 @@ export class CopilotClient { private sessionFsConfig: SessionFsConfig | null = null; private requestHandler: CopilotRequestHandler | null = null; private extensionLaunchProvider?: ExtensionLaunchProvider; + private installationConfirmationHandler?: InstallationConfirmationHandler; private builtinPluginDirectories: string[] = []; private onGitHubTelemetry?: (notification: GitHubTelemetryNotification) => void | Promise; private clientGlobalHandlers: import("./generated/rpc.js").ClientGlobalApiHandlers = {}; @@ -676,6 +759,7 @@ export class CopilotClient { this.sessionFsConfig = options.sessionFs ?? null; this.requestHandler = options.requestHandler ?? null; this.extensionLaunchProvider = options.extensionLaunchProvider; + this.installationConfirmationHandler = options.installationConfirmationHandler; this.onGitHubTelemetry = options.onGitHubTelemetry; this.setupClientGlobalHandlers(); @@ -816,11 +900,24 @@ 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); } private setupClientGlobalHandlers(): void { const handlers: import("./generated/rpc.js").ClientGlobalApiHandlers = {}; + handlers.host = { + exited: async (exit) => { + this.handleHostExit(exit); + }, + }; handlers.extensionLaunchProvider = this.extensionLaunchProvider; if (this.requestHandler) { this.requestAdapter = createCopilotRequestAdapter(this.requestHandler, () => { @@ -941,6 +1038,7 @@ export class CopilotClient { private async doStart(): Promise { this.forceStopping = false; + this.connectionEpoch++; this.connectionClosed = false; this.processTransportError = null; this.state = "connecting"; @@ -991,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; @@ -1007,7 +1108,8 @@ export class CopilotClient { * 1. Closes all active sessions (releases in-memory resources) * 2. Requests runtime shutdown for SDK-owned CLI processes * 3. Closes the JSON-RPC connection - * 4. Terminates the CLI server process (if spawned by this client) + * 4. Signals EOF to an owned stdio process and waits for host cleanup, then + * terminates the process if it does not exit within the shutdown timeout * * Note: session data on disk is preserved, so sessions can be resumed later. * To permanently remove session data before stopping, call @@ -1146,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( @@ -1160,15 +1276,33 @@ export class CopilotClient { } } - // The runtime completes all cleanup before responding to - // runtime.shutdown and then leaves termination to us; it deliberately - // keeps its JSON-RPC server alive to send the response and never - // self-exits. Waiting a grace window for a self-exit that will never - // come just wastes time, so terminate the child immediately and only - // wait to reap it. if (this.cliProcess && !this.isExternalServer) { const child = this.cliProcess; - this.cliProcess = null; + if ( + this.connectionConfig.kind === "stdio" && + child.stdin && + child.exitCode == null && + child.signalCode == null + ) { + const gracefulExitStart = Date.now(); + try { + // Host telemetry is finalized after transport EOF, not the shutdown RPC. + child.stdin.end(); + const exited = await waitForChildExit(child, RUNTIME_SHUTDOWN_TIMEOUT_MS); + this.logDebugTiming( + exited + ? "CopilotClient.stop graceful stdio exit complete" + : "CopilotClient.stop graceful stdio exit timed out; terminating child", + gracefulExitStart + ); + } catch (error) { + errors.push( + new Error( + `Failed to close CLI stdin: ${error instanceof Error ? error.message : String(error)}` + ) + ); + } + } try { if (child.exitCode == null && child.signalCode == null) { child.kill(); @@ -1186,6 +1320,10 @@ export class CopilotClient { `Failed to kill CLI process: ${error instanceof Error ? error.message : String(error)}` ) ); + } finally { + if (this.cliProcess === child) { + this.cliProcess = null; + } } } // Tear down the in-process FFI host (closes the native connection and @@ -1212,6 +1350,7 @@ export class CopilotClient { this.runtimePort = null; this.stderrBuffer = ""; this.processExitPromise = null; + this.disconnectHosts(); return errors; } @@ -1259,6 +1398,7 @@ export class CopilotClient { */ async forceStop(): Promise { this.forceStopping = true; + this.disconnectHosts(); // Clear sessions immediately without trying to destroy them for (const session of this.sessions.values()) { @@ -1522,6 +1662,9 @@ export class CopilotClient { if (config.gitHubToken !== undefined && config.gitHubTokenProvider !== undefined) { throw new Error("gitHubToken and gitHubTokenProvider are mutually exclusive"); } + if (config.cloud != null && config.skillProvider) { + throw new Error("Skill providers are not supported for cloud sessions."); + } if (!this.connection) { await this.start(); } @@ -1605,19 +1748,25 @@ export class CopilotClient { if (config.hooks) { s.registerHooks(config.hooks); } + if (config.skillProvider) { + s.registerSkillProvider(config.skillProvider); + } if (transformCallbacks) { s.registerTransformCallbacks(transformCallbacks); } if (config.onEvent) { s.on(config.onEvent); } - this.sessions.set(sessionId, s); + // Session-FS setup can throw, so finish it before the session + // becomes routable; a failed open must not leave handlers behind. 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` @@ -1639,6 +1788,7 @@ export class CopilotClient { const response = await this.connection!.sendRequest("session.create", { ...(await getTraceContext(this.onGetTraceContext)), model: config.model, + allowedModels: config.allowedModels, sessionId: localSessionId, clientName: config.clientName, reasoningEffort: config.reasoningEffort, @@ -1720,6 +1870,7 @@ export class CopilotClient { enableSessionStore: config.enableSessionStore, enableSkills: config.enableSkills, skillDirectories: config.skillDirectories, + ...(config.skillProvider ? { hasSkillProvider: true } : {}), pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, @@ -1756,7 +1907,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); @@ -1779,9 +1932,28 @@ 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; } + for (const entry of this.hostHandoffs.values()) { + if (entry.requestedSessionId === session.sessionId) { + entry.configs.set(session, config); + } + } return session; } @@ -1817,20 +1989,7 @@ export class CopilotClient { async resumeSessionForExtension( sessionId: string, config: ResumeSessionConfig, - factories?: FactoryHandle[], - extensionOptions?: ExtensionJoinOptions - ): Promise; - /** @internal */ - async resumeSessionForExtension( - sessionId: string, - config: ResumeSessionConfig, - contributions?: ExtensionOrchestrationContributions, - extensionOptions?: ExtensionJoinOptions - ): Promise; - async resumeSessionForExtension( - sessionId: string, - config: ResumeSessionConfig, - contributions: FactoryHandle[] | ExtensionOrchestrationContributions = {}, + contributions: ExtensionOrchestrationContributions = {}, extensionOptions?: ExtensionJoinOptions ): Promise { return this.resumeSessionInternal(sessionId, config, contributions, extensionOptions); @@ -1839,15 +1998,10 @@ export class CopilotClient { private async resumeSessionInternal( sessionId: string, config: ResumeSessionConfig, - contributions: FactoryHandle[] | ExtensionOrchestrationContributions = {}, + contributions: ExtensionOrchestrationContributions = {}, extensionOptions?: ExtensionJoinOptions ): Promise { - const { factories, workflows } = Array.isArray(contributions) - ? { factories: contributions, workflows: undefined } - : contributions; - if (factories !== undefined && workflows !== undefined) { - throw new Error("Session configuration cannot include both factories and workflows"); - } + const { workflows } = contributions; if (config.gitHubToken !== undefined && config.gitHubTokenProvider !== undefined) { throw new Error("gitHubToken and gitHubTokenProvider are mutually exclusive"); } @@ -1871,7 +2025,6 @@ export class CopilotClient { session.registerTools(config.tools); session.registerCanvases(config.canvases); session.registerCommands(config.commands); - session.registerFactories(factories); session.registerWorkflows(workflows); const { wireProvider: bearerWireProvider, @@ -1897,6 +2050,9 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } + if (config.skillProvider) { + session.registerSkillProvider(config.skillProvider); + } const modeDefaults = this.configDefaultsForMode(); config = { ...modeDefaults, ...config }; @@ -1913,10 +2069,13 @@ export class CopilotClient { if (config.onEvent) { session.on(config.onEvent); } - this.sessions.set(sessionId, session); + // Validation and session-FS setup can throw, so finish them before the + // replacement session becomes routable over any resident one. + const toolFilterOptions = this.resolveToolFilterOptions(config); this.setupSessionFs(session, config); + const replacedSession = this.sessions.get(sessionId); + this.sessions.set(sessionId, session); - const toolFilterOptions = this.resolveToolFilterOptions(config); const gitHubTokenProviderRegistrationId = this.registerGitHubTokenProvider( config.gitHubTokenProvider, sessionId @@ -1929,8 +2088,10 @@ export class CopilotClient { try { const response = await this.connection!.sendRequest("session.resume", { + allowedModels: config.allowedModels, ...(await getTraceContext(this.onGetTraceContext)), sessionId, + allowTranscriptRecovery: config.allowTranscriptRecovery, clientName: config.clientName, model: config.model, reasoningEffort: config.reasoningEffort, @@ -1958,7 +2119,6 @@ export class CopilotClient { })), toolSearch: config.toolSearch, canvases: config.canvases?.map((canvas) => canvas.declaration), - factories: factories?.map((factory) => factory.meta), workflows: workflows?.map((workflow) => workflow.meta), requestCanvasRenderer: config.requestCanvasRenderer, requestExtensions: config.requestExtensions, @@ -2014,6 +2174,7 @@ export class CopilotClient { defaultAgent: config.defaultAgent, agent: config.agent, skillDirectories: config.skillDirectories, + ...(config.skillProvider ? { hasSkillProvider: true } : {}), pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, @@ -2055,15 +2216,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, @@ -2075,16 +2238,260 @@ export class CopilotClient { this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId); } catch (e) { session._markDisconnected(); - this.sessions.delete(sessionId); + // The runtime keeps the resident session's bindings when a resume + // fails, so its callbacks must keep reaching the replaced session. + if (this.sessions.get(sessionId) === session) { + if (replacedSession) { + this.sessions.set(sessionId, replacedSession); + } else { + this.sessions.delete(sessionId); + } + } if (gitHubTokenProviderRegistrationId !== undefined) { this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId); } throw e; } + for (const entry of this.hostHandoffs.values()) { + if (entry.requestedSessionId === session.sessionId) { + entry.configs.set(session, { ...config, sessionId }); + } + } return session; } + /** + * Start a complete AHP listener hosted in-process by this runtime. + * + * This connection owns the host. Disposing it or disconnecting the client + * stops the listener without deleting sessions. The host uses a separate + * 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. + * 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 { + 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 { + localServer, + githubEnvironment, + onExit, + createSession, + resumeSession, + onSessionReleased, + } = options; + if (createSession || resumeSession) { + this.hostSessionFactories.set(hostId, { + createSession, + resumeSession, + onSessionReleased, + }); + } + if (onExit) { + this.hostExitCallbacks.set(hostId, onExit); + } + try { + const info = await rpc.host.start({ + hostId, + localServer, + githubEnvironment, + sessionFactory: createSession ? true : undefined, + resumeFactory: resumeSession ? true : undefined, + }); + return new AhpHost( + info, + async () => { + await rpc.host.dispose({ hostId }); + }, + (sessionId) => rpc.host.publishSession({ hostId, sessionId }) + ); + } catch (error) { + this.hostExitCallbacks.delete(hostId); + this.releaseHostSessions(hostId); + throw error; + } + } + + private handleHostExit(exit: AhpHostExit): void { + this.releaseHostSessions(exit.hostId); + const onExit = this.hostExitCallbacks.get(exit.hostId); + if (onExit) { + this.hostExitCallbacks.delete(exit.hostId); + const reportError = (error: unknown) => { + console.error("AHP host exit callback failed", { hostId: exit.hostId, error }); + }; + try { + void Promise.resolve(onExit(exit)).catch(reportError); + } catch (error) { + reportError(error); + } + } + } + + private disconnectHosts(): void { + for (const hostId of new Set([ + ...this.hostExitCallbacks.keys(), + ...this.hostSessionFactories.keys(), + ])) { + this.handleHostExit({ + hostId, + reason: "ownerDisconnected", + error: "Owner connection closed; runtime cleanup cannot be acknowledged on this connection.", + }); + } + } + + private releaseHostSessions(hostId: string): void { + this.hostSessionFactories.delete(hostId); + for (const [handoffId, entry] of this.hostHandoffs) { + if (entry.hostId === hostId) this.releaseHostSession(handoffId); + } + } + + private releaseHostSession(handoffId: string): void { + const entry = this.hostHandoffs.get(handoffId); + if (!entry) return; + this.hostHandoffs.delete(handoffId); + entry.released = true; + entry.configs.clear(); + entry.controller.abort(); + if (entry.session) this.notifyHostSessionReleased(entry); + } + + private notifyHostSessionReleased(entry: { + session?: CopilotSession; + onReleased?: AhpHostOptions["onSessionReleased"]; + }): void { + const { session, onReleased } = entry; + entry.session = undefined; + if (session && onReleased) { + try { + void Promise.resolve(onReleased(session)).catch(() => { + console.error("AHP session release callback failed"); + }); + } catch { + console.error("AHP session release callback failed"); + } + } + } + + private async createHostSession(params: { + hostId: string; + handoffId: string; + resume?: boolean; + config: Omit & { configDir?: string }; + }): Promise<{ sessionId: string }> { + const options = this.hostSessionFactories.get(params.hostId); + if ( + !(params.resume ? options?.resumeSession : options?.createSession) || + this.hostHandoffs.has(params.handoffId) + ) { + throw new Error("AHP session factory is unavailable or handoff already exists"); + } + const entry: { + hostId: string; + requestedSessionId?: string; + controller: AbortController; + configs: Map; + session?: CopilotSession; + released: boolean; + onReleased?: AhpHostOptions["onSessionReleased"]; + } = { + hostId: params.hostId, + requestedSessionId: params.config.sessionId, + controller: new AbortController(), + configs: new Map(), + released: false, + onReleased: options!.onSessionReleased, + }; + this.hostHandoffs.set(params.handoffId, entry); + const { configDir, ...config } = params.config; + if (configDir !== undefined) config.configDirectory = configDir; + const expected = structuredClone(config); + const cancelled = new Promise((_resolve, reject) => { + entry.controller.signal.addEventListener( + "abort", + () => { + reject(new Error("AHP session handoff ended")); + }, + { once: true } + ); + }); + const materialized = Promise.resolve() + .then(() => { + if (params.resume) { + const { sessionId, ...resumeConfig } = config; + if (!sessionId) throw new Error("AHP resume requires sessionId"); + return options!.resumeSession!({ + sessionId, + config: resumeConfig, + signal: entry.controller.signal, + }); + } + return options!.createSession!({ + config, + signal: entry.controller.signal, + }); + }) + .then((session) => { + entry.session = session; + const actual = entry.configs.get(session); + entry.configs.clear(); + if (entry.released) { + this.notifyHostSessionReleased(entry); + throw new Error("AHP session handoff ended"); + } + if ( + this.sessions.get(session.sessionId) !== session || + session.sessionId !== expected.sessionId + ) { + throw new Error( + "AHP callback must return the requested session from this client" + ); + } + // Only host-selected keys constrain materialization. App prompt, + // filters and tools remain untouched when the host did not select them. + const contains = (value: unknown, required: unknown): boolean => { + if (required && typeof required === "object" && !Array.isArray(required)) { + return ( + !!value && + typeof value === "object" && + Object.entries(required).every(([key, child]) => + contains((value as Record)[key], child) + ) + ); + } + return JSON.stringify(value) === JSON.stringify(required); + }; + // Retained originals have no new materialization snapshot. Their + // resident workspace is checked by the runtime; do not reconfigure them. + if ((!params.resume || actual !== undefined) && !contains(actual, expected)) { + throw new Error( + "AHP callback must preserve the supplied session configuration" + ); + } + return { sessionId: session.sessionId }; + }); + try { + return await Promise.race([materialized, cancelled]); + } catch (error) { + this.releaseHostSession(params.handoffId); + throw error; + } + } + /** * Sends a ping request to the server to verify connectivity. * @@ -2611,6 +3018,9 @@ export class CopilotClient { if (this.options.mode === "empty") { env.COPILOT_DISABLE_KEYTAR = "1"; } + if (this.options.mode !== "empty") { + env.COPILOT_RUNTIME_PROCESS_FILE_LOGGING = "1"; + } if (this.options.telemetry) { const t = this.options.telemetry; env.COPILOT_OTEL_ENABLED = "true"; @@ -2825,8 +3235,13 @@ export class CopilotClient { case "inprocess": return this.connectViaFfi(); case "tcp": - case "uri": return this.connectViaTcp(); + case "uri": { + const { host, port } = this.parseCliUrl(this.connectionConfig.url); + this.actualHost = host; + this.runtimePort = port; + return this.connectViaTcp(); + } } } @@ -3087,6 +3502,29 @@ export class CopilotClient { // same connection. These methods carry no implicit sessionId dispatch // — the runtime calls into a single handler for the whole connection. registerClientGlobalApiHandlers(this.connection, this.clientGlobalHandlers); + if (this.installationConfirmationHandler) { + // Registered directly so the review receives the transport's request cancellation. + const confirmation = createInstallationConfirmationAdapter( + this.connection, + this.installationConfirmationHandler + ); + this.connection.onRequest( + "installations.confirm", + (params: InstallationConfirmationRequest, token: CancellationToken) => + confirmation.confirm(params, token) + ); + } + this.connection.onRequest("host.materializeSession", (params) => + this.createHostSession(params) + ); + this.connection.onNotification( + "host.sessionReleased", + (params: { hostId: string; handoffId: string }) => { + if (this.hostHandoffs.get(params.handoffId)?.hostId === params.hostId) { + this.releaseHostSession(params.handoffId); + } + } + ); // `hooks.invoke` is an internal RPC method: the runtime calls it to // invoke a hook callback on the client. Route each call to the matching @@ -3099,23 +3537,96 @@ export class CopilotClient { } ); + // Internal experimental callbacks backing `SessionConfigBase.skillProvider`. + this.connection.onRequest( + "skillProvider.list", + async (params: { sessionId: string }, token: CancellationToken) => + await this.resolveSkillProviderSession(params)._handleSkillProviderList(token) + ); + this.connection.onRequest( + "skillProvider.read", + async (params: { sessionId: string; name: string }, token: CancellationToken) => { + const session = this.resolveSkillProviderSession(params); + if (typeof params.name !== "string") { + throw new Error("Invalid skillProvider.read payload"); + } + return await session._handleSkillProviderRead(params.name, token); + } + ); + 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; } this.connectionClosed = true; this.state = "disconnected"; + this.disconnectHosts(); for (const session of this.sessions.values()) { session._markDisconnected(); } this.sessions.clear(); 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(markDisconnected); + 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"; } }); @@ -3266,6 +3777,17 @@ export class CopilotClient { return { response }; } + private resolveSkillProviderSession(params: { sessionId: string }): CopilotSession { + if (!params || typeof params.sessionId !== "string") { + throw new Error("Invalid skillProvider payload"); + } + const session = this.sessions.get(params.sessionId); + if (!session) { + throw new Error(`Session not found: ${params.sessionId}`); + } + return session; + } + private async handleHooksInvoke(params: { sessionId: string; hookType: string; diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index e1b9a7f72a..99a1c0a3be 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -9,7 +9,6 @@ import { type PermissionHandler, type ResumeSessionConfig, } from "./types.js"; -import type { FactoryHandle } from "./factory.js"; import type { WorkflowHandle } from "./workflow.js"; export { @@ -23,31 +22,9 @@ export { type CanvasOptions, } from "./canvas.js"; -type JoinSessionContributionConfig = - | { - /** - * Factory handles to register when the extension joins the session. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ - factories?: FactoryHandle[]; - workflows?: never; - } - | { - factories?: never; - /** - * Workflow handles to register when the extension joins the session. - * - * @experimental Part of the experimental Dynamic Workflows surface and may - * change or be removed in future SDK or CLI releases. - */ - workflows?: WorkflowHandle[]; - }; - export type JoinSessionConfig = Omit< ResumeSessionConfig, - "onPermissionRequest" | "extensionSdkPath" + "onPermissionRequest" | "extensionSdkPath" | "skillProvider" > & { onPermissionRequest?: PermissionHandler; /** @@ -79,37 +56,16 @@ export type JoinSessionConfig = Omit< * ``` */ requestedEnvironmentVariables?: string[]; -} & JoinSessionContributionConfig; + /** + * Workflow handles to register when the extension joins the session. + * + * @experimental Part of the experimental Dynamic Workflows surface and may + * change or be removed in future SDK or CLI releases. + */ + workflows?: WorkflowHandle[]; +}; -export type { ExtensionInfo, FactoryLimits, FactoryMeta } from "./types.js"; -export { - defineFactory, - FactoryResumeError, - isFactoryRunTerminal, - type RunOptions, - type ResumeOptions, - type FactoryResumeErrorCode, - type SessionFactoryApi, - type FactoryAgentOptions, - type FactoryContext, - type FactoryDefinition, - type FactoryHandle, - type FactoryJsonSchema, - type JsonValue, - type FactoryPipelineStage, - type FactoryStepOptions, - type FactoryRunResult, - type FactoryRunStatus, - type FactoryRunSummary, - type FactoryListRunsOptions, - type FactoryRunsPage, - type FactoryRunDetail, - type FactoryProgressPage, - type FactoryProgressLine, - type FactoryPhaseObservation, - type FactoryPhaseStatus, - type FactoryAgentSummary, -} from "./factory.js"; +export type { ExtensionInfo } from "./types.js"; export { defineWorkflow, WorkflowResumeError, @@ -123,6 +79,7 @@ export { type WorkflowContext, type WorkflowDefinition, type WorkflowHandle, + type JsonValue, type WorkflowJsonSchema, type WorkflowLimits, type WorkflowMeta, @@ -167,20 +124,21 @@ export async function joinSession(config: JoinSessionConfig = {}): Promise = new Set([ - "completed", - "halted", - "paused", - "cancelled", - "error", -]); - -/** - * Whether a factory run status is terminal. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export function isFactoryRunTerminal(status: FactoryRunStatus): boolean { - return FACTORY_TERMINAL_STATUSES.has(status); -} - -declare const factoryHandleBrand: unique symbol; - -/** A value that can be represented losslessly on the SDK JSON wire. */ -export type JsonValue = - | null - | boolean - | number - | string - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Conservative JSON shape language accepted by the Agent Factories surface, for - * both structured factory agent output and a factory's declared `argsSchema`. - * - * This is a best-effort structural guard — used to decide whether a subagent's - * structured output should be accepted or retried, and whether a caller's - * factory `args` match the declared shape — **not** a full JSON Schema - * validator. Only these keywords are honored: `type`, `required`, `enum`, - * `const`, recursive `properties`/`items`, and `anyOf`/`oneOf`/`allOf`. A `type` - * is one of `null`, `boolean`, `integer`, `number`, `string`, `array`, or - * `object`, or a non-empty array of those (for example `["object", "null"]`). - * - * Everything else is **ignored, not enforced**. In particular, string - * constraints (`pattern`, `minLength`, `maxLength`, `format`), numeric ranges - * (`minimum`, `maximum`), `additionalProperties`, and boolean (`true`/`false`) - * schemas do not reject non-conforming output. `oneOf` is treated like `anyOf` - * (at least one branch must match) rather than strict exactly-one. Author - * schemas within this subset; do not rely on unsupported constraints for - * correctness. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export type FactoryJsonSchema = { [key: string]: JsonValue }; - -/** - * Options for one factory-scoped subagent call. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryAgentOptions { - label?: string; - schema?: FactoryJsonSchema; - model?: string; - reasoningEffort?: string; - contextTier?: ContextTier; - agent?: string; -} - -export const FACTORY_AGENT_OPTION_KEYS = [ - "label", - "schema", - "model", - "reasoningEffort", - "contextTier", - "agent", -] as const; - -/** - * Options for a durable factory step. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryStepOptions { - /** Skip the journal and always invoke the producer. */ - volatile?: boolean; -} - -/** - * Per-invocation factory resource ceiling overrides. - * - * An omitted field preserves the existing/default ceiling, a number replaces - * it, and `null` explicitly makes that dimension unlimited. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryLimitOverrides { - maxConcurrentSubagents?: number | null; - maxTotalSubagents?: number | null; - maxAiCredits?: number | null; - timeoutSeconds?: number | null; -} - -/** - * One stage in a per-item factory pipeline. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export type FactoryPipelineStage = ( - previous: TInput, - item: unknown, - index: number -) => Promise | TResult; - -/** - * Context passed to an extension-authored factory body. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryContext { - /** Stable identifier for the current factory run. */ - readonly runId: string; - /** Spawn and await one factory-scoped subagent. */ - agent(prompt: string, options?: FactoryAgentOptions): Promise; - /** Memoize an arbitrary producer under a stable author-supplied key. */ - step( - key: string, - producer: () => Promise | JsonValue, - options?: FactoryStepOptions - ): Promise; - /** - * Pause this run at a durable, one-shot checkpoint. - * - * The first attempt to reach a key pauses and aborts cooperatively. A - * resumed attempt returns from the same key and continues. - */ - pause(key: string): Promise; - /** - * Run thunks concurrently and await all of them. - * - * A thunk that throws becomes `null` in the result array, so one failed - * item does not lose the rest. Cancellation and hard runtime failures - * (`ResponseError`, `ConnectionError`) are the exception: those propagate - * and reject the whole call, because they mean the run itself is in - * trouble rather than one item having failed. - */ - parallel( - thunks: Array<() => Promise | TResult> - ): Promise>; - /** - * Run each item through every stage without barriers between stages. - * - * A stage that throws drops that item to `null` and skips its remaining - * stages. As with {@link FactoryContext.parallel}, cancellation and hard - * runtime failures propagate instead of being recorded per item. - */ - pipeline(items: unknown[], ...stages: FactoryPipelineStage[]): Promise; - /** Start a named factory progress phase. */ - phase(title: string): void; - /** Emit a factory progress line. */ - log(message: string): void; - /** Reject because nested factories are not supported. */ - factory(name: string, args?: JsonValue): Promise; - /** Caller-supplied input, forwarded verbatim. */ - args: TArgs; - /** - * The session instance returned by `joinSession`. It refuses calls that - * start, resume, or pause a factory run. - */ - session: CopilotSession; - /** Cooperative cancellation signal for the current factory run. */ - signal: AbortSignal; -} - -/** - * Definition accepted by {@link defineFactory}. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryDefinition< - TArgs extends JsonValue = JsonValue, - TResult extends JsonValue | void = JsonValue | void, -> { - meta: FactoryMeta; - run(context: FactoryContext): Promise; -} - -/** - * A deeply immutable view of a value. - * - * `defineFactory` deep-freezes the metadata it stores, so the handle's view of - * it has to be readonly all the way down or `handle.meta.name = "..."` and - * `handle.meta.phases.push(...)` would compile and then throw at runtime. - */ -type DeepReadonly = T extends (infer U)[] - ? readonly DeepReadonly[] - : T extends object - ? { readonly [K in keyof T]: DeepReadonly } - : T; - -/** - * Opaque reusable reference to a defined factory. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryHandle< - TArgs extends JsonValue = JsonValue, - TResult extends JsonValue | void = JsonValue | void, -> { - readonly meta: DeepReadonly; - readonly [factoryHandleBrand]: { - readonly args: TArgs; - readonly result: TResult; - }; -} - -/** - * Options for invoking a factory. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface RunOptions { - /** Input surfaced as `context.args`. */ - args?: TArgs; - /** Optional per-invocation resource ceiling overrides. */ - limits?: FactoryLimitOverrides; - /** Whether to notify the originating session when the factory completes. */ - notifyOnComplete?: boolean; - /** Whether to emit factory phase names to the session transcript. */ - logPhaseNames?: boolean; - /** - * Prior run whose persisted identity, arguments, journal, and accounting should be resumed. - * - * @deprecated Use {@link SessionFactoryApi.resume} instead. - */ - resumeFromRunId?: string; -} - -/** - * Options for resuming a factory run by ID. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface ResumeOptions { - /** Optional per-invocation resource ceiling overrides. */ - limits?: FactoryLimitOverrides; - /** Whether to notify the originating session when the factory completes. */ - notifyOnComplete?: boolean; - /** Whether to emit factory phase names to the session transcript. */ - logPhaseNames?: boolean; -} - -/** - * Machine-readable pre-execution factory resume failure. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export type FactoryResumeErrorCode = - | "not_found" - | "non_resumable" - | "already_active" - | "factory_already_running" - | "factory_limits_invalid" - | "factory_session_disposed" - | "factory_storage_unavailable" - | "factory_storage_corrupt"; - -/** - * Friendly factory API exposed on a session. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface SessionFactoryApi { - /** - * Run a registered factory and resolve with its run envelope. - * - * The envelope is returned for every outcome, including `error`, `halted`, - * `paused`, and `cancelled` — inspect `status` and read `result` only when - * the run completed. `paused` settles the current attempt, but the same - * durable run can later resume under its existing run ID. SDK-initiated - * runs do not request permission, so they have no declined outcome. The - * model's `run_factory` tool requests permission before a durable row - * exists; declining it creates no run row. Failures that occur before a run - * exists (such as an unknown factory or attempting to start a run while the - * session is at its active top-level run limit) still reject. - */ - run(name: string, options?: RunOptions): Promise; - run( - factory: FactoryHandle, - options?: RunOptions - ): Promise; - /** - * Resume a run from its persisted factory name, arguments, journal, and accounting. - * - * Resolves with the run envelope like {@link SessionFactoryApi.run}. - * SDK-initiated resumes do not request permission. A pre-execution failure - * with a documented resume code rejects with {@link FactoryResumeError}. - */ - resume(runId: string, options?: ResumeOptions): Promise; - /** Read the latest durable envelope for a factory run. */ - getRun(runId: string): Promise; - /** - * Wait for the current attempt to settle and resolve with its envelope. - * - * Resolves as soon as the run reaches `completed`, `error`, `halted`, - * `paused`, or `cancelled`, and resolves immediately when the current - * attempt has already settled. A `paused` envelope is an attempt-level - * snapshot: resuming the same durable run can later change the envelope - * returned by {@link SessionFactoryApi.getRun}. - * - * This watches the run's `factory.run_updated` invalidation events and - * periodically re-reads the durable envelope so a missed event cannot - * leave the wait hanging. Pass a `signal` to stop waiting; aborting rejects - * and has no effect on the run itself, which keeps executing. Use - * {@link SessionFactoryApi.cancel} to actually stop it. - */ - waitForRun(runId: string, options?: { signal?: AbortSignal }): Promise; - /** - * List the newest default page of this session's durable factory runs. - * - * This backwards-compatible overload returns only the runs array. Pass - * paging options to receive the full page, including its cursors and - * truncation metadata. - */ - listRuns(): Promise; - /** - * Page this session's durable factory runs. - * - * `afterSeq` and `beforeSeq` are exclusive cursors. The result includes - * `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` so callers - * can continue paging without using the raw RPC client. - */ - listRuns(options: FactoryListRunsOptions): Promise; - /** Read durable phases, direct agents, and the latest progress tail for a run. */ - getRunDetail(runId: string): Promise; - /** Page durable progress forward, backward, or from the latest tail. */ - getRunProgress( - runId: string, - options?: Omit - ): Promise; - /** Pause a running factory attempt and return its `paused` envelope. */ - pause(runId: string): Promise; - /** Cancel a factory run and return its terminal envelope. */ - cancel(runId: string): Promise; -} - -/** - * Error thrown when a factory cannot be resumed before execution begins. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export class FactoryResumeError extends Error { - constructor( - public readonly code: FactoryResumeErrorCode, - message: string - ) { - super(message); - this.name = "FactoryResumeError"; - } -} - -interface StoredFactory { - meta: FactoryMeta; - run(context: FactoryContext): Promise; -} - -const factoryHandles = new WeakMap(); - -/** Maximum accepted factory timeout in seconds, derived from Node's maximum timer delay. */ -const MAX_FACTORY_TIMEOUT_SECONDS = 2_147_483.647; -const NANO_AIU_PER_AIU = 1_000_000_000; - -function deepFreeze(value: T): T { - if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { - Object.freeze(value); - for (const nested of Object.values(value)) { - deepFreeze(nested); - } - } - return value; -} - -function validateLimits(meta: FactoryMeta): void { - const limits = meta.limits; - if (!limits) { - return; - } - - for (const field of ["maxConcurrentSubagents", "maxTotalSubagents"] as const) { - const value = limits[field]; - if (value !== undefined && (!Number.isInteger(value) || value <= 0)) { - throw new Error(`Factory limit "${field}" must be a positive integer`); - } - } - - if ( - limits.timeoutSeconds !== undefined && - (!Number.isFinite(limits.timeoutSeconds) || limits.timeoutSeconds <= 0) - ) { - throw new Error( - 'Factory limit "timeoutSeconds" must be a positive, finite number of seconds' - ); - } - if ( - limits.timeoutSeconds !== undefined && - limits.timeoutSeconds > MAX_FACTORY_TIMEOUT_SECONDS - ) { - throw new Error( - `Factory limit "timeoutSeconds" must not exceed ${MAX_FACTORY_TIMEOUT_SECONDS} seconds` - ); - } - - if (limits.maxAiCredits !== undefined) { - const maxNanoAiu = Math.round(limits.maxAiCredits * NANO_AIU_PER_AIU); - if ( - !Number.isFinite(limits.maxAiCredits) || - limits.maxAiCredits <= 0 || - !Number.isSafeInteger(maxNanoAiu) || - maxNanoAiu < 1 - ) { - throw new Error( - 'Factory limit "maxAiCredits" must be a positive, finite number that rounds to a safe positive integer nano-AIU ceiling' - ); - } - } -} - -function validatePhases(meta: FactoryMeta): void { - const titles = new Set(); - for (const phase of meta.phases) { - if (phase.title.trim().length === 0) { - throw new Error("Factory phase titles must not be empty"); - } - if (titles.has(phase.title)) { - throw new Error(`Factory phase title "${phase.title}" is declared more than once`); - } - titles.add(phase.title); - } -} - -/** - * Defines an extension-authored factory and returns an opaque registration handle. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export function defineFactory< - TArgs extends JsonValue = JsonValue, - TResult extends JsonValue | void = JsonValue | void, ->(definition: FactoryDefinition): FactoryHandle { - // Snapshot before validating so post-registration mutation of the caller's - // object cannot slip past the authoring-boundary checks. - const meta = deepFreeze(structuredClone(definition.meta)); - validateLimits(meta); - validatePhases(meta); - - const stored: StoredFactory = { - meta, - run: definition.run, - }; - const handle = Object.freeze({ meta }) as unknown as FactoryHandle; - - factoryHandles.set(handle, stored); - return handle; -} - -/** @internal */ -export function getFactoryDefinition(handle: FactoryHandle): StoredFactory { - const definition = factoryHandles.get(handle); - if (!definition) { - throw new Error("Invalid factory handle"); - } - return definition; -} 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 16f5b1695a..1a3205e9de 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -31,8 +31,27 @@ export type AuthInfo = | TokenProviderAuthInfo | CopilotApiTokenAuthInfo | UserAuthInfo + | AccountAuthInfo | GhCliAuthInfo | ApiKeyAuthInfo; +/** + * The provider kind stamped on a signed-in account. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountKind". + */ +/** @experimental */ +export type AccountKind = + /** An OAuth github.com account. */ + | "githubDotCom" + /** A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com. */ + | "proxima" + /** A GitHub (EMU) account derived from a base Entra identity. */ + | "entraEmu" + /** A base Microsoft Entra identity. */ + | "entra" + /** A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra identity as an EMU account; its bearer is a Loki-scoped inference token consumed through the model-provider path, not the GitHub switcher. */ + | "loki"; /** * User to log out * @@ -43,6 +62,102 @@ export type AuthInfo = export type AccountLogoutRequest = { [k: string]: unknown | undefined; }; +/** + * Selects which accounts collection to enumerate. A no-arg selector is the empty-payload variant. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthEnumerateQuery". + */ +/** @experimental */ +export type AuthEnumerateQuery = + | { + /** + * Account-collection query variant discriminator. + */ + kind: "accounts"; + } + | { + /** + * Whether an interactive Entra broker is available on the host; gates Entra availability in the returned list. + */ + brokerAvailable?: boolean; + /** + * Account-collection query variant discriminator. + */ + kind: "providers"; + }; +/** + * Selects which typed accounts datum to read. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthReadQuery". + */ +/** @experimental */ +export type AuthReadQuery = + | { + /** + * Account read-datum query variant discriminator. + */ + kind: "activeAccount"; + } + | { + /** + * Account read-datum query variant discriminator. + */ + kind: "status"; + } + | { + /** + * Account read-datum query variant discriminator. + */ + kind: "lastErrors"; + }; +/** + * One non-interactive accounts mutation command (the selector is fused with its typed args). + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthWrite". + */ +/** @experimental */ +export type AuthWrite = + | { + /** + * Opaque selection id of the account to make active. + */ + selectionId: string; + /** + * Account mutation command variant discriminator. + */ + kind: "switchActive"; + } + | { + /** + * Opaque selection id of the account to log out; absent logs out the active account. + */ + selectionId?: string; + /** + * Account mutation command variant discriminator. + */ + kind: "logout"; + } + | { + /** + * Authentication host URL. + */ + host: string; + /** + * Login/username for the credential. + */ + login: string; + /** + * GitHub authentication token to install. + */ + token: string; + /** + * Account mutation command variant discriminator. + */ + kind: "setCredentials"; + }; /** * Resolved Anthropic adaptive-thinking capability for a model. * @@ -246,6 +361,48 @@ export type AgentRegistrySpawnValidationErrorField = | "model" /** The permissionMode parameter */ | "permissionMode"; +/** + * The enumerated collection, keyed by the same selector as the query. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthEnumerateValue". + */ +/** @experimental */ +export type AuthEnumerateValue = + | { + /** + * The signed-in account forest; empty when not logged in. + */ + items: AccountStatus[]; + /** + * Enumerated account-collection variant discriminator. + */ + kind: "accounts"; + } + | { + /** + * The providers offered for interactive login. + */ + items: ProviderDescriptor[]; + /** + * Enumerated account-collection variant discriminator. + */ + kind: "providers"; + }; +/** + * A provider a consumer may interactively sign in with. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "LoginProviderKind". + */ +/** @experimental */ +export type LoginProviderKind = + /** OAuth github.com sign-in via the browser (web loopback + PKCE). */ + | "githubDotCom" + /** A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com; the host is supplied interactively through the neutral input-required step. */ + | "proxima" + /** Microsoft Entra sign-in that derives a GitHub (EMU) credential. */ + | "entra"; /** * Authentication type * @@ -260,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. */ @@ -270,6 +429,112 @@ export type AuthInfoType = | "token-provider" /** Authentication from a Copilot API token. */ | "copilot-api-token"; +/** + * 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). + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthLoginStep". + */ +/** @experimental */ +export type AuthLoginStep = + | { + /** + * Authorize URL the consumer should open in a browser (consumer-driven browser-open). + */ + url: string; + /** + * Login flow step variant discriminator. + */ + kind: "open-url"; + } + | { + /** + * Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. a GitHub Enterprise Cloud host, *.ghe.com). + */ + prompt: string; + /** + * Login flow step variant discriminator. + */ + kind: "input-required"; + } + | { + /** + * Login flow step variant discriminator. + */ + kind: "awaiting"; + } + | { + /** + * Login flow step variant discriminator. + */ + kind: "needs-interaction"; + } + | { + result: AuthLoginResultDto; + /** + * Login flow step variant discriminator. + */ + kind: "completed"; + } + | { + /** + * Human-readable failure message. + */ + message: string; + /** + * Login flow step variant discriminator. + */ + kind: "error"; + }; +/** + * 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 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"; +/** + * The read result, keyed by the same selector as the query. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthReadValue". + */ +/** @experimental */ +export type AuthReadValue = + | { + account?: AccountStatus; + authInfo?: AuthIdentity; + /** + * Account read-datum variant discriminator. + */ + kind: "activeAccount"; + } + | { + status: AuthStatusDto; + /** + * Account read-datum variant discriminator. + */ + kind: "status"; + } + | { + /** + * Validation errors from the most recent authentication attempt. + */ + errors: AuthValidationError[]; + /** + * Account read-datum variant discriminator. + */ + kind: "lastErrors"; + }; /** * Validation errors from the most recent authentication attempt. * @@ -538,13 +803,23 @@ export type CatalogAiSkillCandidateKind = /** An AI skill. */ "ai-skill"; /** @experimental */ export type CatalogAiSkillMediaType = /** An AI skill card. */ "application/ai-skill"; /** - * Typed non-installable state for an AI skill candidate + * Typed installability state for an AI skill candidate * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "CatalogAiSkillInstallability". */ /** @experimental */ -export type CatalogAiSkillInstallability = /** AI skills are discovery-only on this surface. */ "not-installable-kind"; +export type CatalogAiSkillInstallability = + /** This AI skill candidate carries verified materialisation metadata and the selected session may plan installation. */ + | "installable" + /** Skill installation is understood but disabled for the selected session. */ + | "feature-disabled" + /** The candidate lacks verified materialisation metadata required for installation. */ + | "materialisation-unavailable" + /** Policy refuses Skill installation for the selected session or authority. */ + | "policy-forbids" + /** Compatibility value for discovery-only callers that did not negotiate Skill installation. */ + | "not-installable-kind"; /** * Where a candidate's card came from. Exactly one of a URL or embedded data: the union has no variant carrying both, and no variant carrying neither, so the rule holds structurally rather than by validation. * @@ -648,7 +923,15 @@ export type CatalogCapability = /** Understands exact candidate selection through model-safe opaque references and host-only candidate-handle hand-off. */ | "catalog-selection" /** Requires an eligible credential for the selected GitHub.com account before search egress and prohibits client-side anonymous retry, including after HTTP 401 or 403. The credential is scoped to the fixed catalog authority without redirect forwarding. Neither a grant nor successful response proves that the authority accepted the identity or selected a particular backend. Preserve this requirement on every page and retry; callers omitting it retain optional authentication. */ - | "catalog-search-credential-required"; + | "catalog-search-credential-required" + /** Captures the exact existing native session, account, host and connection for authenticated catalogue search, selection and planning. Requires catalog-search-credential-required; does not grant installation or create a session. */ + | "catalog-search-session-bound" + /** Understands effect-free preparation, exact human-confirmed apply and owned removal for fully resolved personal remote MCP choices without supplied inputs or configured secrets. Advertised only when the real producer and lower owned admission are linked; requires original connection and bound session authority for new work. */ + | "mcp-confirmed-remote-installation" + /** Extends mcp-confirmed-remote-installation to declared non-secret header and URL values and receipt-owned header secrets for personal remote MCP choices. Requires mcp-confirmed-remote-installation and bound session authority. Secret values are written only to the reviewed backend after confirmation and are never returned; package and stdio choices remain unsupported. Advertised only when owned secret effects and owned secret activation are linked. */ + | "mcp-configured-remote-installation" + /** Understands verified Agent Finder Skill install, uninstall, recovery and installation-scoped enablement APIs. Advertised only by runtimes with the Skill installation engine linked; acquisition may still be refused as feature-disabled per selected session. */ + | "skill-confirmed-installation"; /** * Bounded extensible wire-feature identifier. Known values are described by `CatalogCapability`; newer callers may send future identifiers so an older runtime can return a typed negotiation refusal instead of failing schema validation. Capability negotiation establishes contract understanding, while each operation's result separately reports runtime availability. * @@ -715,6 +998,8 @@ export type CatalogHandleRejectionReason = */ /** @experimental */ export type CatalogInvalidRequestField = + /** The selected existing attached session was missing, malformed or unavailable. */ + | "policySessionId" /** The search query was empty or longer than permitted. */ | "query" /** The requested result count fell outside its permitted range. */ @@ -1157,6 +1442,28 @@ export type ConnectorMcpStatus = | "disabled" /** The Connector currently has no live server configuration. */ | "not_configured"; +/** + * Availability. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorDiscoveryAvailability". + */ +/** @experimental */ +export type ConnectorDiscoveryAvailability = + /** Enabled. */ + | "enabled" + /** Disabled. */ + | "disabled" + /** Unavailable. */ + | "unavailable"; +/** + * 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. * @@ -1185,6 +1492,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. * @@ -1492,6 +1839,38 @@ export type DiscoveredMcpServerType = | "sse" /** Server is backed by an in-memory runtime implementation. */ | "memory"; +/** + * 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` "ModelProviderProvenanceSource". + */ +/** @experimental */ +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"; +/** + * Typed outcome for a provider operation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderOperationOutcomeCode". + */ +/** @experimental */ +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"; /** * Indicates whether the command was accepted into the local execution queue. * @@ -1500,6 +1879,24 @@ export type DiscoveredMcpServerType = */ /** @experimental */ 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 * @@ -1634,271 +2031,73 @@ export type ExternalToolTextResultForLlmContentResourceDetails = | EmbeddedTextResourceContents | EmbeddedBlobResourceContents; /** - * Execution-critical factory storage operation. + * Content filtering mode to apply to all tools, or a map of tool name to content filtering mode. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryDurableOperation". + * via the `definition` "FilterMapping". */ /** @experimental */ -export type FactoryDurableOperation = - /** Creating the durable run and declared phases. */ - | "createRun" - /** Persisting the transition to running. */ - | "markRunStarted" - /** Persisting the terminal run envelope. */ - | "finishRun" - /** Persisting subagent admission accounting. */ - | "reserveAgent" - /** Rolling back an uncommitted subagent admission. */ - | "releaseAgent" - /** Persisting an idempotent model-usage charge. */ - | "chargeCredit" - /** Persisting active execution time. */ - | "addElapsed" - /** Reading the authoritative AI-credit total. */ - | "reconcileCreditTotal" - /** Reading a journal entry without treating storage failure as a cache miss. */ - | "journalGet" - /** Persisting a journal entry before reporting success. */ - | "journalPut" - /** Renewing the durable owner lease that proves this process still owns the run. */ - | "refreshLease"; +export type FilterMapping = + | { + [k: string]: ContentFilterMode; + } + | ContentFilterMode; /** - * Current or terminal state of a factory run. + * Why the runtime is requesting a GitHub credential. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunStatus". + * via the `definition` "GitHubTokenAcquireReason". */ /** @experimental */ -export type FactoryRunStatus = - /** The run was minted and is awaiting approval. */ - | "pending" - /** The run is executing. */ - | "running" - /** The run completed successfully. */ - | "completed" - /** The run was interrupted while resource budget remained. */ - | "halted" - /** The current attempt stopped intentionally and the run may be resumed. */ - | "paused" - /** The run was cancelled before completion. */ - | "cancelled" - /** The factory body failed or reached a cumulative resource ceiling. */ - | "error"; +export type GitHubTokenAcquireReason = + /** The runtime is acquiring the registration's first credential. */ + | "initial" + /** The runtime is replacing a credential that is approaching expiry. */ + | "refresh"; /** - * Machine-readable factory run failure. + * SDK host response to a GitHub credential request. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunFailure". + * via the `definition` "GitHubTokenAcquireResult". */ /** @experimental */ -export type FactoryRunFailure = +export type GitHubTokenAcquireResult = | { - kind: FactoryRunFailureKind; /** - * Approved effective ceiling that was reached. + * GitHub access token acquired by the SDK host. */ - value: number; + accessToken: string; /** - * Suggested larger ceiling when the runtime can derive one safely. + * OAuth token type. Defaults to bearer when omitted. */ - suggestedValue?: number; + tokenType?: string; /** - * Factory run identifier. + * Remaining token lifetime in seconds when callback execution completes. It must exceed the one-hour preflight refresh threshold. */ - runId: string; + expiresIn: number; /** - * Factory failure variant discriminator. + * GitHub credential response variant discriminator. */ - type: "factory_limit_reached"; - } - | { - /** - * Factory run identifier whose changed limits were declined. - */ - runId: string; - /** - * Human-readable reason the resume did not proceed. - */ - reason: string; - /** - * Factory failure variant discriminator. - */ - type: "factory_resume_declined"; - } - | { - /** - * Stable failure code. - */ - code: string; - operation: FactoryDurableOperation; - /** - * Factory run identifier. - */ - runId: string; - /** - * Factory failure variant discriminator. - */ - type: "factory_durable_failure"; - } - | { - /** - * Factory run identifier. - */ - runId: string; - /** - * Confirmed usage in nano-AIU, representing the floor of what the run spent. - */ - drainedNanoAiu: number; - /** - * Factory failure variant discriminator. - */ - type: "factory_accounting_incomplete"; - } - | { - /** - * Factory run identifier. - */ - runId: string; - /** - * Factory failure variant discriminator. - */ - type: "factory_provider_disconnected"; - }; -/** - * Cumulative resource ceiling that stopped a factory run. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunFailureKind". - */ -/** @experimental */ -export type FactoryRunFailureKind = - /** The run admitted the approved maximum total number of subagents. */ - | "maxTotalSubagents" - /** The run reached the approved accumulated active-execution time in seconds. */ - | "timeoutSeconds" - /** The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. */ - | "maxAiCredits"; -/** - * Durable metadata describing who initiated a factory pause. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryPauseInfo". - */ -/** @experimental */ -export type FactoryPauseInfo = - | { - /** - * Factory pause initiator discriminator. - */ - type: "user"; + kind: "token"; } | { /** - * Stable author-defined checkpoint key that initiated the pause. - */ - key: string; - /** - * Factory pause initiator discriminator. + * GitHub credential response variant discriminator. */ - type: "checkpoint"; + kind: "cancelled"; }; /** - * Kind of factory progress line. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryLogLineKind". - */ -/** @experimental */ -export type FactoryLogLineKind = - /** A narrator log line. */ - | "log" - /** A named factory phase marker. */ - | "phase"; -/** - * Action the runtime selected for a durable factory pause checkpoint. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryPauseCheckpointAction". - */ -/** @experimental */ -export type FactoryPauseCheckpointAction = - /** The checkpoint was committed by a prior paused attempt, so execution may continue. */ - | "continue" - /** This attempt claimed the checkpoint and must cooperatively stop. */ - | "pause"; -/** - * Derived lifecycle state of a factory phase. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryPhaseStatus". - */ -/** @experimental */ -export type FactoryPhaseStatus = - /** The phase has not been entered yet. */ - | "pending" - /** The phase is currently entered and accumulating active time. */ - | "active" - /** The phase was entered and has since been closed. */ - | "completed" - /** The phase was never entered because a later phase was entered or the run reached a terminal state. */ - | "skipped"; -/** - * Content filtering mode to apply to all tools, or a map of tool name to content filtering mode. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FilterMapping". - */ -/** @experimental */ -export type FilterMapping = - | { - [k: string]: ContentFilterMode; - } - | ContentFilterMode; -/** - * Why the runtime is requesting a GitHub credential. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "GitHubTokenAcquireReason". - */ -/** @experimental */ -export type GitHubTokenAcquireReason = - /** The runtime is acquiring the registration's first credential. */ - | "initial" - /** The runtime is replacing a credential that is approaching expiry. */ - | "refresh"; -/** - * SDK host response to a GitHub credential request. + * Source for direct repo installs (when marketplace is empty) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "GitHubTokenAcquireResult". + * via the `definition` "InstalledPluginSource". */ /** @experimental */ -export type GitHubTokenAcquireResult = - | { - /** - * GitHub access token acquired by the SDK host. - */ - accessToken: string; - /** - * OAuth token type. Defaults to bearer when omitted. - */ - tokenType?: string; - /** - * Remaining token lifetime in seconds when callback execution completes. It must exceed the one-hour preflight refresh threshold. - */ - expiresIn: number; - /** - * GitHub credential response variant discriminator. - */ - kind: "token"; - } - | { - /** - * GitHub credential response variant discriminator. - */ - kind: "cancelled"; - }; +export type InstalledPluginSource = + | string + | InstalledPluginSourceGitHub + | InstalledPluginSourceUrl + | InstalledPluginSourceLocal; /** * Optional compaction parameters. * @@ -2005,1384 +2204,1742 @@ export type HistoryRewindOutcome = | "checkpoint-cleanup-failed" /** Files and conversation were rewound, but obsolete file snapshots could not be removed; only conversation-and-files rewinds produce this. */ | "snapshot-prune-failed"; + +/** @experimental */ +export type HostExitReason = + /** The owner requested disposal. */ + | "disposed" + /** The hosting task or its SDK transport exited; this does not mean the runtime process exited. */ + | "exited" + /** The owning SDK connection disconnected. */ + | "ownerDisconnected" + /** The runtime is shutting down. */ + | "runtimeShutdown"; /** - * Source for direct repo installs (when marketplace is empty) + * Only resource kinds with an implemented installation engine have a review variant. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstalledPluginSource". + * via the `definition` "InstallationReview". */ /** @experimental */ -export type InstalledPluginSource = - | string - | InstalledPluginSourceGitHub - | InstalledPluginSourceUrl - | InstalledPluginSourceLocal; +export type InstallationReview = + | { + review: McpInstallationReview; + /** + * Reviewed resource discriminator. + */ + resource: "mcp"; + } + | { + review: SkillInstallationReview; + /** + * Reviewed resource discriminator. + */ + resource: "skill"; + }; /** - * Which tier this target belongs to + * Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstructionDiscoveryPathLocation". + * via the `definition` "McpInstallationReview". */ /** @experimental */ -export type InstructionDiscoveryPathLocation = - /** Instructions live in user-level configuration. */ - | "user" - /** Instructions live in repository-level configuration. */ - | "repository" - /** Instructions live under the current working directory. */ - | "working-directory" - /** Instructions live in plugin-provided configuration. */ - | "plugin"; +export type McpInstallationReview = + | { + identity: McpPlanResourceIdentity; + provenance: McpPlanProvenance; + catalogueTrust?: CatalogTrustSnapshot; + catalogue?: InstallationCatalogueIdentity; + target: McpPlanTarget; + policy: McpPlanPolicyResult; + selectedChoice: McpPlanTransportChoice; + configurationChange: McpPlanConfigurationChange; + /** + * Non-secret values supplied for this selected alternative. + */ + inputs: McpInstallationInput[]; + /** + * Exact reviewed placeholders supplied separately. Never secret values. + */ + suppliedSecrets: McpPlanSecretReference[]; + secretStorage: McpInstallationSecretStorage; + effectiveConfiguration?: McpInstallationRemoteConfiguration; + /** + * Exact reviewed installation action. + */ + action: "install"; + } + | { + /** + * Receipt-owned installation being removed. + */ + installationId: string; + identity: McpPlanResourceIdentity; + provenance: McpPlanProvenance; + target: McpPlanTarget; + policy: McpPlanPolicyResult; + /** + * Whether uninstall restores a protected pre-install configuration. + */ + restoresPreviousConfiguration: boolean; + /** + * Exact planner-owned secret slots to remove, excluding shared OAuth grants. + */ + ownedSecretCount: number; + /** + * Shared profile authentication is deliberately retained, not pending cleanup. + */ + preservesSharedAuthentication: boolean; + /** + * Exact reviewed installation action. + */ + action: "uninstall"; + }; /** - * Whether the target is a single file or a directory of instruction files + * Configuration scope an MCP install plan targets * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstructionDiscoveryPathKind". + * via the `definition` "McpPlanScope". */ /** @experimental */ -export type InstructionDiscoveryPathKind = - /** The target is a single instruction file. */ - | "file" - /** The target is a directory that holds instruction files. */ - | "directory"; +export type McpPlanScope = /** The user's own MCP configuration. */ "user"; /** - * Category of instruction source — used for merge logic + * What policy decided for a planned server * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstructionSourceType". + * via the `definition` "McpPlanPolicyDecision". */ /** @experimental */ -export type InstructionSourceType = - /** Instructions loaded from the user's home configuration. */ - | "home" - /** Instructions loaded from repository-scoped files. */ - | "repo" - /** Instructions loaded from model-specific files. */ - | "model" - /** Instructions loaded from VS Code instruction files. */ - | "vscode" - /** Instructions discovered from nested agent files. */ - | "nested-agents" - /** Instructions inherited from child instruction files. */ - | "child-instructions" - /** Instructions supplied by an installed plugin. */ - | "plugin"; +export type McpPlanPolicyDecision = + /** Policy permits the server. */ + | "allowed" + /** Policy forbids the server, so the plan cannot be applied. */ + | "blocked" + /** Policy permits the server only after an explicit approval. */ + | "requires-approval"; /** - * Where this source lives — used for UI grouping + * One eligible way to run the server, represented as a tagged package or remote variant so package identity and endpoint states cannot contradict the install method. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstructionSourceLocation". + * via the `definition` "McpPlanTransportChoice". */ /** @experimental */ -export type InstructionSourceLocation = - /** Instructions live in user-level configuration. */ - | "user" - /** Instructions live in repository-level configuration. */ - | "repository" - /** Instructions live under the current working directory. */ - | "working-directory" - /** Instructions live in plugin-provided configuration. */ - | "plugin"; +export type McpPlanTransportChoice = McpPlanTransportChoicePackage | McpPlanTransportChoiceRemote; /** - * 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. + * Transport exposed by a locally launched package * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "LlmInferenceHttpRequestStartTransport". + * via the `definition` "McpPlanPackageTransport". */ /** @experimental */ -export type LlmInferenceHttpRequestStartTransport = - /** Plain HTTP or SSE response. Each body chunk is an opaque byte range; the response is a status line, headers, and a (possibly streamed) body. */ - | "http" - /** Full-duplex WebSocket channel. Each body chunk maps to exactly one WebSocket message and the `binary` flag distinguishes text from binary frames; request and response chunks flow concurrently. */ - | "websocket"; +export type McpPlanPackageTransport = + /** A locally launched process spoken to over standard input and output. */ + "stdio"; /** - * Repository host type + * Discriminator for a package-backed transport choice * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionContextHostType". + * via the `definition` "McpPlanPackageInstallMethod". */ /** @experimental */ -export type SessionContextHostType = - /** Session repository is hosted on GitHub. */ - | "github" - /** Session repository is hosted on Azure DevOps. */ - | "ado"; +export type McpPlanPackageInstallMethod = /** Install and run a local package. */ "package"; /** - * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". + * One non-secret value a transport choice needs, represented as a scalar or enumerated variant so enum values cannot be missing or attached to another type. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionLogLevel". + * via the `definition` "McpPlanRequiredValue". */ /** @experimental */ -export type SessionLogLevel = - /** Informational message. */ - | "info" - /** Warning message that may require attention. */ - | "warning" - /** Error message describing a failure. */ - | "error"; +export type McpPlanRequiredValue = McpPlanRequiredValueScalar | McpPlanRequiredValueEnum; /** - * UI theme preference per SEP-1865 + * Discriminator for a scalar required value * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsHostContextDetailsTheme". + * via the `definition` "McpPlanRequiredValueScalarKind". */ /** @experimental */ -export type McpAppsHostContextDetailsTheme = - /** Light UI theme */ - | "light" - /** Dark UI theme */ - | "dark"; +export type McpPlanRequiredValueScalarKind = /** The value uses one scalar type. */ "scalar"; /** - * Current display mode (SEP-1865) + * Where a required value is applied when the planned server is launched * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsHostContextDetailsDisplayMode". + * via the `definition` "McpPlanValueCategory". */ /** @experimental */ -export type McpAppsHostContextDetailsDisplayMode = - /** Rendered inline within the host conversation surface */ - | "inline" - /** Rendered as a fullscreen overlay */ - | "fullscreen" - /** Rendered as a picture-in-picture floating panel */ - | "pip"; +export type McpPlanValueCategory = + /** Set as an environment variable on the launched process. */ + | "environment-variable" + /** Passed to the runtime that launches the package. */ + | "runtime-argument" + /** Passed to the packaged server itself. */ + | "package-argument" + /** Sent as a request header to a remote endpoint. */ + | "header" + /** Substituted into the remote endpoint URL. */ + | "url-variable"; /** - * Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. + * Scalar type a required value must conform to * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsHostContextDetailsAvailableDisplayMode". + * via the `definition` "McpPlanScalarValueType". */ /** @experimental */ -export type McpAppsHostContextDetailsAvailableDisplayMode = - /** Rendered inline within the host conversation surface */ - | "inline" - /** Rendered as a fullscreen overlay */ - | "fullscreen" - /** Rendered as a picture-in-picture floating panel */ - | "pip"; +export type McpPlanScalarValueType = + /** Free text. */ + | "string" + /** A number. */ + | "number" + /** A boolean. */ + | "boolean" + /** A filesystem path. */ + | "path"; /** - * Platform type for responsive design + * Discriminator for an enumerated required value * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsHostContextDetailsPlatform". + * via the `definition` "McpPlanRequiredValueEnumKind". */ /** @experimental */ -export type McpAppsHostContextDetailsPlatform = - /** Host runs in a web browser */ - | "web" - /** Host runs as a desktop application */ - | "desktop" - /** Host runs on a mobile device */ - | "mobile"; +export type McpPlanRequiredValueEnumKind = /** The value uses a fixed non-empty enumeration. */ "enum"; /** - * UI theme preference per SEP-1865 + * Discriminator for an enumerated required value * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsSetHostContextDetailsTheme". + * via the `definition` "McpPlanEnumValueType". */ /** @experimental */ -export type McpAppsSetHostContextDetailsTheme = - /** Light UI theme */ - | "light" - /** Dark UI theme */ - | "dark"; +export type McpPlanEnumValueType = /** One of a fixed, non-empty set of permitted values. */ "enum"; /** - * Current display mode (SEP-1865) + * A runtime-assigned secret placeholder. The identifier is carried once, inside the placeholder, so it cannot contradict a separate secret-id field. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsSetHostContextDetailsDisplayMode". + * via the `definition` "McpPlanSecretReference". */ /** @experimental */ -export type McpAppsSetHostContextDetailsDisplayMode = - /** Rendered inline within the host conversation surface */ - | "inline" - /** Rendered as a fullscreen overlay */ - | "fullscreen" - /** Rendered as a picture-in-picture floating panel */ - | "pip"; +export type McpPlanSecretReference = string; /** - * Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. + * Transport exposed by a remote endpoint * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsSetHostContextDetailsAvailableDisplayMode". + * via the `definition` "McpPlanRemoteTransport". */ /** @experimental */ -export type McpAppsSetHostContextDetailsAvailableDisplayMode = - /** Rendered inline within the host conversation surface */ - | "inline" - /** Rendered as a fullscreen overlay */ - | "fullscreen" - /** Rendered as a picture-in-picture floating panel */ - | "pip"; +export type McpPlanRemoteTransport = + /** An HTTP endpoint. */ + | "http" + /** A streamable HTTP endpoint. */ + | "streamable-http" + /** A server-sent events endpoint. */ + | "sse"; /** - * Platform type for responsive design + * Discriminator for a remote-endpoint transport choice * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpAppsSetHostContextDetailsPlatform". + * via the `definition` "McpPlanRemoteInstallMethod". */ /** @experimental */ -export type McpAppsSetHostContextDetailsPlatform = - /** Host runs in a web browser */ - | "web" - /** Host runs as a desktop application */ - | "desktop" - /** Host runs on a mobile device */ - | "mobile"; +export type McpPlanRemoteInstallMethod = /** Connect to a remote endpoint. */ "remote"; /** - * Serializable MCP server configuration (stdio process or remote HTTP/SSE) + * Whether a planned configuration change would create or modify an entry * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpSerializableServerConfig". + * via the `definition` "McpPlanConfigurationOperation". */ /** @experimental */ -export type McpSerializableServerConfig = McpServerConfigStdio | McpServerConfigHttp; +export type McpPlanConfigurationOperation = + /** Creates a configuration entry that does not exist yet. */ + | "add" + /** Modifies a configuration entry that already exists. */ + | "update"; /** - * Telemetry-obfuscation policy for an MCP server's tools. + * Explicit backend selection is part of the final review; failures never switch backends. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpSafeForTelemetry". + * via the `definition` "McpInstallationSecretStorage". */ /** @experimental */ -export type McpSafeForTelemetry = boolean | McpSafeForTelemetryFields; +export type McpInstallationSecretStorage = + /** The selected operating-system keychain, without fallback to file storage. */ + | "keychain" + /** The explicitly selected private file backend. */ + | "private-file"; /** - * Set to `true` to use defaults, or provide an object with additional auth or OIDC settings. + * Safe verified Skill review fields. No raw credential, candidate handle or plan handle. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerAuthConfig". + * via the `definition` "SkillInstallationReview". */ /** @experimental */ -export type McpServerAuthConfig = boolean | McpServerAuthConfigRedirectPort; +export type SkillInstallationReview = + | { + /** + * Skill invocation name from SKILL.md. + */ + name: string; + /** + * Skill description from SKILL.md when present. + */ + description?: string; + catalogue: InstallationCatalogueIdentity; + source: SkillInstallationSource; + target: SkillInstallationLocation; + /** + * Installing never grants immediate use; the Skill is written disabled. + */ + installsDisabled: boolean; + /** + * Reviewed files and digests. + */ + files: SkillInstallationFileReview[]; + /** + * Total reviewed payload size in bytes. + */ + totalBytes: number; + /** + * Relative path of the verified Skill entrypoint. + */ + entrypointPath: string; + /** + * Complete verified SKILL.md content. Planning refuses with review-too-large + * when this exceeds 262144 UTF-8 bytes; it is never truncated. + */ + entrypointContent: string; + /** + * Exact reviewed Skill installation action. + */ + action: "install"; + } + | { + installation: SkillInstallationSummary; + /** + * Whether current files differ from the receipt. Apply refuses drift. + */ + filesModified: boolean; + /** + * Files recorded by the installation receipt. + */ + files: SkillInstallationFileReview[]; + /** + * Total receipt-owned payload size in bytes. + */ + totalBytes: number; + catalogue: InstallationCatalogueIdentity; + /** + * Exact reviewed Skill installation action. + */ + action: "uninstall"; + }; + +/** @experimental */ +export type SkillInstallationScope = /** The user's personal Copilot home. */ "personal"; /** - * Controls if tools provided by this server can be loaded on demand via tool search (auto) or always included in the initial tool list (never) + * Bound-session observation after reconciling persisted enablement. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerConfigDeferTools". + * via the `definition` "SkillInstallationSessionState". */ /** @experimental */ -export type McpServerConfigDeferTools = - /** Tools may be deferred under certain conditions */ - | "auto" - /** Tools are always included in the initial tool list, even when tool search is enabled. */ - | "never"; +export type SkillInstallationSessionState = + /** The selected session has loaded this Skill and it is enabled. */ + | "loaded-enabled" + /** The selected session has loaded this Skill or settings and it is disabled. */ + | "loaded-disabled" + /** The selected session has not loaded Skills after the latest change. */ + | "not-loaded" + /** The selected session could not be inspected. */ + | "unknown"; /** - * Local MCP transport type. + * Owned Skill state observed from files and receipts. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerConfigStdioType". + * via the `definition` "SkillInstallationOwnershipState". */ /** @experimental */ -export type McpServerConfigStdioType = - /** Legacy alias for the local stdio transport. */ - | "local" - /** Server communicates over stdio with a local child process. */ - | "stdio"; +export type SkillInstallationOwnershipState = + /** Owned files and receipt evidence match. */ + | "intact" + /** Owned files no longer match the receipt. */ + | "modified" + /** Ownership evidence requires recovery before mutation. */ + | "recovery-required"; /** - * Remote transport type. Defaults to "http" when omitted. + * Explicit user decisions, never inferred from a permission grant or model response. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerConfigHttpType". + * via the `definition` "InstallationDecision". */ /** @experimental */ -export type McpServerConfigHttpType = - /** Streamable HTTP transport. */ - | "http" - /** Server-Sent Events transport. */ - | "sse"; +export type InstallationDecision = + /** The user explicitly approved the exact review on this request. */ + | "confirm" + /** The user declined the reviewed operation. */ + | "decline" + /** The user cancelled the pending decision without granting consent. */ + | "cancel"; /** - * OAuth grant type to use when authenticating to the remote MCP server. + * Which tier this target belongs to * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerConfigHttpOauthGrantType". + * via the `definition` "InstructionDiscoveryPathLocation". */ /** @experimental */ -export type McpServerConfigHttpOauthGrantType = - /** Interactive browser-based authorization code flow with PKCE. */ - | "authorization_code" - /** Headless client credentials flow using the configured OAuth client. */ - | "client_credentials"; +export type InstructionDiscoveryPathLocation = + /** Instructions live in user-level configuration. */ + | "user" + /** Instructions live in repository-level configuration. */ + | "repository" + /** Instructions live under the current working directory. */ + | "working-directory" + /** Instructions live in plugin-provided configuration. */ + | "plugin"; /** - * Structured MCP elicitation mode. + * Whether the target is a single file or a directory of instruction files * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpElicitationFormMode". + * via the `definition` "InstructionDiscoveryPathKind". */ /** @experimental */ -export type McpElicitationFormMode = "form"; +export type InstructionDiscoveryPathKind = + /** The target is a single instruction file. */ + | "file" + /** The target is a directory that holds instruction files. */ + | "directory"; /** - * Host response: supply dynamic headers or decline this refresh. + * Category of instruction source — used for merge logic * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpHeadersHandlePendingHeadersRefreshRequest". + * via the `definition` "InstructionSourceType". */ /** @experimental */ -export type McpHeadersHandlePendingHeadersRefreshRequest = - | { - /** - * Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. - */ - headers: { - [k: string]: string | undefined; - }; - /** - * Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value. - */ - ttlMs?: number; - /** - * Headers-refresh response variant discriminator. - */ - kind: "headers"; - } - | { - /** - * Headers-refresh response variant discriminator. - */ - kind: "none"; - } - | { - /** - * Host credential broker failure, denial, or revocation reason. - */ - message: string; - /** - * Headers-refresh response variant discriminator. - */ - kind: "error"; - }; +export type InstructionSourceType = + /** Instructions loaded from the user's home configuration. */ + | "home" + /** Instructions loaded from repository-scoped files. */ + | "repo" + /** Instructions loaded from model-specific files. */ + | "model" + /** Instructions loaded from VS Code instruction files. */ + | "vscode" + /** Instructions discovered from nested agent files. */ + | "nested-agents" + /** Instructions inherited from child instruction files. */ + | "child-instructions" + /** Instructions supplied by an installed plugin. */ + | "plugin"; /** - * One eligible way to run the server, represented as a tagged package or remote variant so package identity and endpoint states cannot contradict the install method. + * Where this source lives — used for UI grouping * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanTransportChoice". + * via the `definition` "InstructionSourceLocation". */ /** @experimental */ -export type McpPlanTransportChoice = McpPlanTransportChoicePackage | McpPlanTransportChoiceRemote; +export type InstructionSourceLocation = + /** Instructions live in user-level configuration. */ + | "user" + /** Instructions live in repository-level configuration. */ + | "repository" + /** Instructions live under the current working directory. */ + | "working-directory" + /** Instructions live in plugin-provided configuration. */ + | "plugin"; /** - * Transport exposed by a locally launched package + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanPackageTransport". + * via the `definition` "LlmInferenceHttpRequestStartTransport". */ /** @experimental */ -export type McpPlanPackageTransport = - /** A locally launched process spoken to over standard input and output. */ - "stdio"; +export type LlmInferenceHttpRequestStartTransport = + /** Plain HTTP or SSE response. Each body chunk is an opaque byte range; the response is a status line, headers, and a (possibly streamed) body. */ + | "http" + /** Full-duplex WebSocket channel. Each body chunk maps to exactly one WebSocket message and the `binary` flag distinguishes text from binary frames; request and response chunks flow concurrently. */ + | "websocket"; /** - * Discriminator for a package-backed transport choice + * Repository host type * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanPackageInstallMethod". + * via the `definition` "SessionContextHostType". */ /** @experimental */ -export type McpPlanPackageInstallMethod = /** Install and run a local package. */ "package"; +export type SessionContextHostType = + /** Session repository is hosted on GitHub. */ + | "github" + /** Session repository is hosted on Azure DevOps. */ + | "ado"; /** - * One non-secret value a transport choice needs, represented as a scalar or enumerated variant so enum values cannot be missing or attached to another type. + * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanRequiredValue". + * via the `definition` "SessionLogLevel". */ /** @experimental */ -export type McpPlanRequiredValue = McpPlanRequiredValueScalar | McpPlanRequiredValueEnum; +export type SessionLogLevel = + /** Informational message. */ + | "info" + /** Warning message that may require attention. */ + | "warning" + /** Error message describing a failure. */ + | "error"; /** - * Discriminator for a scalar required value + * A channel accepted by managedSettings.compose. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanRequiredValueScalarKind". + * via the `definition` "ManagedSettingsChannel". */ /** @experimental */ -export type McpPlanRequiredValueScalarKind = /** The value uses one scalar type. */ "scalar"; +export type ManagedSettingsChannel = + /** Device policy, the strongest channel. */ + | "device" + /** Account or organization policy. */ + | "server" + /** Session-local helper output, the weakest channel. */ + | "policyHelper"; /** - * Where a required value is applied when the planned server is launched + * Severity of a managed-settings validation finding. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanValueCategory". + * via the `definition` "ManagedSettingsDiagnosticSeverity". */ /** @experimental */ -export type McpPlanValueCategory = - /** Set as an environment variable on the launched process. */ - | "environment-variable" - /** Passed to the runtime that launches the package. */ - | "runtime-argument" - /** Passed to the packaged server itself. */ - | "package-argument" - /** Sent as a request header to a remote endpoint. */ - | "header" - /** Substituted into the remote endpoint URL. */ - | "url-variable"; +export type ManagedSettingsDiagnosticSeverity = + /** The runtime rejects the document. */ + | "error" + /** The runtime accepts the document but ignores the flagged content. */ + | "warning"; /** - * Scalar type a required value must conform to + * UI theme preference per SEP-1865 * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanScalarValueType". + * via the `definition` "McpAppsHostContextDetailsTheme". */ /** @experimental */ -export type McpPlanScalarValueType = - /** Free text. */ - | "string" - /** A number. */ - | "number" - /** A boolean. */ - | "boolean" - /** A filesystem path. */ - | "path"; +export type McpAppsHostContextDetailsTheme = + /** Light UI theme */ + | "light" + /** Dark UI theme */ + | "dark"; /** - * Discriminator for an enumerated required value + * Current display mode (SEP-1865) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanRequiredValueEnumKind". + * via the `definition` "McpAppsHostContextDetailsDisplayMode". */ /** @experimental */ -export type McpPlanRequiredValueEnumKind = /** The value uses a fixed non-empty enumeration. */ "enum"; +export type McpAppsHostContextDetailsDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; /** - * Discriminator for an enumerated required value + * Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanEnumValueType". + * via the `definition` "McpAppsHostContextDetailsAvailableDisplayMode". */ /** @experimental */ -export type McpPlanEnumValueType = /** One of a fixed, non-empty set of permitted values. */ "enum"; +export type McpAppsHostContextDetailsAvailableDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; /** - * A runtime-assigned secret placeholder. The identifier is carried once, inside the placeholder, so it cannot contradict a separate secret-id field. + * Platform type for responsive design * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanSecretReference". + * via the `definition` "McpAppsHostContextDetailsPlatform". */ /** @experimental */ -export type McpPlanSecretReference = string; +export type McpAppsHostContextDetailsPlatform = + /** Host runs in a web browser */ + | "web" + /** Host runs as a desktop application */ + | "desktop" + /** Host runs on a mobile device */ + | "mobile"; /** - * Transport exposed by a remote endpoint + * UI theme preference per SEP-1865 * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanRemoteTransport". + * via the `definition` "McpAppsSetHostContextDetailsTheme". */ /** @experimental */ -export type McpPlanRemoteTransport = - /** An HTTP endpoint. */ - | "http" - /** A streamable HTTP endpoint. */ - | "streamable-http" - /** A server-sent events endpoint. */ - | "sse"; +export type McpAppsSetHostContextDetailsTheme = + /** Light UI theme */ + | "light" + /** Dark UI theme */ + | "dark"; /** - * Discriminator for a remote-endpoint transport choice + * Current display mode (SEP-1865) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanRemoteInstallMethod". + * via the `definition` "McpAppsSetHostContextDetailsDisplayMode". */ /** @experimental */ -export type McpPlanRemoteInstallMethod = /** Connect to a remote endpoint. */ "remote"; +export type McpAppsSetHostContextDetailsDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; /** - * Configuration scope an MCP install plan targets + * Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanScope". + * via the `definition` "McpAppsSetHostContextDetailsAvailableDisplayMode". */ /** @experimental */ -export type McpPlanScope = /** The user's own MCP configuration. */ "user"; +export type McpAppsSetHostContextDetailsAvailableDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; /** - * What policy decided for a planned server + * Platform type for responsive design * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanPolicyDecision". + * via the `definition` "McpAppsSetHostContextDetailsPlatform". */ /** @experimental */ -export type McpPlanPolicyDecision = - /** Policy permits the server. */ - | "allowed" - /** Policy forbids the server, so the plan cannot be applied. */ - | "blocked" - /** Policy permits the server only after an explicit approval. */ - | "requires-approval"; +export type McpAppsSetHostContextDetailsPlatform = + /** Host runs in a web browser */ + | "web" + /** Host runs as a desktop application */ + | "desktop" + /** Host runs on a mobile device */ + | "mobile"; /** - * Whether a planned configuration change would create or modify an entry + * Serializable MCP server configuration (stdio process or remote HTTP/SSE) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanConfigurationOperation". + * via the `definition` "McpSerializableServerConfig". */ /** @experimental */ -export type McpPlanConfigurationOperation = - /** Creates a configuration entry that does not exist yet. */ - | "add" - /** Modifies a configuration entry that already exists. */ - | "update"; +export type McpSerializableServerConfig = McpServerConfigStdio | McpServerConfigHttp; /** - * Consumer allowed to call an MCP tool. + * Telemetry-obfuscation policy for an MCP server's tools. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpToolUiVisibility". + * via the `definition` "McpSafeForTelemetry". */ /** @experimental */ -export type McpToolUiVisibility = - /** The model may call the tool. */ - | "model" - /** An MCP App view may call the tool. */ - | "app"; +export type McpSafeForTelemetry = boolean | McpSafeForTelemetryFields; /** - * Host response to the pending OAuth request. + * Set to `true` to use defaults, or provide an object with additional auth or OIDC settings. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpOauthPendingRequestResponse". + * via the `definition` "McpServerAuthConfig". */ /** @experimental */ -export type McpOauthPendingRequestResponse = - | { - /** - * Access token acquired by the SDK host - */ - accessToken: string; - /** - * OAuth token type. Defaults to bearer when omitted. - */ - tokenType?: string; - /** - * Token lifetime in seconds, if known. - */ - expiresIn?: number; - /** - * OAuth response variant discriminator. - */ - kind: "token"; - } - | { - /** - * OAuth response variant discriminator. - */ - kind: "cancelled"; - }; +export type McpServerAuthConfig = boolean | McpServerAuthConfigRedirectPort; /** - * OAuth grant type override for this login. + * Controls if tools provided by this server can be loaded on demand via tool search (auto) or always included in the initial tool list (never) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpOauthLoginGrantType". + * via the `definition` "McpServerConfigDeferTools". */ /** @experimental */ -export type McpOauthLoginGrantType = - /** Interactive browser-based OAuth flow using an authorization code, typically with PKCE. */ +export type McpServerConfigDeferTools = + /** Tools may be deferred under certain conditions */ + | "auto" + /** Tools are always included in the initial tool list, even when tool search is enabled. */ + | "never"; +/** + * Local MCP transport type. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpServerConfigStdioType". + */ +/** @experimental */ +export type McpServerConfigStdioType = + /** Legacy alias for the local stdio transport. */ + | "local" + /** Server communicates over stdio with a local child process. */ + | "stdio"; +/** + * Remote transport type. Defaults to "http" when omitted. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpServerConfigHttpType". + */ +/** @experimental */ +export type McpServerConfigHttpType = + /** Streamable HTTP transport. */ + | "http" + /** Server-Sent Events transport. */ + | "sse"; +/** + * OAuth grant type to use when authenticating to the remote MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpServerConfigHttpOauthGrantType". + */ +/** @experimental */ +export type McpServerConfigHttpOauthGrantType = + /** Interactive browser-based authorization code flow with PKCE. */ | "authorization_code" - /** Headless OAuth flow where a confidential client authenticates directly with a client secret. */ + /** Headless client credentials flow using the configured OAuth client. */ | "client_credentials"; /** - * Why a passive MCP OAuth probe determined authentication is needed. + * Structured MCP elicitation mode. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpOauthProbeNeedsAuthReason". + * via the `definition` "McpElicitationFormMode". */ /** @experimental */ -export type McpOauthProbeNeedsAuthReason = - /** No token was sent and the server requires authentication. */ - | "initial" - /** A cached token was sent and rejected. */ - | "refresh" - /** The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. */ - | "upscope"; +export type McpElicitationFormMode = "form"; /** - * 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. + * Host response: supply dynamic headers or decline this refresh. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpOauthProbeResult". + * via the `definition` "McpHeadersHandlePendingHeadersRefreshRequest". */ /** @experimental */ -export type McpOauthProbeResult = +export type McpHeadersHandlePendingHeadersRefreshRequest = | { - httpResponse: McpOauthHttpResponse; /** - * Probe outcome variant discriminator. + * Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. */ - status: "no-auth-required"; - } - | { - httpResponse: McpOauthHttpResponse; + headers: { + [k: string]: string | undefined; + }; /** - * Probe outcome variant discriminator. + * Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value. */ - status: "authenticated"; + ttlMs?: number; + /** + * Headers-refresh response variant discriminator. + */ + kind: "headers"; } | { - httpResponse: McpOauthHttpResponse; - reason: McpOauthProbeNeedsAuthReason; - wwwAuthenticateParams?: McpOauthWWWAuthenticateParams; /** - * Probe outcome variant discriminator. + * Headers-refresh response variant discriminator. */ - status: "needs-auth"; + kind: "none"; } | { /** - * Human-readable probe failure detail. + * Host credential broker failure, denial, or revocation reason. */ - error: string; - httpResponse?: McpOauthHttpResponse; + message: string; /** - * Probe outcome variant discriminator. + * Headers-refresh response variant discriminator. */ - status: "failed"; + kind: "error"; }; /** - * What an install plan is computed from: a candidate handle from a previous search, or a card supplied directly. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanInstallSource". - */ -/** @experimental */ -export type McpPlanInstallSource = McpPlanInstallSourceCandidate | McpPlanInstallSourceCard; -/** - * Discriminator for a candidate-backed install-plan source - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanInstallSourceCandidateKind". - */ -/** @experimental */ -export type McpPlanInstallSourceCandidateKind = /** Plan from a candidate returned by catalog search. */ "candidate"; -/** - * Discriminator for a caller-supplied-card install-plan source - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanInstallSourceCardKind". - */ -/** @experimental */ -export type McpPlanInstallSourceCardKind = /** Plan directly from a caller-supplied card. */ "card"; -/** - * A card supplied directly by the caller. Exactly one of a URL or embedded data, encoded structurally so neither both nor neither can be expressed. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerCardReference". - */ -/** @experimental */ -export type McpServerCardReference = McpServerCardUrl | McpServerCardEmbedded; -/** - * Discriminator for a URL-backed MCP server card + * Bounded refusal categories, without echoing handles, credentials or configuration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerCardUrlKind". + * via the `definition` "McpInstallationFailureReason". */ /** @experimental */ -export type McpServerCardUrlKind = /** Retrieve the card from its URL. */ "url"; +export type McpInstallationFailureReason = + /** The selected choice or request is unsupported or malformed. */ + | "invalid-request" + /** The bounded original-connection operation limit was reached. */ + | "operation-limit" + /** The original operation was cancelled. */ + | "cancelled" + /** Required installation capabilities were omitted. */ + | "capability-required" + /** The original host cannot receive human confirmation. */ + | "confirmation-unavailable" + /** The confirmation response is malformed or mismatched. */ + | "confirmation-invalid" + /** Existing authenticated session and host authority is unavailable. */ + | "policy-context-unavailable" + /** The original authority or policy changed. */ + | "policy-changed" + /** Current managed policy refuses the operation. */ + | "policy-denied" + /** Configuration changed after the reviewed snapshot. */ + | "configuration-changed" + /** Installed configuration no longer matches ownership evidence. */ + | "configuration-modified" + /** No matching owned resource or original operation exists. */ + | "resource-not-found" + /** The original plan deadline elapsed. */ + | "plan-expired" + /** The one-use plan or prepared operation was already consumed. */ + | "plan-replayed" + /** The handle belongs to a different runtime, session or connection. */ + | "foreign-runtime" + /** Fresh bound planning is required. */ + | "replan-required" + /** Exact original source revalidation is unsupported. */ + | "source-revalidation-unavailable" + /** The source differs from the retained commitment. */ + | "source-changed" + /** The original source could not be retrieved safely. */ + | "source-unavailable" + /** Authoritative Registry interpretation is unavailable. */ + | "registry-unavailable" + /** The selected secret backend is unavailable. */ + | "secret-store-unavailable" + /** Required owned admission and lifecycle support is absent. */ + | "lifecycle-unavailable" + /** A storage operation failed; inspect any allocated operation before retrying. */ + | "write-failed"; +/** + * Read-only or recovery management result, never permission to activate or replay. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpInstallationManagementOutcome". + */ +/** @experimental */ +export type McpInstallationManagementOutcome = + | { + /** + * Installation management outcome discriminator. + */ + kind: "recovery-required"; + } + | { + operation: McpPreparedInstall; + /** + * Installation management outcome discriminator. + */ + kind: "install-prepared"; + } + | { + /** + * Owned receipts visible to the selected account and host. + */ + installations: McpInstallationSummary[]; + /** + * Installation management outcome discriminator. + */ + kind: "listed"; + } + | { + /** + * Freshly inspected receipts after successful durable reconciliation. + */ + installations: McpInstallationSummary[]; + /** + * Installation management outcome discriminator. + */ + kind: "recovered"; + } + | { + plan: McpUninstallPlan; + /** + * Installation management outcome discriminator. + */ + kind: "uninstall-planned"; + } + | { + operation: McpInstallationOperationStatus; + /** + * Installation management outcome discriminator. + */ + kind: "operation"; + } + | { + reason: McpInstallationFailureReason; + /** + * Installation management outcome discriminator. + */ + kind: "refused"; + }; /** - * Discriminator for an embedded MCP server card + * Configuration ownership and setup observations, distinct from tool permissions. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerCardEmbeddedKind". + * via the `definition` "McpInstallationState". */ /** @experimental */ -export type McpServerCardEmbeddedKind = /** Use the embedded card document. */ "embedded"; +export type McpInstallationState = + /** Owned configuration exists; inventory alone does not grant activation. */ + | "needs-setup" + /** The selected authorised session reports an active installation. */ + | "active" + /** The selected server requires explicit sign-in. */ + | "authentication-required" + /** The selected server could not be activated. */ + | "activation-failed" + /** Owned configuration no longer matches its receipt. */ + | "configuration-modified" + /** Confirmed durable work or unsafe evidence requires recovery. */ + | "recovery-required"; /** - * Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. + * Status snapshot from the original connection, independent of new-work account availability. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanInstallResult". + * via the `definition` "McpInstallationOperationStatus". */ /** @experimental */ -export type McpPlanInstallResult = - | McpPlanInstallPlanned - | CatalogNegotiationRefusedError - | CatalogHandleRejectedError - | CatalogInvalidRequestError - | CatalogAuthenticationRequiredError - | CatalogPolicyRejectedError - | CatalogNetworkFailureError - | CatalogUnsafeRetrievalError - | CatalogMalformedCardError - | CatalogContractViolationError - | CatalogUnavailableTransportError - | CatalogNotInstallableError - | CatalogUnavailableError; +export type McpInstallationOperationStatus = + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation has been requested, not proof that a write was undone. + */ + cancellationRequested: boolean; + /** + * Original operation progress discriminator. + */ + phase: "preparing"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether the inert prepared operation was asked to cancel. + */ + cancellationRequested: boolean; + /** + * Original operation progress discriminator. + */ + phase: "prepared"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether the pending human callback was asked to cancel. + */ + cancellationRequested: boolean; + /** + * Original operation progress discriminator. + */ + phase: "awaiting-confirmation"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether source or authority revalidation was asked to cancel. + */ + cancellationRequested: boolean; + /** + * Original operation progress discriminator. + */ + phase: "revalidating"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether applying was asked to cancel; already-started effects retain their lease. + */ + cancellationRequested: boolean; + /** + * Original operation progress discriminator. + */ + phase: "applying"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation was requested before the terminal result. + */ + cancellationRequested: boolean; + outcome: McpInstallationOutcome; + /** + * Original operation progress discriminator. + */ + phase: "completed"; + }; /** - * MCP server configuration (stdio, remote HTTP/SSE, or in-process) + * Terminal mutation result. Uncertainty is not approval, rollback or permission to replay. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerConfig". + * via the `definition` "McpInstallationOutcome". */ /** @experimental */ -export type McpServerConfig = (McpServerConfigStdio | McpServerConfigHttp) | undefined; -/** - * 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. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpSamplingExecutionAction". +export type McpInstallationOutcome = + | { + installation: McpInstallationSummary; + /** + * Durable installation succeeded but final transaction cleanup remains. + */ + cleanupPending: boolean; + /** + * Terminal installation outcome discriminator. + */ + kind: "installed"; + } + | { + /** + * Exact removed receipt identity. + */ + installationId: string; + /** + * Original removal operation. + */ + operationId: string; + /** + * Durable removal succeeded but final cleanup remains. + */ + cleanupPending: boolean; + /** + * Whether protected pre-install configuration was restored. + */ + restoredPreviousConfiguration: boolean; + /** + * Exact owned input slots removed, excluding shared OAuth credentials. + */ + removedOwnedSecrets: number; + /** + * Any grants in the incumbent shared OAuth store remain unowned and retained. + */ + preservedSharedAuthentication: boolean; + /** + * Terminal installation outcome discriminator. + */ + kind: "uninstalled"; + } + | { + reason: McpInstallationFailureReason; + /** + * Original connection-owned operation. + */ + operationId: string; + /** + * Terminal installation outcome discriminator. + */ + kind: "rolled-back"; + } + | { + /** + * Operation whose durable result must be recovered and inspected. + */ + operationId: string; + /** + * Terminal installation outcome discriminator. + */ + kind: "recovery-required"; + } + | { + /** + * Original operation explicitly declined by the user. + */ + operationId: string; + /** + * Terminal installation outcome discriminator. + */ + kind: "declined"; + } + | { + /** + * Original operation cancelled before a terminal application result. + */ + operationId: string; + /** + * Terminal installation outcome discriminator. + */ + kind: "cancelled"; + } + | { + reason: McpInstallationFailureReason; + /** + * Present once an operation has been allocated; never a plan handle. + */ + operationId?: string; + /** + * Terminal installation outcome discriminator. + */ + kind: "refused"; + }; +/** + * Management result with contract receipt, or a typed request/negotiation refusal. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpInstallationManagementResult". */ /** @experimental */ -export type McpSamplingExecutionAction = - /** The sampling inference completed and produced a result. */ - | "success" - /** The sampling inference failed or was rejected. */ - | "failure" - /** The sampling inference was cancelled before completion. */ - | "cancelled"; +export type McpInstallationManagementResult = + | { + /** + * A handled management outcome with an honoured contract. + */ + kind: "outcome"; + outcome: McpInstallationManagementOutcome; + negotiated: CatalogNegotiatedContract; + } + | CatalogNegotiationRefusedError + | CatalogInvalidRequestError; /** - * In-process MCP transport type. + * An installation result together with the exact honoured contract, or a negotiation refusal. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerConfigMemoryType". + * via the `definition` "McpInstallationResult". */ /** @experimental */ -/** @internal */ -export type McpServerConfigMemoryType = "memory"; +export type McpInstallationResult = + | { + /** + * A handled operation outcome with an honoured contract. + */ + kind: "outcome"; + outcome: McpInstallationOutcome; + negotiated: CatalogNegotiatedContract; + } + | CatalogNegotiationRefusedError + | CatalogInvalidRequestError; /** - * 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". + * Consumer allowed to call an MCP tool. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpSetEnvValueModeDetails". + * via the `definition` "McpToolUiVisibility". */ /** @experimental */ -export type McpSetEnvValueModeDetails = - /** Treat MCP server environment values as literal strings. */ - | "direct" - /** Treat MCP server environment values as host-side references to resolve before launch. */ - | "indirect"; +export type McpToolUiVisibility = + /** The model may call the tool. */ + | "model" + /** An MCP App view may call the tool. */ + | "app"; /** - * Per-source context-window attribution, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + * Host response to the pending OAuth request. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionContextAttribution". + * via the `definition` "McpOauthPendingRequestResponse". */ /** @experimental */ -export type SessionContextAttribution = { - /** - * 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. - */ - totalTokens: number; - /** - * 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. - */ - modelId: string; - /** - * How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). - */ - modelSource: string; - /** - * Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. - */ - promptTokenLimit: number; - /** - * Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. - */ - limit: number; - /** - * Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. - */ - bufferTokens: number; - /** - * Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. - */ - compactionThreshold: number; - /** - * 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`. - */ - categories: { - /** - * System prompt tokens, excluding custom instructions. - */ - systemPrompt: number; - /** - * Custom-instructions tokens (0 when none are configured). - */ - customInstructions: number; - /** - * Non-MCP tool-definition tokens. - */ - systemTools: number; - /** - * MCP tool-definition tokens. - */ - mcpTools: number; - /** - * Conversation (user/assistant/tool) message tokens. - */ - messages: number; - /** - * Remaining unused window capacity (clamped at 0). - */ - freeSpace: number; - /** - * Output reserve plus post-blocking-threshold buffer. - */ - buffer: number; - }; - /** - * Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. - */ - entries: { - /** - * Source category for this entry. Not a closed set — tolerate unknown values. Known values today: `skill`, `subagent`, `mcpServer`, `tool`, `system`, `toolDefinition`, `plugin`. - */ - kind: string; - /** - * 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`. - */ - id: string; - /** - * Human-readable display label, e.g. `bash` or `skill: tmux`. Presentation-only; may be localized/reformatted without notice — do not key off it. - */ - label: string; - /** - * Token count currently in context attributable to this entry. - */ - tokens: number; - /** - * 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. - */ - parentId?: string; - /** - * Supplementary per-entry metadata (e.g. `messageCount`, `role`, `evictable`, `pluginSource`). Values are stringified; parse as needed and ignore unrecognized keys. - */ - attributes?: { - [k: string]: string | undefined; +export type McpOauthPendingRequestResponse = + | { + /** + * Access token acquired by the SDK host + */ + accessToken: string; + /** + * OAuth token type. Defaults to bearer when omitted. + */ + tokenType?: string; + /** + * Token lifetime in seconds, if known. + */ + expiresIn?: number; + /** + * OAuth response variant discriminator. + */ + kind: "token"; + } + | { + /** + * OAuth response variant discriminator. + */ + kind: "cancelled"; }; - }[]; - /** - * Successful compaction history for the session. - */ - compactions: { - /** - * Number of successful compactions in this session. - */ - count: number; - }; -} | null; /** - * Token breakdown for the current context window, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + * OAuth grant type override for this login. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionContextInfo". + * via the `definition` "McpOauthLoginGrantType". */ /** @experimental */ -export type SessionContextInfo = { - /** - * The model used for token counting - */ - modelName: string; - /** - * Tokens consumed by the system prompt - */ - systemTokens: number; - /** - * Tokens consumed by user/assistant/tool messages - */ - conversationTokens: number; - /** - * Tokens consumed by tool definitions sent to the model (excludes deferred tools) - */ - toolDefinitionsTokens: number; - /** - * Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) - */ - mcpToolsTokens: number; - /** - * Sum of system, conversation and tool-definition tokens - */ - totalTokens: number; - /** - * Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) - */ - promptTokenLimit: number; - /** - * Token count at which background compaction starts (configurable percentage of promptTokenLimit) - */ - compactionThreshold: number; - /** - * Prompt token limit plus the model's full output token limit. - */ - limit: number; - /** - * Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) - */ - bufferTokens: number; -} | null; -/** - * Hosting platform type of the repository - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionWorkingDirectoryContextHostType". - */ -/** @experimental */ -export type SessionWorkingDirectoryContextHostType = - /** The working directory repository is hosted on GitHub. */ - | "github" - /** The working directory repository is hosted on Azure DevOps. */ - | "ado"; +export type McpOauthLoginGrantType = + /** Interactive browser-based OAuth flow using an authorization code, typically with PKCE. */ + | "authorization_code" + /** Headless OAuth flow where a confidential client authenticates directly with a client secret. */ + | "client_credentials"; /** - * The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot') + * Outcome of starting the original prepared owned login. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataSnapshotCurrentMode". + * via the `definition` "McpOwnedOauthLoginStatus". */ /** @experimental */ -export type MetadataSnapshotCurrentMode = - /** The agent is responding interactively to the user. */ - | "interactive" - /** The agent is preparing a plan before making changes. */ - | "plan" - /** The agent is working autonomously toward task completion. */ - | "autopilot"; +export type McpOwnedOauthLoginStatus = + /** The original requester may open the returned authorisation URL. */ + | "awaiting-browser" + /** Cached credentials were accepted and the original server finished reconnecting. */ + | "connected"; /** - * Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. + * Why a passive MCP OAuth probe determined authentication is needed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataSnapshotRemoteMetadataTaskType". + * via the `definition` "McpOauthProbeNeedsAuthReason". */ /** @experimental */ -export type MetadataSnapshotRemoteMetadataTaskType = - /** Remote task originated from Copilot Coding Agent. */ - | "cca" - /** Remote task originated from a CLI remote-session invocation. */ - | "cli"; +export type McpOauthProbeNeedsAuthReason = + /** No token was sent and the server requires authentication. */ + | "initial" + /** A cached token was sent and rejected. */ + | "refresh" + /** The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. */ + | "upscope"; /** - * Current policy state for this model + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelPolicyState". + * via the `definition` "McpOauthProbeResult". */ /** @experimental */ -export type ModelPolicyState = - /** The model is enabled by policy. */ - | "enabled" - /** The model is disabled by policy. */ - | "disabled" - /** No explicit policy is configured for the model. */ - | "unconfigured"; +export type McpOauthProbeResult = + | { + httpResponse: McpOauthHttpResponse; + /** + * Probe outcome variant discriminator. + */ + status: "no-auth-required"; + } + | { + httpResponse: McpOauthHttpResponse; + /** + * Probe outcome variant discriminator. + */ + status: "authenticated"; + } + | { + httpResponse: McpOauthHttpResponse; + reason: McpOauthProbeNeedsAuthReason; + wwwAuthenticateParams?: McpOauthWWWAuthenticateParams; + /** + * Probe outcome variant discriminator. + */ + status: "needs-auth"; + } + | { + /** + * Human-readable probe failure detail. + */ + error: string; + httpResponse?: McpOauthHttpResponse; + /** + * Probe outcome variant discriminator. + */ + status: "failed"; + }; /** - * Model capability category for grouping in the model picker + * What an install plan is computed from: a candidate handle from a previous search, or a card supplied directly. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelPickerCategory". + * via the `definition` "McpPlanInstallSource". */ /** @experimental */ -export type ModelPickerCategory = - /** Lightweight model category optimized for faster, lower-cost interactions. */ - | "lightweight" - /** Versatile model category suitable for a broad range of tasks. */ - | "versatile" - /** Powerful model category optimized for complex tasks. */ - | "powerful"; +export type McpPlanInstallSource = McpPlanInstallSourceCandidate | McpPlanInstallSourceCard; /** - * Relative cost tier for token-based billing users + * Discriminator for a candidate-backed install-plan source * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelPickerPriceCategory". + * via the `definition` "McpPlanInstallSourceCandidateKind". */ /** @experimental */ -export type ModelPickerPriceCategory = - /** Lowest relative token cost tier. */ - | "low" - /** Medium relative token cost tier. */ - | "medium" - /** High relative token cost tier. */ - | "high" - /** Highest relative token cost tier. */ - | "very_high"; +export type McpPlanInstallSourceCandidateKind = /** Plan from a candidate returned by catalog search. */ "candidate"; /** - * Optional listing options. + * Discriminator for a caller-supplied-card install-plan source * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelListRequest". + * via the `definition` "McpPlanInstallSourceCardKind". */ /** @experimental */ -export type ModelListRequest = - | { - [k: string]: unknown | undefined; - } - | { - /** - * If true, bypasses the per-session model list cache and re-fetches from CAPI. - */ - skipCache?: boolean; - }; +export type McpPlanInstallSourceCardKind = /** Plan directly from a caller-supplied card. */ "card"; /** - * Whether the requested preference was already effective or was accepted for later transactional activation. + * A card supplied directly by the caller. Exactly one of a URL or embedded data, encoded structurally so neither both nor neither can be expressed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelSwitchAutoTierStatus". + * via the `definition` "McpServerCardReference". */ /** @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 McpServerCardReference = McpServerCardUrl | McpServerCardEmbedded; /** - * Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. + * Discriminator for a URL-backed MCP server card * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderConfigType". + * via the `definition` "McpServerCardUrlKind". */ /** @experimental */ -export type ProviderConfigType = - /** Generic OpenAI-compatible API. */ - | "openai" - /** Azure OpenAI Service endpoint. */ - | "azure" - /** Anthropic API endpoint. */ - | "anthropic"; +export type McpServerCardUrlKind = /** Retrieve the card from its URL. */ "url"; /** - * Wire API format (openai/azure only). Defaults to "completions". + * Discriminator for an embedded MCP server card * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderConfigWireApi". + * via the `definition` "McpServerCardEmbeddedKind". */ /** @experimental */ -export type ProviderConfigWireApi = - /** OpenAI Chat Completions wire format. */ - | "completions" - /** OpenAI Responses API wire format. */ - | "responses"; +export type McpServerCardEmbeddedKind = /** Use the embedded card document. */ "embedded"; /** - * Provider transport. Defaults to "http". + * Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderConfigTransport". + * via the `definition` "McpPlanInstallResult". */ /** @experimental */ -export type ProviderConfigTransport = - /** HTTP request/streaming transport. */ - | "http" - /** WebSocket transport. */ - | "websockets"; +export type McpPlanInstallResult = + | McpPlanInstallPlanned + | CatalogNegotiationRefusedError + | CatalogHandleRejectedError + | CatalogInvalidRequestError + | CatalogAuthenticationRequiredError + | CatalogPolicyRejectedError + | CatalogNetworkFailureError + | CatalogUnsafeRetrievalError + | CatalogMalformedCardError + | CatalogContractViolationError + | CatalogUnavailableTransportError + | CatalogNotInstallableError + | CatalogUnavailableError; /** - * Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. + * The sender role of an MCP prompt message. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "OptionsUpdateAdditionalContentExclusionPolicyScope". + * via the `definition` "McpPromptRole". */ /** @experimental */ -export type OptionsUpdateAdditionalContentExclusionPolicyScope = - /** The content exclusion policy applies to the current repository. */ - | "repo" - /** The content exclusion policy applies across all repositories. */ - | "all"; +export type McpPromptRole = + /** A message from the user. */ + | "user" + /** A message from the assistant. */ + | "assistant"; /** - * 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. + * MCP server configuration (stdio, remote HTTP/SSE, or in-process) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "OptionsUpdateContextTier". + * via the `definition` "McpServerConfig". */ /** @experimental */ -export type OptionsUpdateContextTier = - /** Use the model's default context tier and its standard token limits / pricing. */ - | "default" - /** Use the model's long-context tier (when available) so larger inputs are accepted and tier-specific pricing applies. */ - | "long_context"; +export type McpServerConfig = (McpServerConfigStdio | McpServerConfigHttp) | undefined; /** - * How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "OptionsUpdateEnvValueMode". + * via the `definition` "McpSamplingExecutionAction". */ /** @experimental */ -export type OptionsUpdateEnvValueMode = - /** Pass MCP server environment values as literal strings. */ - | "direct" - /** Resolve MCP server environment values from host-side references. */ - | "indirect"; +export type McpSamplingExecutionAction = + /** The sampling inference completed and produced a result. */ + | "success" + /** The sampling inference failed or was rejected. */ + | "failure" + /** The sampling inference was cancelled before completion. */ + | "cancelled"; /** - * Reasoning summary mode for supported model clients. + * In-process MCP transport type. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "OptionsUpdateReasoningSummary". + * via the `definition` "McpServerConfigMemoryType". */ /** @experimental */ -export type OptionsUpdateReasoningSummary = - /** Do not request reasoning summaries from the model. */ - | "none" - /** Request a concise summary of model reasoning. */ - | "concise" - /** Request a detailed summary of model reasoning. */ - | "detailed"; +/** @internal */ +export type McpServerConfigMemoryType = "memory"; /** - * Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. + * 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". * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "OptionsUpdateToolFilterPrecedence". + * via the `definition` "McpSetEnvValueModeDetails". */ /** @experimental */ -export type OptionsUpdateToolFilterPrecedence = - /** If availableTools is set, it is the only constraint that applies (excludedTools is ignored). Preserves CLI / pre-existing client behavior. Default. */ - | "available" - /** A tool is enabled if and only if it matches the allowlist (or the allowlist is unset) AND it does not match the denylist. Makes 'all except X' expressible by combining the two lists. */ - | "excluded"; +export type McpSetEnvValueModeDetails = + /** Treat MCP server environment values as literal strings. */ + | "direct" + /** Treat MCP server environment values as host-side references to resolve before launch. */ + | "indirect"; /** - * The client's response to the pending permission prompt + * Per-source context-window attribution, or null if the session has not yet been initialized (no system prompt or tool metadata cached). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionDecision". + * via the `definition` "SessionContextAttribution". */ /** @experimental */ -export type PermissionDecision = - | PermissionDecisionApproveOnce - | PermissionDecisionApproveForSession - | PermissionDecisionApproveForLocation - | PermissionDecisionApprovePermanently - | PermissionDecisionReject - | PermissionDecisionUserNotAvailable - | PermissionDecisionApproved - | PermissionDecisionApprovedForSession - | PermissionDecisionApprovedForLocation - | PermissionDecisionCancelled - | PermissionDecisionDeniedByRules - | PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser - | PermissionDecisionDeniedInteractivelyByUser - | PermissionDecisionDeniedByContentExclusionPolicy - | PermissionDecisionDeniedByPermissionRequestHook; +export type SessionContextAttribution = { + /** + * 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. + */ + totalTokens: number; + /** + * 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. + */ + modelId: string; + /** + * How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). + */ + modelSource: string; + /** + * Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. + */ + promptTokenLimit: number; + /** + * Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. + */ + limit: number; + /** + * Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + */ + bufferTokens: number; + /** + * Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. + */ + compactionThreshold: number; + /** + * 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`. + */ + categories: { + /** + * System prompt tokens, excluding custom instructions. + */ + systemPrompt: number; + /** + * Custom-instructions tokens (0 when none are configured). + */ + customInstructions: number; + /** + * Non-MCP tool-definition tokens. + */ + systemTools: number; + /** + * MCP tool-definition tokens. + */ + mcpTools: number; + /** + * Conversation (user/assistant/tool) message tokens. + */ + messages: number; + /** + * Remaining unused window capacity (clamped at 0). + */ + freeSpace: number; + /** + * Overlapping output reservation plus post-blocking-threshold buffer. + */ + buffer: number; + }; + /** + * Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. + */ + entries: { + /** + * Source category for this entry. Not a closed set — tolerate unknown values. Known values today: `skill`, `subagent`, `mcpServer`, `tool`, `system`, `toolDefinition`, `plugin`. + */ + kind: string; + /** + * 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`. + */ + id: string; + /** + * Human-readable display label, e.g. `bash` or `skill: tmux`. Presentation-only; may be localized/reformatted without notice — do not key off it. + */ + label: string; + /** + * Token count currently in context attributable to this entry. + */ + tokens: number; + /** + * 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. + */ + parentId?: string; + /** + * Supplementary per-entry metadata (e.g. `messageCount`, `role`, `evictable`, `pluginSource`). Values are stringified; parse as needed and ignore unrecognized keys. + */ + attributes?: { + [k: string]: string | undefined; + }; + }[]; + /** + * Successful compaction history for the session. + */ + compactions: { + /** + * Number of successful compactions in this session. + */ + count: number; + }; +} | null; /** - * Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) + * Token breakdown for the current context window, or null if the session has not yet been initialized (no system prompt or tool metadata cached). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionDecisionApproveForSessionApproval". + * via the `definition` "SessionContextInfo". */ /** @experimental */ -export type PermissionDecisionApproveForSessionApproval = - | PermissionDecisionApproveForSessionApprovalCommands - | PermissionDecisionApproveForSessionApprovalRead - | PermissionDecisionApproveForSessionApprovalWrite - | PermissionDecisionApproveForSessionApprovalMcp - | PermissionDecisionApproveForSessionApprovalMcpSampling - | PermissionDecisionApproveForSessionApprovalMemory - | PermissionDecisionApproveForSessionApprovalCustomTool - | PermissionDecisionApproveForSessionApprovalExtensionManagement - | PermissionDecisionApproveForSessionApprovalFactory - | PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess - | PermissionDecisionApproveForSessionApprovalExtensionEnvAccess; +export type SessionContextInfo = { + /** + * The model used for token counting + */ + modelName: string; + /** + * Tokens consumed by the system prompt + */ + systemTokens: number; + /** + * Tokens consumed by user/assistant/tool messages + */ + conversationTokens: number; + /** + * Tokens consumed by tool definitions sent to the model (excludes deferred tools) + */ + toolDefinitionsTokens: number; + /** + * Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) + */ + mcpToolsTokens: number; + /** + * Sum of system, conversation and tool-definition tokens + */ + totalTokens: number; + /** + * Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. + */ + promptTokenLimit: number; + /** + * Token count at which background compaction starts (configurable percentage of promptTokenLimit) + */ + compactionThreshold: number; + /** + * Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. + */ + limit: number; + /** + * Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). + */ + bufferTokens: number; +} | null; /** - * Approval to persist for this location + * Hosting platform type of the repository * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionDecisionApproveForLocationApproval". + * via the `definition` "SessionWorkingDirectoryContextHostType". */ /** @experimental */ -export type PermissionDecisionApproveForLocationApproval = - | PermissionDecisionApproveForLocationApprovalCommands - | PermissionDecisionApproveForLocationApprovalRead - | PermissionDecisionApproveForLocationApprovalWrite - | PermissionDecisionApproveForLocationApprovalMcp - | PermissionDecisionApproveForLocationApprovalMcpSampling - | PermissionDecisionApproveForLocationApprovalMemory - | PermissionDecisionApproveForLocationApprovalCustomTool - | PermissionDecisionApproveForLocationApprovalExtensionManagement - | PermissionDecisionApproveForLocationApprovalFactory - | PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess - | PermissionDecisionApproveForLocationApprovalExtensionEnvAccess; +export type SessionWorkingDirectoryContextHostType = + /** The working directory repository is hosted on GitHub. */ + | "github" + /** The working directory repository is hosted on Azure DevOps. */ + | "ado"; /** - * Disposition of a permission request as observed by the responding client. + * The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot') * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionDecisionOutcome". + * via the `definition` "MetadataSnapshotCurrentMode". */ /** @experimental */ -export type PermissionDecisionOutcome = - /** The request was approved automatically without a new human decision. */ - | "auto_approved" - /** The request was denied without an interactive user decision; source records why. */ - | "autopilot_denied" - /** The response came from an interactive user prompt. */ - | "prompted_user"; +export type MetadataSnapshotCurrentMode = + /** The agent is responding interactively to the user. */ + | "interactive" + /** The agent is preparing a plan before making changes. */ + | "plan" + /** The agent is working autonomously toward task completion. */ + | "autopilot"; /** - * Client surface that submitted a permission response. + * Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionDecisionSurface". + * via the `definition` "MetadataSnapshotRemoteMetadataTaskType". */ /** @experimental */ -export type PermissionDecisionSurface = - /** The interactive Copilot CLI terminal UI. */ - | "tui" - /** The non-interactive Copilot CLI prompt mode. */ - | "prompt_mode" - /** The Copilot App client. */ - | "copilot_app" - /** An Agent Client Protocol host. */ - | "acp" - /** A generic Copilot SDK client. */ - | "sdk"; +export type MetadataSnapshotRemoteMetadataTaskType = + /** Remote task originated from Copilot Coding Agent. */ + | "cca" + /** Remote task originated from a CLI remote-session invocation. */ + | "cli"; /** - * Response capability available to the client when it settled a permission request. + * Current policy state for this model * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionResponseCapability". + * via the `definition` "ModelPolicyState". */ /** @experimental */ -export type PermissionResponseCapability = - /** The client could ask a user for this decision. */ - | "interactive" - /** The client could return an automated response but could not ask a user. */ - | "headless" - /** The client had no response path available. */ - | "none"; +export type ModelPolicyState = + /** The model is enabled by policy. */ + | "enabled" + /** The model is disabled by policy. */ + | "disabled" + /** No explicit policy is configured for the model. */ + | "unconfigured"; /** - * Tool approval to persist and apply + * Model capability category for grouping in the model picker * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionsLocationsAddToolApprovalDetails". + * via the `definition` "ModelPickerCategory". */ /** @experimental */ -export type PermissionsLocationsAddToolApprovalDetails = - | PermissionsLocationsAddToolApprovalDetailsCommands - | PermissionsLocationsAddToolApprovalDetailsRead - | PermissionsLocationsAddToolApprovalDetailsWrite - | PermissionsLocationsAddToolApprovalDetailsMcp - | PermissionsLocationsAddToolApprovalDetailsMcpSampling - | PermissionsLocationsAddToolApprovalDetailsMemory - | PermissionsLocationsAddToolApprovalDetailsCustomTool - | PermissionsLocationsAddToolApprovalDetailsExtensionManagement - | PermissionsLocationsAddToolApprovalDetailsFactory - | PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess - | PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess; +export type ModelPickerCategory = + /** Lightweight model category optimized for faster, lower-cost interactions. */ + | "lightweight" + /** Versatile model category suitable for a broad range of tasks. */ + | "versatile" + /** Powerful model category optimized for complex tasks. */ + | "powerful"; /** - * Whether the location is a git repo or directory + * Relative cost tier for token-based billing users * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionLocationType". + * via the `definition` "ModelPickerPriceCategory". */ /** @experimental */ -export type PermissionLocationType = - /** The permission location is persisted at the git repository root. */ - | "repo" - /** The permission location is persisted at the working directory. */ - | "dir"; +export type ModelPickerPriceCategory = + /** Lowest relative token cost tier. */ + | "low" + /** Medium relative token cost tier. */ + | "medium" + /** High relative token cost tier. */ + | "high" + /** Highest relative token cost tier. */ + | "very_high"; /** - * Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionModeSource". + * via the `definition` "ModelProviderKind". */ /** @experimental */ -export type PermissionModeSource = - /** The mode was set from a CLI command-line flag. */ - | "cli_flag" - /** The mode was set by a slash command. */ - | "slash_command" - /** The mode was set by confirming autopilot behavior. */ - | "autopilot_confirmation" - /** The mode was set at startup by the `defaultPermissionMode` user setting. */ - | "user_setting" - /** The mode was set at startup by authenticated organization targeting. */ - | "organization_targeting" - /** The mode was set through an RPC caller. */ - | "rpc"; +export type ModelProviderKind = + /** GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. */ + | "copilot" + /** Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. */ + | "loki"; /** - * Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. + * Optional listing options. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionsConfigureAdditionalContentExclusionPolicyScope". + * via the `definition` "ModelListRequest". */ /** @experimental */ -export type PermissionsConfigureAdditionalContentExclusionPolicyScope = - /** The content exclusion policy applies to the current repository. */ - | "repo" - /** The content exclusion policy applies across all repositories. */ - | "all"; +export type ModelListRequest = + | { + [k: string]: unknown | undefined; + } + | { + /** + * If true, bypasses the per-session model list cache and re-fetches from CAPI. + */ + skipCache?: boolean; + }; /** - * 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. + * When the runtime may run an adapter without an explicit user action. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionsModifyRulesScope". + * via the `definition` "ModelProviderAutomaticDiscoveryMode". */ /** @experimental */ -export type PermissionsModifyRulesScope = - /** Apply the rule change only to this session. */ - | "session" - /** Persist the rule change for this project location. */ - | "location"; +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"; /** - * Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. + * Network reach an adapter may use during discovery. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionsSetApproveAllSource". + * via the `definition` "ModelProviderDiscoveryNetworkScope". */ /** @experimental */ -export type PermissionsSetApproveAllSource = - /** Allow-all was enabled from a CLI command-line flag. */ - | "cli_flag" - /** Allow-all was enabled by a slash command. */ - | "slash_command" - /** Allow-all was enabled by confirming autopilot behavior. */ - | "autopilot_confirmation" - /** Allow-all was enabled at startup by the `defaultPermissionMode` user setting. */ - | "user_setting" - /** Allow-all was enabled through an RPC caller. */ - | "rpc"; +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"; /** - * Where completed plugin content was staged before atomic promotion. + * 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` "PluginInstallStagingMode". + * via the `definition` "ModelProviderConfigurationDisposition". */ /** @experimental */ -export type PluginInstallStagingMode = - /** A sibling of the installed-plugins root, outside the recursively watched tree. */ - | "external" - /** A sibling of the destination plugin directory, used when external staging is unavailable. */ - | "destination_sibling"; +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"; /** - * Optional flags controlling which side effects the reload performs. + * Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PluginsReloadRequest". + * via the `definition` "ProviderConfigType". */ /** @experimental */ -export type PluginsReloadRequest = - | { - [k: string]: unknown | undefined; - } - | { - /** - * Reload MCP server connections after refreshing plugins. Defaults to true. - */ - reloadMcp?: boolean; - /** - * Re-run custom-agent discovery after refreshing plugins. Defaults to true. - */ - reloadCustomAgents?: boolean; - /** - * 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). - */ - reloadHooks?: boolean; - /** - * 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). - */ - reloadExtensions?: boolean; - /** - * When true, skip repo-level hooks during the hook reload. Use before folder trust is confirmed; load them post-trust via `sessions.loadDeferredRepoHooks`. - */ - deferRepoHooks?: boolean; - }; - +export type ProviderConfigType = + /** Generic OpenAI-compatible API. */ + | "openai" + /** Azure OpenAI Service endpoint. */ + | "azure" + /** Anthropic API endpoint. */ + | "anthropic"; +/** + * Wire API format (openai/azure only). Defaults to "completions". + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderConfigWireApi". + */ /** @experimental */ -export type ProtocolAppendMode = "append"; - +export type ProviderConfigWireApi = + /** OpenAI Chat Completions wire format. */ + | "completions" + /** OpenAI Responses API wire format. */ + | "responses"; +/** + * Provider transport. Defaults to "http". + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderConfigTransport". + */ /** @experimental */ -export type ProtocolCustomizeMode = "customize"; +export type ProviderConfigTransport = + /** HTTP request/streaming transport. */ + | "http" + /** WebSocket transport. */ + | "websockets"; /** - * Controls whether the runtime may defer loading an external tool definition. + * The product serving the model, reported in telemetry as `model_provider`. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProtocolExternalToolDefer". + * via the `definition` "ProviderConfigModelProvider". */ /** @experimental */ -export type ProtocolExternalToolDefer = - /** The runtime may defer the tool according to its tool-loading policy. */ - | "auto" - /** The runtime must include the tool without deferring it. */ - | "never"; +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 ProtocolMarkerSectionOverride = - | { - /** - * Section override action discriminator. - */ - action: "transform"; - } - | { - /** - * Section override action discriminator. - */ - action: "preserve"; - }; +export type ProtocolSystemMessageConfig = + | ProtocolSystemMessageAppendConfig + | ProtocolSystemMessageReplaceConfig + | ProtocolSystemMessageCustomizeConfig; + +/** @experimental */ +export type ProtocolAppendMode = "append"; /** @experimental */ export type ProtocolReplaceMode = "replace"; /** @experimental */ -export type ProtocolSectionOverride = ProtocolStaticSectionOverride | ProtocolMarkerSectionOverride; +export type ProtocolCustomizeMode = "customize"; + +/** @experimental */ +export type ProtocolSectionOverride = (ProtocolStaticSectionOverride | ProtocolMarkerSectionOverride) | undefined; /** @experimental */ export type ProtocolStaticSectionAction = @@ -3396,10 +3953,19 @@ export type ProtocolStaticSectionAction = | "prepend"; /** @experimental */ -export type ProtocolSystemMessageConfig = - | ProtocolSystemMessageAppendConfig - | ProtocolSystemMessageReplaceConfig - | ProtocolSystemMessageCustomizeConfig; +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. * @@ -3439,3354 +4005,4772 @@ export type ProviderEndpointTransport = /** WebSocket transport. */ | "websockets"; /** - * Optional model identifier to scope the endpoint snapshot to. + * 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` "ProviderGetEndpointRequest". + * via the `definition` "ModelSwitchAutoTierStatus". */ /** @experimental */ -export type ProviderGetEndpointRequest = - | { - [k: string]: unknown | undefined; - } - | { - /** - * 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. - */ - modelId?: string; - }; +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"; /** - * Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. + * Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PushAttachment". + * via the `definition` "OptionsUpdateAdditionalContentExclusionPolicyScope". */ /** @experimental */ -export type PushAttachment = - | PushAttachmentFile - | PushAttachmentDirectory - | PushAttachmentSelection - | PushAttachmentGitHubReference - | PushAttachmentGitHubCommit - | PushAttachmentGitHubRelease - | PushAttachmentGitHubActionsJob - | PushAttachmentGitHubRepository - | PushAttachmentGitHubFileDiff - | PushAttachmentGitHubTreeComparison - | PushAttachmentGitHubUrl - | PushAttachmentGitHubFile - | PushAttachmentGitHubSnippet - | PushAttachmentBlob - | ExtensionContextPushInput; +export type OptionsUpdateAdditionalContentExclusionPolicyScope = + /** The content exclusion policy applies to the current repository. */ + | "repo" + /** The content exclusion policy applies across all repositories. */ + | "all"; /** - * Type of GitHub reference + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PushAttachmentGitHubReferenceType". + * via the `definition` "OptionsUpdateContextTier". */ /** @experimental */ -export type PushAttachmentGitHubReferenceType = - /** GitHub issue reference. */ - | "issue" - /** GitHub pull request reference. */ - | "pr" - /** GitHub discussion reference. */ - | "discussion"; -/** - * The UI mode the agent was in when this message was sent. Defaults to the session's current mode. - * +export type OptionsUpdateContextTier = + /** Use the model's default context tier and its standard token limits / pricing. */ + | "default" + /** Use the model's long-context tier (when available) so larger inputs are accepted and tier-specific pricing applies. */ + | "long_context"; +/** + * How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). + * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SendAgentMode". + * via the `definition` "OptionsUpdateEnvValueMode". */ /** @experimental */ -export type SendAgentMode = - /** The agent is responding interactively to the user. */ - | "interactive" - /** The agent is preparing a plan before making changes. */ - | "plan" - /** The agent is working autonomously toward task completion. */ - | "autopilot" - /** The agent is in shell-focused UI mode. */ - | "shell"; +export type OptionsUpdateEnvValueMode = + /** Pass MCP server environment values as literal strings. */ + | "direct" + /** Resolve MCP server environment values from host-side references. */ + | "indirect"; /** - * How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + * Reasoning summary mode for supported model clients. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SendMode". + * via the `definition` "OptionsUpdateReasoningSummary". */ /** @experimental */ -export type SendMode = - /** Append the message to the normal session queue. */ - | "enqueue" - /** Interject the message during the in-progress turn. */ - | "immediate"; +export type OptionsUpdateReasoningSummary = + /** Do not request reasoning summaries from the model. */ + | "none" + /** Request a concise summary of model reasoning. */ + | "concise" + /** Request a detailed summary of model reasoning. */ + | "detailed"; /** - * Whether this item is a queued user message or a queued slash command / model change + * Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "QueuePendingItemsKind". + * via the `definition` "OptionsUpdateToolFilterPrecedence". */ /** @experimental */ -export type QueuePendingItemsKind = - /** A queued user message. */ - | "message" - /** A queued slash command or model-change command. */ - | "command"; +export type OptionsUpdateToolFilterPrecedence = + /** If availableTools is set, it is the only constraint that applies (excludedTools is ignored). Preserves CLI / pre-existing client behavior. Default. */ + | "available" + /** A tool is enabled if and only if it matches the allowlist (or the allowlist is unset) AND it does not match the denylist. Makes 'all except X' expressible by combining the two lists. */ + | "excluded"; /** - * State of the runtime-managed remote-control singleton. + * The client's response to the pending permission prompt * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "RemoteControlStatus". + * via the `definition` "PermissionDecision". */ /** @experimental */ -export type RemoteControlStatus = - | RemoteControlStatusOff - | RemoteControlStatusConnecting - | RemoteControlStatusActive - | RemoteControlStatusError; +export type PermissionDecision = + | PermissionDecisionApproveOnce + | PermissionDecisionApproveForSession + | PermissionDecisionApproveReadOnlyForSession + | PermissionDecisionApproveForLocation + | PermissionDecisionApprovePermanently + | PermissionDecisionReject + | PermissionDecisionUserNotAvailable + | PermissionDecisionApproved + | PermissionDecisionApprovedForSession + | PermissionDecisionApprovedForLocation + | PermissionDecisionCancelled + | PermissionDecisionDeniedByRules + | PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser + | PermissionDecisionDeniedInteractivelyByUser + | PermissionDecisionDeniedByContentExclusionPolicy + | PermissionDecisionDeniedByPermissionRequestHook; /** - * Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. + * Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "RemoteSessionMode". + * via the `definition` "PermissionDecisionApproveForSessionApproval". */ /** @experimental */ -export type RemoteSessionMode = - /** Disable remote session export and steering. */ - | "off" - /** Export session events to GitHub without enabling remote steering. */ - | "export" - /** Enable both remote session export and remote steering. */ - | "on"; +export type PermissionDecisionApproveForSessionApproval = + | PermissionDecisionApproveForSessionApprovalCommands + | PermissionDecisionApproveForSessionApprovalRead + | PermissionDecisionApproveForSessionApprovalWrite + | PermissionDecisionApproveForSessionApprovalMcp + | PermissionDecisionApproveForSessionApprovalMcpSampling + | PermissionDecisionApproveForSessionApprovalMemory + | PermissionDecisionApproveForSessionApprovalCustomTool + | PermissionDecisionApproveForSessionApprovalExtensionManagement + | PermissionDecisionApproveForSessionApprovalWorkflow + | PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess + | PermissionDecisionApproveForSessionApprovalExtensionEnvAccess; /** - * 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. + * Approval to persist for this location * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "RemoteSessionHostStatus". + * via the `definition` "PermissionDecisionApproveForLocationApproval". */ /** @experimental */ -export type RemoteSessionHostStatus = - /** No turn is running. */ - | "idle" - /** A turn is running. */ - | "working" - /** The session is blocked on the user: a permission prompt or an `ask_user` question. */ - | "input-needed" - /** The session ended its last turn in an error. */ - | "error"; +export type PermissionDecisionApproveForLocationApproval = + | PermissionDecisionApproveForLocationApprovalCommands + | PermissionDecisionApproveForLocationApprovalRead + | PermissionDecisionApproveForLocationApprovalWrite + | PermissionDecisionApproveForLocationApprovalMcp + | PermissionDecisionApproveForLocationApprovalMcpSampling + | PermissionDecisionApproveForLocationApprovalMemory + | PermissionDecisionApproveForLocationApprovalCustomTool + | PermissionDecisionApproveForLocationApprovalExtensionManagement + | PermissionDecisionApproveForLocationApprovalWorkflow + | PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess + | PermissionDecisionApproveForLocationApprovalExtensionEnvAccess; /** - * Whether the remote task originated from CCA or CLI `--remote`. + * Disposition of a permission request as observed by the responding client. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "RemoteSessionMetadataTaskType". + * via the `definition` "PermissionDecisionOutcome". */ /** @experimental */ -export type RemoteSessionMetadataTaskType = - /** GitHub Copilot coding agent task. */ - | "cca" - /** CLI remote task. */ - | "cli"; +export type PermissionDecisionOutcome = + /** The request was approved automatically without a new human decision. */ + | "auto_approved" + /** The request was denied without an interactive user decision; source records why. */ + | "autopilot_denied" + /** The response came from an interactive user prompt. */ + | "prompted_user"; /** - * Provider-native structured output format. JSON Schema is forwarded without rewriting or validating the schema or the generated output. + * Client surface that submitted a permission response. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ResponseFormat". + * via the `definition` "PermissionDecisionSurface". */ /** @experimental */ -export type ResponseFormat = { - jsonSchema: JsonSchemaResponseFormat; - /** - * Output format discriminator. Currently only json_schema is supported. - */ - type: "json_schema"; -}; +export type PermissionDecisionSurface = + /** The interactive Copilot CLI terminal UI. */ + | "tui" + /** The non-interactive Copilot CLI prompt mode. */ + | "prompt_mode" + /** The Copilot App client. */ + | "copilot_app" + /** An Agent Client Protocol host. */ + | "acp" + /** A generic Copilot SDK client. */ + | "sdk"; /** - * Origin of the sandbox choice supplied by an internal client. + * Response capability available to the client when it settled a permission request. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SandboxConfigSource". + * via the `definition` "PermissionResponseCapability". */ /** @experimental */ -/** @internal */ -export type SandboxConfigSource = - /** The client applied the default because no sandbox preference was configured. */ - | "never_configured" - /** The user's persisted settings enabled the sandbox. */ - | "user_enabled" - /** The user's persisted settings disabled the sandbox. */ - | "user_disabled" - /** A command-line flag selected the sandbox state for this session. */ - | "session_flag" - /** The user disabled the sandbox for the current session. */ - | "session_disabled" - /** The client disabled the sandbox because the host cannot enforce it. */ - | "unsupported_host" - /** A repository policy selected the sandbox state. */ - | "repository_policy"; +export type PermissionResponseCapability = + /** The client could ask a user for this decision. */ + | "interactive" + /** The client could return an automated response but could not ask a user. */ + | "headless" + /** The client had no response path available. */ + | "none"; /** - * 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. + * Tool approval to persist and apply * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SandboxHostCapabilityName". + * via the `definition` "PermissionsLocationsAddToolApprovalDetails". */ /** @experimental */ -export type SandboxHostCapabilityName = string; +export type PermissionsLocationsAddToolApprovalDetails = + | PermissionsLocationsAddToolApprovalDetailsCommands + | PermissionsLocationsAddToolApprovalDetailsRead + | PermissionsLocationsAddToolApprovalDetailsWrite + | PermissionsLocationsAddToolApprovalDetailsMcp + | PermissionsLocationsAddToolApprovalDetailsMcpSampling + | PermissionsLocationsAddToolApprovalDetailsMemory + | PermissionsLocationsAddToolApprovalDetailsCustomTool + | PermissionsLocationsAddToolApprovalDetailsExtensionManagement + | PermissionsLocationsAddToolApprovalDetailsWorkflow + | PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess + | PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess; /** - * A session-scoped sandbox transition applied while handling a slash command + * Whether the location is a git repo or directory * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SandboxSessionChange". + * via the `definition` "PermissionLocationType". */ /** @experimental */ -export type SandboxSessionChange = - /** The sandbox is off for the rest of this session; nothing was persisted and a new session starts from managed policy. */ - | "disabled" - /** A previous session-scoped opt-out was cleared and the sandbox is enforced again. */ - | "restored"; +export type PermissionLocationType = + /** The permission location is persisted at the git repository root. */ + | "repo" + /** The permission location is persisted at the working directory. */ + | "dir"; /** - * Current authentication information, or null when no authentication is active. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionAuthInfoResult". + * via the `definition` "PermissionModeSource". */ /** @experimental */ -export type SessionAuthInfoResult = AuthIdentity | null; +export type PermissionModeSource = + /** The mode was set from a CLI command-line flag. */ + | "cli_flag" + /** The mode was set by a slash command. */ + | "slash_command" + /** The mode was set by confirming autopilot behavior. */ + | "autopilot_confirmation" + /** The mode was set at startup by the `defaultPermissionMode` user setting. */ + | "user_setting" + /** The mode was set at startup because the authenticated account matched an organization targeting policy. */ + | "organization_targeting" + /** The mode was set through an RPC caller. */ + | "rpc"; /** - * Session capability enabled for this session + * Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionCapability". + * via the `definition` "PermissionsConfigureAdditionalContentExclusionPolicyScope". */ /** @experimental */ -export type SessionCapability = - /** TUI-specific prompt hints such as keyboard shortcuts. */ - | "tui-hints" - /** Plan-mode handling and instructions. */ - | "plan-mode" - /** Memory tool and memories prompt section. */ - | "memory" - /** Copilot CLI documentation tool and prompt section. */ - | "cli-documentation" - /** Interactive ask_user tool support. */ - | "ask-user" - /** Interactive CLI identity and behavior. */ - | "interactive-mode" - /** Automatic hidden system notifications. */ - | "system-notifications" - /** SDK elicitation support. */ - | "elicitation" - /** Cross-session history tools and session-store SQL prompt/tool metadata. */ - | "session-store" - /** MCP Apps UI passthrough. */ - | "mcp-apps" - /** Host-provided canvas rendering support. */ - | "canvas-renderer"; +export type PermissionsConfigureAdditionalContentExclusionPolicyScope = + /** The content exclusion policy applies to the current repository. */ + | "repo" + /** The content exclusion policy applies across all repositories. */ + | "all"; /** - * Error classification + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionFsErrorCode". + * via the `definition` "PermissionsModifyRulesScope". */ /** @experimental */ -export type SessionFsErrorCode = - /** The requested path does not exist. */ - | "ENOENT" - /** The filesystem operation failed for an unspecified reason. */ - | "UNKNOWN"; +export type PermissionsModifyRulesScope = + /** Apply the rule change only to this session. */ + | "session" + /** Persist the rule change for this project location. */ + | "location"; /** - * Entry type + * Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionFsReaddirWithTypesEntryType". + * via the `definition` "PermissionsSetApproveAllSource". */ /** @experimental */ -export type SessionFsReaddirWithTypesEntryType = - /** The entry is a file. */ - | "file" - /** The entry is a directory. */ - | "directory"; +export type PermissionsSetApproveAllSource = + /** Allow-all was enabled from a CLI command-line flag. */ + | "cli_flag" + /** Allow-all was enabled by a slash command. */ + | "slash_command" + /** Allow-all was enabled by confirming autopilot behavior. */ + | "autopilot_confirmation" + /** Allow-all was enabled at startup by the `defaultPermissionMode` user setting. */ + | "user_setting" + /** Allow-all was enabled through an RPC caller. */ + | "rpc"; /** - * Path conventions used by this filesystem + * Where completed plugin content was staged before atomic promotion. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionFsSetProviderConventions". + * via the `definition` "PluginInstallStagingMode". */ /** @experimental */ -export type SessionFsSetProviderConventions = - /** Paths use Windows path conventions. */ - | "windows" - /** Paths use POSIX path conventions. */ - | "posix"; +export type PluginInstallStagingMode = + /** A sibling of the installed-plugins root, outside the recursively watched tree. */ + | "external" + /** A sibling of the destination plugin directory, used when external staging is unavailable. */ + | "destination_sibling"; /** - * How to execute the query: 'exec' for DDL/multi-statement (no results), 'query' for SELECT (returns rows), 'run' for INSERT/UPDATE/DELETE (returns rowsAffected) + * Optional flags controlling which side effects the reload performs. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionFsSqliteQueryType". + * via the `definition` "PluginsReloadRequest". */ /** @experimental */ -export type SessionFsSqliteQueryType = - /** Execute DDL or multi-statement SQL without returning rows. */ - | "exec" - /** Execute a SELECT-style query and return rows. */ - | "query" - /** Execute INSERT, UPDATE, or DELETE SQL and return affected-row metadata. */ - | "run"; +export type PluginsReloadRequest = + | { + [k: string]: unknown | undefined; + } + | { + /** + * Reload MCP server connections after refreshing plugins. Defaults to true. + */ + reloadMcp?: boolean; + /** + * Re-run custom-agent discovery after refreshing plugins. Defaults to true. + */ + reloadCustomAgents?: boolean; + /** + * 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). + */ + reloadHooks?: boolean; + /** + * 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). + */ + reloadExtensions?: boolean; + /** + * When true, skip repo-level hooks during the hook reload. Use before folder trust is confirmed; load them post-trust via `sessions.loadDeferredRepoHooks`. + */ + deferRepoHooks?: boolean; + }; /** - * SQLite transaction failure classification. + * Controls whether the runtime may defer loading an external tool definition. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionFsSqliteTransactionErrorClass". + * via the `definition` "ProtocolExternalToolDefer". */ /** @experimental */ -export type SessionFsSqliteTransactionErrorClass = - /** SQLite reported BUSY or LOCKED before commit; the transaction was rolled back and may be retried. */ - | "busyOrLocked" - /** The statement, database, or provider failed definitively and must not be retried automatically. */ - | "fatal" - /** The transport failed after the provider may have committed; retrying could duplicate effects. */ - | "postCommitAmbiguous"; +export type ProtocolExternalToolDefer = + /** The runtime may defer the tool according to its tool-loading policy. */ + | "auto" + /** The runtime must include the tool without deferring it. */ + | "never"; /** - * Source descriptor for direct repo installs (when marketplace is empty) + * Optional model identifier to scope the endpoint snapshot to. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionInstalledPluginSource". + * via the `definition` "ProviderGetEndpointRequest". */ /** @experimental */ -export type SessionInstalledPluginSource = - | string - | SessionInstalledPluginSourceGitHub - | SessionInstalledPluginSourceUrl - | SessionInstalledPluginSourceLocal; +export type ProviderGetEndpointRequest = + | { + [k: string]: unknown | undefined; + } + | { + /** + * 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. + */ + modelId?: string; + }; /** - * Client population used for the prediction baseline. + * Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionLimitPredictionClientType". + * via the `definition` "PushAttachment". */ /** @experimental */ -export type SessionLimitPredictionClientType = - /** Interactive CLI sessions where a user can accept, edit, or top up the limit. */ - | "cli-interactive" - /** Prompt/non-interactive CLI sessions where the initial limit must cover more of the run. */ - | "cli-prompt"; +export type PushAttachment = + | PushAttachmentFile + | PushAttachmentDirectory + | PushAttachmentSelection + | PushAttachmentGitHubReference + | PushAttachmentGitHubCommit + | PushAttachmentGitHubRelease + | PushAttachmentGitHubActionsJob + | PushAttachmentGitHubRepository + | PushAttachmentGitHubFileDiff + | PushAttachmentGitHubTreeComparison + | PushAttachmentGitHubUrl + | PushAttachmentGitHubFile + | PushAttachmentGitHubSnippet + | PushAttachmentBlob + | ExtensionContextPushInput; /** - * Baseline fallback level used to create the prediction. + * Type of GitHub reference * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionLimitPredictionSource". + * via the `definition` "PushAttachmentGitHubReferenceType". */ /** @experimental */ -export type SessionLimitPredictionSource = - /** The prediction used the exact resolved model's baseline cell. */ - | "model" - /** The exact model was unavailable, so the prediction used the model family's baseline cell. */ - | "family" - /** No model or family cell was available, so the prediction used the global client-type baseline cell. */ - | "global"; +export type PushAttachmentGitHubReferenceType = + /** GitHub issue reference. */ + | "issue" + /** GitHub pull request reference. */ + | "pr" + /** GitHub discussion reference. */ + | "discussion"; /** - * Semantic usage tier used for a recommended cap or additional headroom. + * The UI mode the agent was in when this message was sent. Defaults to the session's current mode. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionLimitPredictionTier". + * via the `definition` "SendAgentMode". */ /** @experimental */ -export type SessionLimitPredictionTier = - /** Recommended starting tier. */ - | "recommended" - /** Additional headroom for longer-running sessions. */ - | "additional_headroom" - /** Generous headroom for unusually high usage. */ - | "generous_headroom" - /** Maximum available headroom tier. */ - | "maximum_headroom"; +export type SendAgentMode = + /** The agent is responding interactively to the user. */ + | "interactive" + /** The agent is preparing a plan before making changes. */ + | "plan" + /** The agent is working autonomously toward task completion. */ + | "autopilot" + /** The agent is in shell-focused UI mode. */ + | "shell"; /** - * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + * How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionLimitPredictionRequest". + * via the `definition` "SendMode". */ /** @experimental */ -export type SessionLimitPredictionRequest = - | { - [k: string]: unknown | undefined; - } - | { - /** - * Optional model identifier override. If omitted, the session's current model is used. - */ - modelId?: string; - clientType?: SessionLimitPredictionClientType; - }; +export type SendMode = + /** Append the message to the normal session queue. */ + | "enqueue" + /** Interject the message during the in-progress turn. */ + | "immediate"; /** - * Prediction result. Available results include prediction details; unavailable results include an explicit reason. + * Whether this item is a queued user message or a queued slash command / model change * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionLimitPredictionResult". + * via the `definition` "QueuePendingItemsKind". */ /** @experimental */ -export type SessionLimitPredictionResult = - | { - prediction: SessionLimitPredictionDetails; - /** - * Prediction result variant discriminator. - */ - kind: "available"; - } - | { - reason: SessionLimitPredictionUnavailableReason; - /** - * Prediction result variant discriminator. - */ - kind: "unavailable"; - }; +export type QueuePendingItemsKind = + /** A queued user message. */ + | "message" + /** A queued slash command or model-change command. */ + | "command"; /** - * Reason a prediction could not be computed. + * State of the runtime-managed remote-control singleton. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionLimitPredictionUnavailableReason". + * via the `definition` "RemoteControlStatus". */ /** @experimental */ -export type SessionLimitPredictionUnavailableReason = - /** The current model is auto and has not resolved to a concrete model yet. */ - | "auto_unresolved" - /** No model was provided and the session does not currently have a selected model. */ - | "no_model"; +export type RemoteControlStatus = + | RemoteControlStatusOff + | RemoteControlStatusConnecting + | RemoteControlStatusActive + | RemoteControlStatusError; /** - * Local or remote session metadata entry. Narrow on `isRemote` to access source-specific fields. + * Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionListEntry". + * via the `definition` "RemoteSessionMode". */ /** @experimental */ -export type SessionListEntry = LocalSessionMetadataValue | RemoteSessionMetadataValue; +export type RemoteSessionMode = + /** Disable remote session export and steering. */ + | "off" + /** Export session events to GitHub without enabling remote steering. */ + | "export" + /** Enable both remote session export and remote steering. */ + | "on"; /** - * 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). + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspaceSummary". + * via the `definition` "RemoteSessionHostStatus". */ /** @experimental */ -export type WorkspaceSummary = { - /** - * Workspace identifier (1:1 with sessionId) - */ - id: string; - /** - * Current working directory at session start - */ - cwd?: string; - /** - * Resolved git root for cwd, if any - */ - git_root?: string; - /** - * Repository identifier in 'owner/repo' or 'org/project/repo' format, if any - */ - repository?: string; - host_type?: WorkspaceSummaryHostType; - /** - * Branch checked out at session start, if any - */ - branch?: string; - /** - * Display name for the session, if set - */ - name?: string; - /** - * Whether the display name was explicitly set by the user - */ - user_named?: boolean; - /** - * ISO 8601 timestamp when the workspace was created - */ - created_at?: string; - /** - * ISO 8601 timestamp when the workspace was last updated - */ - updated_at?: string; -} | null; +export type RemoteSessionHostStatus = + /** No turn is running. */ + | "idle" + /** A turn is running. */ + | "working" + /** The session is blocked on the user: a permission prompt or an `ask_user` question. */ + | "input-needed" + /** The session ended its last turn in an error. */ + | "error"; /** - * Repository host type, if known + * Whether the remote task originated from CCA or CLI `--remote`. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspaceSummaryHostType". + * via the `definition` "RemoteSessionMetadataTaskType". */ /** @experimental */ -export type WorkspaceSummaryHostType = - /** Workspace summary repository is hosted on GitHub. */ - | "github" - /** Workspace summary repository is hosted on Azure DevOps. */ - | "ado"; +export type RemoteSessionMetadataTaskType = + /** GitHub Copilot coding agent task. */ + | "cca" + /** CLI remote task. */ + | "cli"; /** - * Initial reasoning summary mode for supported model clients. + * Provider-native structured output format. JSON Schema is forwarded without rewriting or validating the schema or the generated output. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionOpenOptionsReasoningSummary". + * via the `definition` "ResponseFormat". */ /** @experimental */ -export type SessionOpenOptionsReasoningSummary = - /** Do not request reasoning summaries from the model. */ - | "none" - /** Request a concise summary of model reasoning. */ - | "concise" - /** Request a detailed summary of model reasoning. */ - | "detailed"; +export type ResponseFormat = { + jsonSchema: JsonSchemaResponseFormat; + /** + * Output format discriminator. Currently only json_schema is supported. + */ + type: "json_schema"; +}; /** - * Controls automatic non-interactive profile loading where supported. Explicit initScripts are unaffected. + * Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ShellInitProfile". + * via the `definition` "SandboxConfigSource". */ /** @experimental */ -export type ShellInitProfile = - /** Disable automatic non-interactive profile loading. Explicit initScripts still run. */ - | "none" - /** Allow automatic non-interactive profile loading when supported. Explicit initScripts still run. */ - | "non-interactive"; +export type SandboxConfigSource = + /** The client applied the default because no sandbox preference was configured. */ + | "never_configured" + /** The user's persisted settings enabled the sandbox. */ + | "user_enabled" + /** The user's persisted settings disabled the sandbox. */ + | "user_disabled" + /** An explicit session-scoped choice selected the sandbox state, such as a command-line flag. */ + | "session_flag" + /** The user disabled the sandbox for the current session. */ + | "session_disabled" + /** The client disabled the sandbox because the host cannot enforce it. */ + | "unsupported_host" + /** A repository policy selected the sandbox state. */ + | "repository_policy"; /** - * Supported built-in shells for initialization scripts. + * 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` "ShellInitScriptShell". + * via the `definition` "SandboxHostCapabilityName". */ /** @experimental */ -export type ShellInitScriptShell = - /** Source the script in the built-in Bash shell on macOS and Linux. */ - | "bash" - /** Source the script in the built-in PowerShell shell on Windows. */ - | "powershell"; +export type SandboxHostCapabilityName = string; /** - * How MCP server environment values are interpreted. + * State of the persistent certificate authority of the sandbox credential proxy. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionOpenOptionsEnvValueMode". + * via the `definition` "SandboxProxyCaState". */ /** @experimental */ -export type SessionOpenOptionsEnvValueMode = - /** Pass MCP server environment values as literal strings. */ - | "direct" - /** Resolve MCP server environment values from host-side references. */ - | "indirect"; +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"; /** - * Allowed values for the `SessionOpenOptionsAdditionalContentExclusionPolicyScope` enumeration. + * A session-scoped sandbox transition applied while handling a slash command * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionOpenOptionsAdditionalContentExclusionPolicyScope". + * via the `definition` "SandboxSessionChange". */ /** @experimental */ -export type SessionOpenOptionsAdditionalContentExclusionPolicyScope = - /** The content exclusion policy applies to the current repository. */ - | "repo" - /** The content exclusion policy applies across all repositories. */ - | "all"; +export type SandboxSessionChange = + /** The sandbox is off for the rest of this session; nothing was persisted and a new session starts from managed policy. */ + | "disabled" + /** A previous session-scoped opt-out was cleared and the sandbox is enforced again. */ + | "restored"; /** - * Open a session by creating, resuming, attaching, connecting to a remote, or handing off. + * Current authentication information, or null when no authentication is active. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionOpenParams". + * via the `definition` "SessionAuthInfoResult". */ /** @experimental */ -export type SessionOpenParams = - | SessionsOpenCreate - | SessionsOpenResume - | SessionsOpenResumeLast - | SessionsOpenAttach - | SessionsOpenRemote - | SessionsOpenCloud - | SessionsOpenHandoff; +export type SessionAuthInfoResult = AuthIdentity | null; /** - * Task type determines the handoff strategy (CCA fetches events; CLI prepares a transient session). + * Session capability enabled for this session * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionsOpenHandoffTaskType". + * via the `definition` "SessionCapability". */ /** @experimental */ -export type SessionsOpenHandoffTaskType = - /** GitHub Copilot coding agent task. */ - | "cca" - /** CLI remote task. */ - | "cli"; +export type SessionCapability = + /** TUI-specific prompt hints such as keyboard shortcuts. */ + | "tui-hints" + /** Plan-mode handling and instructions. */ + | "plan-mode" + /** Memory tool and memories prompt section. */ + | "memory" + /** Copilot CLI documentation tool and prompt section. */ + | "cli-documentation" + /** Interactive ask_user tool support. */ + | "ask-user" + /** Interactive CLI identity and behavior. */ + | "interactive-mode" + /** Automatic hidden system notifications. */ + | "system-notifications" + /** SDK elicitation support. */ + | "elicitation" + /** Cross-session history tools and session-store SQL prompt/tool metadata. */ + | "session-store" + /** MCP Apps UI passthrough. */ + | "mcp-apps" + /** Host-provided canvas rendering support. */ + | "canvas-renderer"; /** - * Outcome of the open request. + * Error classification * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionsOpenStatus". + * via the `definition` "SessionFsErrorCode". */ /** @experimental */ -export type SessionsOpenStatus = - /** A new session was created. */ - | "created" - /** An existing session was loaded or reattached. */ - | "resumed" - /** No matching persisted session was found. */ - | "not_found" - /** Connected to an existing remote session. */ - | "connected" - /** Remote session was handed off to a new local session. */ - | "handed_off"; +export type SessionFsErrorCode = + /** The requested path does not exist. */ + | "ENOENT" + /** The filesystem operation failed for an unspecified reason. */ + | "UNKNOWN"; /** - * Handoff step. + * Entry type * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionsOpenProgressStep". + * via the `definition` "SessionFsReaddirWithTypesEntryType". */ /** @experimental */ -export type SessionsOpenProgressStep = - /** Loading the source session's events from the remote service. */ - | "load-session" - /** Validating that the local repository matches the remote session's repository. */ - | "validate-repo" - /** Checking the local working tree for uncommitted changes that would block the handoff. */ - | "check-changes" - /** Checking out the branch associated with the remote session in the local working tree. */ - | "checkout-branch" - /** Creating the new local session and seeding it with the source session's events. */ - | "create-session" - /** Persisting the newly-created local session to disk. */ - | "save-session"; +export type SessionFsReaddirWithTypesEntryType = + /** The entry is a file. */ + | "file" + /** The entry is a directory. */ + | "directory"; /** - * Step status. + * Path conventions used by this filesystem * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionsOpenProgressStatus". + * via the `definition` "SessionFsSetProviderConventions". */ /** @experimental */ -export type SessionsOpenProgressStatus = - /** The step has started and has not yet finished. */ - | "in-progress" - /** The step has completed successfully. */ - | "complete"; +export type SessionFsSetProviderConventions = + /** Paths use Windows path conventions. */ + | "windows" + /** Paths use POSIX path conventions. */ + | "posix"; /** - * Client metadata outcome for one requested local session. + * How to execute the query: 'exec' for DDL/multi-statement (no results), 'query' for SELECT (returns rows), 'run' for INSERT/UPDATE/DELETE (returns rowsAffected) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionsClientMetadataEntry". + * via the `definition` "SessionFsSqliteQueryType". */ /** @experimental */ -export type SessionsClientMetadataEntry = - | { - /** - * Requested session ID. - */ - sessionId: string; - metadata: ClientMetadata; - /** - * Client metadata outcome discriminator. - */ - status: "ok"; - } - | { - /** - * Requested session ID. - */ - sessionId: string; - /** - * Client metadata outcome discriminator. - */ - status: "notFound"; - } - | { - /** - * Requested session ID. - */ - sessionId: string; - /** - * Client metadata outcome discriminator. - */ - status: "corrupt"; - } - | { - /** - * Requested session ID. - */ - sessionId: string; - /** - * Client metadata outcome discriminator. - */ - status: "unsupportedVersion"; - } - | { - /** - * Requested session ID. - */ - sessionId: string; - /** - * Filesystem or provider error code. Clients should not assume every provider uses operating-system error codes. - */ - code: string; - /** - * Human-readable diagnostic message. Not stable for programmatic matching. - */ - message: string; - /** - * Client metadata outcome discriminator. - */ - status: "unavailable"; - }; +export type SessionFsSqliteQueryType = + /** Execute DDL or multi-statement SQL without returning rows. */ + | "exec" + /** Execute a SELECT-style query and return rows. */ + | "query" + /** Execute INSERT, UPDATE, or DELETE SQL and return affected-row metadata. */ + | "run"; /** - * Authentication credentials accepted by session.gitHubAuth.setCredentials. Session-owned token-provider identities cannot be installed through this method. + * SQLite transaction failure classification. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SettableAuthInfo". + * via the `definition` "SessionFsSqliteTransactionErrorClass". */ /** @experimental */ -export type SettableAuthInfo = - | HMACAuthInfo - | EnvAuthInfo - | SettableTokenAuthInfo - | CopilotApiTokenAuthInfo - | UserAuthInfo - | GhCliAuthInfo - | ApiKeyAuthInfo; +export type SessionFsSqliteTransactionErrorClass = + /** SQLite reported BUSY or LOCKED before commit; the transaction was rolled back and may be retried. */ + | "busyOrLocked" + /** The statement, database, or provider failed definitively and must not be retried automatically. */ + | "fatal" + /** The transport failed after the provider may have committed; retrying could duplicate effects. */ + | "postCommitAmbiguous"; /** - * Rust-owned settings predicates exposed across the SDK boundary. Raw feature-flag names are intentionally not part of the contract. + * Source descriptor for direct repo installs (when marketplace is empty) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionSettingsPredicateName". + * via the `definition` "SessionInstalledPluginSource". */ /** @experimental */ -export type SessionSettingsPredicateName = - /** Whether the security-tools feature flag enables security tool wiring. */ - | "securityToolsEnabled" - /** Whether third-party security tools should receive the security prompt. */ - | "thirdPartySecurityPromptEnabled" - /** Whether validation may run in parallel. */ - | "parallelValidationEnabled" - /** Whether runtime timing telemetry is enabled. */ - | "runtimeTimingTelemetryEnabled" - /** Whether the co-author hook is enabled. */ - | "coAuthorHookEnabled" - /** Whether Chronicle integration is enabled. */ - | "chronicleEnabled" - /** Whether content-exclusion policy may self-fetch data. */ - | "contentExclusionSelfFetchEnabled" - /** Whether Claude Opus token-limit caps should be applied. */ - | "capClaudeOpusTokenLimitsEnabled" - /** Whether code-review behavior is enabled. */ - | "codeReviewFeatureEnabled" - /** Whether CCA should use the TypeScript autofind behavior. */ - | "ccaUseTsAutofindEnabled" - /** Whether the dependency checker is enabled. */ - | "dependencyCheckerEnabled" - /** Whether the Dependabot checker is enabled. */ - | "dependabotCheckerEnabled" - /** Whether the CodeQL checker is enabled. */ - | "codeqlCheckerEnabled" - /** Whether trivial-change handling is enabled. */ - | "trivialChangeEnabled" - /** Whether trivial-change skip behavior is enabled. */ - | "trivialChangeSkipEnabled" - /** Whether trivial-change handling is enabled for code review. */ - | "trivialChangeEnabledForCodeReview" - /** Whether trivial-change skip behavior is enabled for code review. */ - | "trivialChangeSkipEnabledForCodeReview" - /** Whether trivial-change handling is enabled for a specific tool. */ - | "trivialChangeEnabledForTool" - /** Whether trivial-change skip behavior is enabled for a specific tool. */ - | "trivialChangeSkipEnabledForTool"; +export type SessionInstalledPluginSource = + | string + | SessionInstalledPluginSourceGitHub + | SessionInstalledPluginSourceUrl + | SessionInstalledPluginSourceLocal; /** - * Ordered client metadata outcomes for the requested local sessions. + * Client population used for the prediction baseline. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionsGetClientMetadataResult". + * via the `definition` "SessionLimitPredictionClientType". */ /** @experimental */ -export type SessionsGetClientMetadataResult = SessionsClientMetadataEntry[]; +export type SessionLimitPredictionClientType = + /** Interactive CLI sessions where a user can accept, edit, or top up the limit. */ + | "cli-interactive" + /** Prompt/non-interactive CLI sessions where the initial limit must cover more of the run. */ + | "cli-prompt"; /** - * Which session sources to include. Defaults to `local` for backward compatibility. + * Baseline fallback level used to create the prediction. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionSource". + * via the `definition` "SessionLimitPredictionSource". */ /** @experimental */ -export type SessionSource = - /** Return only local sessions. */ - | "local" - /** Return only remote sessions. */ - | "remote" - /** Return both local and remote sessions. */ - | "all"; +export type SessionLimitPredictionSource = + /** The prediction used the exact resolved model's baseline cell. */ + | "model" + /** The exact model was unavailable, so the prediction used the model family's baseline cell. */ + | "family" + /** No model or family cell was available, so the prediction used the global client-type baseline cell. */ + | "global"; /** - * Sharing status for a synced session. "repo" makes the session visible to anyone with read access to the repository; "unshared" restricts it to the creator and collaborators. + * Semantic usage tier used for a recommended cap or additional headroom. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionVisibilityStatus". + * via the `definition` "SessionLimitPredictionTier". */ /** @experimental */ -export type SessionVisibilityStatus = - /** The session is visible to repository readers. */ - | "repo" - /** The session is restricted to its creator and collaborators. */ - | "unshared"; +export type SessionLimitPredictionTier = + /** Recommended starting tier. */ + | "recommended" + /** Additional headroom for longer-running sessions. */ + | "additional_headroom" + /** Generous headroom for unusually high usage. */ + | "generous_headroom" + /** Maximum available headroom tier. */ + | "maximum_headroom"; /** - * Signal to send (default: SIGTERM) + * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ShellKillSignal". + * via the `definition` "SessionLimitPredictionRequest". */ /** @experimental */ -export type ShellKillSignal = - /** Request graceful process termination. */ - | "SIGTERM" - /** Forcefully terminate the process. */ - | "SIGKILL" - /** Send an interrupt signal to the process. */ - | "SIGINT"; +export type SessionLimitPredictionRequest = + | { + [k: string]: unknown | undefined; + } + | { + /** + * Optional model identifier override. If omitted, the session's current model is used. + */ + modelId?: string; + clientType?: SessionLimitPredictionClientType; + }; /** - * Which tier this directory belongs to + * Prediction result. Available results include prediction details; unavailable results include an explicit reason. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SkillDiscoveryScope". + * via the `definition` "SessionLimitPredictionResult". */ /** @experimental */ -export type SkillDiscoveryScope = - /** A project's repository skill directory. */ - | "project" - /** The user's personal Copilot skill directory. */ - | "personal-copilot" - /** The user's personal agents skill directory. */ - | "personal-agents" - /** A configured custom skill directory. */ - | "custom"; +export type SessionLimitPredictionResult = + | { + prediction: SessionLimitPredictionDetails; + /** + * Prediction result variant discriminator. + */ + kind: "available"; + } + | { + reason: SessionLimitPredictionUnavailableReason; + /** + * Prediction result variant discriminator. + */ + kind: "unavailable"; + }; /** - * Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). + * Reason a prediction could not be computed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SlashCommandInvocationResult". + * via the `definition` "SessionLimitPredictionUnavailableReason". */ /** @experimental */ -export type SlashCommandInvocationResult = - | SlashCommandTextResult - | SlashCommandAgentPromptResult - | SlashCommandCompletedResult - | SlashCommandSelectSubcommandResult - | SlashCommandAddTimelineEntryResult - | SlashCommandShowDialogResult - | SlashCommandSetModelResult - | SlashCommandSetPlanModelResult; +export type SessionLimitPredictionUnavailableReason = + /** The current model is auto and has not resolved to a concrete model yet. */ + | "auto_unresolved" + /** No model was provided and the session does not currently have a selected model. */ + | "no_model"; /** - * Subagent settings to apply, or null to clear the live session override + * Local or remote session metadata entry. Narrow on `isRemote` to access source-specific fields. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SubagentSettings". + * via the `definition` "SessionListEntry". */ /** @experimental */ -export type SubagentSettings = { - /** - * Per-agent settings keyed by subagent agent_type - */ - agents?: { - [k: string]: SubagentSettingsEntry | undefined; - }; - /** - * Names of subagents the user has turned off; they cannot be dispatched +export type SessionListEntry = LocalSessionMetadataValue | RemoteSessionMetadataValue; +/** + * 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). + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspaceSummary". + */ +/** @experimental */ +export type WorkspaceSummary = { + /** + * Workspace identifier (1:1 with sessionId) */ - disabledSubagents?: string[]; + id: string; /** - * Maximum number of subagents that can run concurrently; applies to usage-based billing users only + * Current working directory at session start */ - maxConcurrency?: number; + cwd?: string; /** - * Maximum subagent nesting depth; applies to usage-based billing users only + * Resolved git root for cwd, if any */ - maxDepth?: number; + git_root?: string; + /** + * Repository identifier in 'owner/repo' or 'org/project/repo' format, if any + */ + repository?: string; + host_type?: WorkspaceSummaryHostType; + /** + * Branch checked out at session start, if any + */ + branch?: string; + /** + * Display name for the session, if set + */ + name?: string; + /** + * Whether the display name was explicitly set by the user + */ + user_named?: boolean; + /** + * ISO 8601 timestamp when the workspace was created + */ + created_at?: string; + /** + * ISO 8601 timestamp when the workspace was last updated + */ + updated_at?: string; } | null; /** - * Context tier override for matching subagents + * Repository host type, if known * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SubagentSettingsEntryContextTier". + * via the `definition` "WorkspaceSummaryHostType". */ /** @experimental */ -export type SubagentSettingsEntryContextTier = - /** Inherit the parent session's effective context tier at dispatch time. */ - | "inherit" - /** Use the model's default context window. */ - | "default" - /** Pin the subagent to the long-context tier when supported. */ - | "long_context"; +export type WorkspaceSummaryHostType = + /** Workspace summary repository is hosted on GitHub. */ + | "github" + /** Workspace summary repository is hosted on Azure DevOps. */ + | "ado"; /** - * Current lifecycle status of the task + * Initial reasoning summary mode for supported model clients. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskStatus". + * via the `definition` "SessionOpenOptionsReasoningSummary". */ /** @experimental */ -export type TaskStatus = - /** The task is actively executing. */ - | "running" - /** The task is waiting for additional input. */ - | "idle" - /** The task finished successfully. */ - | "completed" - /** The task finished with an error. */ - | "failed" - /** The task was cancelled before completion. */ - | "cancelled"; +export type SessionOpenOptionsReasoningSummary = + /** Do not request reasoning summaries from the model. */ + | "none" + /** Request a concise summary of model reasoning. */ + | "concise" + /** Request a detailed summary of model reasoning. */ + | "detailed"; /** - * Whether task execution is synchronously awaited or managed in the background + * Controls automatic non-interactive profile loading where supported. Explicit initScripts are unaffected. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskExecutionMode". + * via the `definition` "ShellInitProfile". */ /** @experimental */ -export type TaskExecutionMode = - /** The task was started with synchronous waiting. */ - | "sync" - /** The task is managed in the background. */ - | "background"; +export type ShellInitProfile = + /** Disable automatic non-interactive profile loading. Explicit initScripts still run. */ + | "none" + /** Allow automatic non-interactive profile loading when supported. Explicit initScripts still run. */ + | "non-interactive"; /** - * Active status a client owner may publish with a progress update. + * Supported built-in shells for initialization scripts. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskClientActiveStatus". + * via the `definition` "ShellInitScriptShell". */ /** @experimental */ -export type TaskClientActiveStatus = - /** The external owner is actively working. */ - | "running" - /** The external owner is connected but waiting. */ - | "idle"; +export type ShellInitScriptShell = + /** Source the script in the built-in Bash shell on macOS and Linux. */ + | "bash" + /** Source the script in the built-in PowerShell shell on Windows. */ + | "powershell"; /** - * Client-owned tasks always execute outside the runtime in background mode. + * How MCP server environment values are interpreted. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskClientExecutionMode". + * via the `definition` "SessionOpenOptionsEnvValueMode". */ /** @experimental */ -export type TaskClientExecutionMode = "background"; +export type SessionOpenOptionsEnvValueMode = + /** Pass MCP server environment values as literal strings. */ + | "direct" + /** Resolve MCP server environment values from host-side references. */ + | "indirect"; /** - * Discriminator for a client-owned task. + * Allowed values for the `SessionOpenOptionsAdditionalContentExclusionPolicyScope` enumeration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskClientType". + * via the `definition` "SessionOpenOptionsAdditionalContentExclusionPolicyScope". */ /** @experimental */ -export type TaskClientType = "client"; +export type SessionOpenOptionsAdditionalContentExclusionPolicyScope = + /** The content exclusion policy applies to the current repository. */ + | "repo" + /** The content exclusion policy applies across all repositories. */ + | "all"; /** - * Lifecycle status of a client-owned task. + * Open a session by creating, resuming, attaching, connecting to a remote, or handing off. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskClientStatus". + * via the `definition` "SessionOpenParams". */ /** @experimental */ -export type TaskClientStatus = - /** The external owner is actively working. */ - | "running" - /** The external owner is connected but waiting. */ - | "idle" - /** The owner reported successful completion. */ - | "completed" - /** The owner reported failure. */ - | "failed" - /** The owner reported or confirmed cancellation. */ - | "cancelled" - /** The bound owner join disappeared; external executor state is unknown. */ - | "orphaned"; +export type SessionOpenParams = + | SessionsOpenCreate + | SessionsOpenResume + | SessionsOpenResumeLast + | SessionsOpenAttach + | SessionsOpenRemote + | SessionsOpenCloud + | SessionsOpenHandoff; /** - * Connection class owning a client task. + * Task type determines the handoff strategy (CCA fetches events; CLI prepares a transient session). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskClientOwnerKind". + * via the `definition` "SessionsOpenHandoffTaskType". */ /** @experimental */ -export type TaskClientOwnerKind = - /** A discovered extension connection owns the task. */ - | "extension" - /** A generic SDK connection owns the task. */ - | "sdk"; +export type SessionsOpenHandoffTaskType = + /** GitHub Copilot coding agent task. */ + | "cca" + /** CLI remote task. */ + | "cli"; /** - * Presence of the task's bound join. + * Outcome of the open request. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskClientOwnerPresence". + * via the `definition` "SessionsOpenStatus". */ /** @experimental */ -export type TaskClientOwnerPresence = - /** The bound session join is connected. */ +export type SessionsOpenStatus = + /** A new session was created. */ + | "created" + /** An existing session was loaded or reattached. */ + | "resumed" + /** No matching persisted session was found. */ + | "not_found" + /** Connected to an existing remote session. */ | "connected" - /** The bound session join is disconnected. */ - | "disconnected"; + /** Remote session was handed off to a new local session. */ + | "handed_off"; /** - * Progress or terminal update for a client-owned task. + * Handoff step. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskClientUpdate". + * via the `definition` "SessionsOpenProgressStep". */ /** @experimental */ -export type TaskClientUpdate = +export type SessionsOpenProgressStep = + /** Loading the source session's events from the remote service. */ + | "load-session" + /** Validating that the local repository matches the remote session's repository. */ + | "validate-repo" + /** Checking the local working tree for uncommitted changes that would block the handoff. */ + | "check-changes" + /** Checking out the branch associated with the remote session in the local working tree. */ + | "checkout-branch" + /** Creating the new local session and seeding it with the source session's events. */ + | "create-session" + /** Persisting the newly-created local session to disk. */ + | "save-session"; +/** + * Step status. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsOpenProgressStatus". + */ +/** @experimental */ +export type SessionsOpenProgressStatus = + /** The step has started and has not yet finished. */ + | "in-progress" + /** The step has completed successfully. */ + | "complete"; +/** + * Client metadata outcome for one requested local session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsClientMetadataEntry". + */ +/** @experimental */ +export type SessionsClientMetadataEntry = | { - status?: TaskClientActiveStatus; /** - * Optional progress message appended to recent activity when nonempty + * Requested session ID. */ - message?: string; + sessionId: string; + metadata: ClientMetadata; /** - * Optional progress phase; null clears the current phase + * Client metadata outcome discriminator. */ - phase?: string | null; + status: "ok"; + } + | { /** - * Optional completion percentage; null clears the current percentage + * Requested session ID. */ - percentage?: number | null; + sessionId: string; /** - * Client task update variant discriminator. + * Client metadata outcome discriminator. */ - kind: "progress"; + status: "notFound"; } | { /** - * Optional final progress message + * Requested session ID. */ - message?: string; + sessionId: string; /** - * Optional opaque successful terminal result + * Client metadata outcome discriminator. */ - result?: JsonValue; - /** - * Client task update variant discriminator. - */ - kind: "completed"; + status: "corrupt"; } | { /** - * Optional final progress message - */ - message?: string; - /** - * Human-readable terminal failure message - */ - error: string; - /** - * Optional owner-supplied terminal failure code + * Requested session ID. */ - code?: string; + sessionId: string; /** - * Client task update variant discriminator. + * Client metadata outcome discriminator. */ - kind: "failed"; + status: "unsupportedVersion"; } | { /** - * Optional final progress message + * Requested session ID. */ - message?: string; + sessionId: string; /** - * Optional human-readable cancellation reason + * Filesystem or provider error code. Clients should not assume every provider uses operating-system error codes. */ - reason?: string; + code: string; /** - * Client task update variant discriminator. + * Human-readable diagnostic message. Not stable for programmatic matching. */ - kind: "cancelled"; + message: string; + /** + * Client metadata outcome discriminator. + */ + status: "unavailable"; }; /** - * Tracked task union returned by task APIs, containing an agent, client, or shell task. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskInfo". - */ -/** @experimental */ -export type TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo; -/** - * Whether the shell runs inside a managed PTY session or as an independent background process - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskShellInfoAttachmentMode". - */ -/** @experimental */ -export type TaskShellInfoAttachmentMode = - /** The shell runs in a managed PTY session. */ - | "attached" - /** The shell runs as an independent background process. */ - | "detached"; -/** - * Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. + * Authentication credentials accepted by session.gitHubAuth.setCredentials. Session-owned token-provider identities cannot be installed through this method. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TaskProgress". + * via the `definition` "SettableAuthInfo". */ /** @experimental */ -export type TaskProgress = TaskAgentProgress | TaskClientProgress | TaskShellProgress | null; +export type SettableAuthInfo = + | HMACAuthInfo + | EnvAuthInfo + | SettableTokenAuthInfo + | CopilotApiTokenAuthInfo + | UserAuthInfo + | AccountAuthInfo + | GhCliAuthInfo + | ApiKeyAuthInfo; /** - * Canonical result returned by a session tool. + * Rust-owned settings predicates exposed across the SDK boundary. Raw feature-flag names are intentionally not part of the contract. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ToolResult". + * via the `definition` "SessionSettingsPredicateName". */ /** @experimental */ -export type ToolResult = string | ToolResultExpanded; +export type SessionSettingsPredicateName = + /** Whether the security-tools feature flag enables security tool wiring. */ + | "securityToolsEnabled" + /** Whether third-party security tools should receive the security prompt. */ + | "thirdPartySecurityPromptEnabled" + /** Whether validation may run in parallel. */ + | "parallelValidationEnabled" + /** Whether runtime timing telemetry is enabled. */ + | "runtimeTimingTelemetryEnabled" + /** Whether the co-author hook is enabled. */ + | "coAuthorHookEnabled" + /** Whether Chronicle integration is enabled. */ + | "chronicleEnabled" + /** Whether content-exclusion policy may self-fetch data. */ + | "contentExclusionSelfFetchEnabled" + /** Whether Claude Opus token-limit caps should be applied. */ + | "capClaudeOpusTokenLimitsEnabled" + /** Whether code-review behavior is enabled. */ + | "codeReviewFeatureEnabled" + /** Whether CCA should use the TypeScript autofind behavior. */ + | "ccaUseTsAutofindEnabled" + /** Whether the dependency checker is enabled. */ + | "dependencyCheckerEnabled" + /** Whether the Dependabot checker is enabled. */ + | "dependabotCheckerEnabled" + /** Whether the CodeQL checker is enabled. */ + | "codeqlCheckerEnabled" + /** Whether trivial-change handling is enabled. */ + | "trivialChangeEnabled" + /** Whether trivial-change skip behavior is enabled. */ + | "trivialChangeSkipEnabled" + /** Whether trivial-change handling is enabled for code review. */ + | "trivialChangeEnabledForCodeReview" + /** Whether trivial-change skip behavior is enabled for code review. */ + | "trivialChangeSkipEnabledForCodeReview" + /** Whether trivial-change handling is enabled for a specific tool. */ + | "trivialChangeEnabledForTool" + /** Whether trivial-change skip behavior is enabled for a specific tool. */ + | "trivialChangeSkipEnabledForTool"; /** - * Execution outcome classification. + * Ordered client metadata outcomes for the requested local sessions. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ToolResultType". + * via the `definition` "SessionsGetClientMetadataResult". */ /** @experimental */ -export type ToolResultType = - /** The tool completed successfully. */ - | "success" - /** The tool failed. */ - | "failure" - /** The tool exceeded its execution timeout. */ - | "timeout" - /** The tool request was rejected before execution. */ - | "rejected" - /** Permission policy denied the tool request. */ - | "denied"; +export type SessionsGetClientMetadataResult = SessionsClientMetadataEntry[]; /** - * User's choice for auto-mode switching: yes (allow this turn), yes_always (allow + persist as setting), or no (decline). + * Which session sources to include. Defaults to `local` for backward compatibility. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UIAutoModeSwitchResponse". + * via the `definition` "SessionSource". */ /** @experimental */ -export type UIAutoModeSwitchResponse = - /** Allow the automatic mode switch for this turn. */ - | "yes" - /** Allow this mode switch and persist the preference. */ - | "yes_always" - /** Decline the automatic mode switch. */ - | "no"; +export type SessionSource = + /** Return only local sessions. */ + | "local" + /** Return only remote sessions. */ + | "remote" + /** Return both local and remote sessions. */ + | "all"; /** - * Submitted UI elicitation field value: string, number, boolean, or an array of strings. + * Sharing status for a synced session. "repo" makes the session visible to anyone with read access to the repository; "unshared" restricts it to the creator and collaborators. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UIElicitationFieldValue". + * via the `definition` "SessionVisibilityStatus". */ /** @experimental */ -export type UIElicitationFieldValue = string | number | boolean | string[]; +export type SessionVisibilityStatus = + /** The session is visible to repository readers. */ + | "repo" + /** The session is restricted to its creator and collaborators. */ + | "unshared"; /** - * Definition for a single elicitation form field. + * Signal to send (default: SIGTERM) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UIElicitationSchemaProperty". + * via the `definition` "ShellKillSignal". */ /** @experimental */ -export type UIElicitationSchemaProperty = - | ( - | UIElicitationStringEnumField - | UIElicitationStringOneOfField - | UIElicitationArrayEnumField - | UIElicitationArrayAnyOfField - | UIElicitationSchemaPropertyBoolean - | UIElicitationSchemaPropertyString - | UIElicitationSchemaPropertyNumber - ) - | undefined; +export type ShellKillSignal = + /** Request graceful process termination. */ + | "SIGTERM" + /** Forcefully terminate the process. */ + | "SIGKILL" + /** Send an interrupt signal to the process. */ + | "SIGINT"; /** - * Optional format hint that constrains the accepted input. + * Which tier this directory belongs to * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UIElicitationSchemaPropertyStringFormat". + * via the `definition` "SkillDiscoveryScope". */ /** @experimental */ -export type UIElicitationSchemaPropertyStringFormat = - /** Email address string format. */ - | "email" - /** URI string format. */ - | "uri" - /** Calendar date string format. */ - | "date" - /** Date-time string format. */ - | "date-time"; +export type SkillDiscoveryScope = + /** A project's repository skill directory. */ + | "project" + /** The user's personal Copilot skill directory. */ + | "personal-copilot" + /** The user's personal agents skill directory. */ + | "personal-agents" + /** A configured custom skill directory. */ + | "custom"; /** - * Numeric type accepted by the field. + * Bounded refusal categories for verified Skill installation management. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UIElicitationSchemaPropertyNumberType". + * via the `definition` "SkillInstallationFailureReason". */ /** @experimental */ -export type UIElicitationSchemaPropertyNumberType = - /** Any JSON number. */ - | "number" - /** Integer JSON number. */ - | "integer"; +export type SkillInstallationFailureReason = + /** The selected session does not have the Agent Finder Skill installation feature flag enabled for acquisition. */ + | "feature-disabled" + /** The request is unsupported or malformed. */ + | "invalid-request" + /** The bounded operation limit was reached. */ + | "operation-limit" + /** The original operation was cancelled. */ + | "cancelled" + /** The original host cannot receive human confirmation. */ + | "confirmation-unavailable" + /** The confirmation response is malformed or mismatched. */ + | "confirmation-invalid" + /** Existing authenticated session and host authority is unavailable. */ + | "policy-context-unavailable" + /** The original authority or policy changed. */ + | "policy-changed" + /** No matching owned resource or original operation exists. */ + | "resource-not-found" + /** The original plan deadline elapsed. */ + | "plan-expired" + /** The one-use plan was already consumed. */ + | "plan-replayed" + /** The handle belongs to a different runtime, session or connection. */ + | "foreign-runtime" + /** The handle is not the expected Skill handle kind. */ + | "wrong-kind" + /** The handle was minted for a different search result or authority. */ + | "search-mismatch" + /** The handle or operation expired. */ + | "expired" + /** The candidate handle is not a verified installable Skill candidate. */ + | "invalid-candidate" + /** The verified Skill descriptor could not be retrieved safely. */ + | "descriptor-unavailable" + /** The verified Skill descriptor failed validation. */ + | "descriptor-invalid" + /** The Skill entrypoint could not be retrieved or verified. */ + | "entrypoint-unavailable" + /** The Skill entrypoint is not a valid Skill. */ + | "invalid-skill" + /** The reviewed Skill payload could not be acquired. */ + | "payload-unavailable" + /** The acquired payload no longer matches the reviewed descriptor. */ + | "payload-mismatch" + /** The retained Skill source changed after planning. */ + | "source-changed" + /** The selected runtime cannot inspect or control the requested lifecycle operation. */ + | "lifecycle-unavailable" + /** Durable Skill installation evidence requires recovery before mutation. */ + | "recovery-required" + /** The Skill entrypoint exceeds the bounded complete review size. */ + | "review-too-large" + /** Skill installation storage or admission is busy. */ + | "busy" + /** An owned Skill with the same identity or target already exists. */ + | "already-installed" + /** Installed Skill files no longer match ownership evidence. */ + | "configuration-modified" + /** A storage operation failed; inspect durable state before retrying. */ + | "write-failed"; +/** + * Management outcome for verified Skill inventory, planning and removal. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillInstallationManagementOutcome". + */ +/** @experimental */ +export type SkillInstallationManagementOutcome = + | { + /** + * Skill installation management outcome discriminator. + */ + kind: "recovery-required"; + } + | { + plan: SkillInstallPlan; + /** + * Skill installation management outcome discriminator. + */ + kind: "install-planned"; + } + | { + /** + * Owned Skill installation summaries. + */ + installations: SkillInstallationSummary[]; + /** + * Skill installation management outcome discriminator. + */ + kind: "listed"; + } + | { + /** + * Owned Skill installation summaries after recovery. + */ + installations: SkillInstallationSummary[]; + /** + * Skill installation management outcome discriminator. + */ + kind: "recovered"; + } + | { + reason: SkillInstallationFailureReason; + /** + * Original operation identity. + */ + operation_id: string; + /** + * Skill installation management outcome discriminator. + */ + kind: "rolled-back"; + } + | { + plan: SkillUninstallPlan; + /** + * Skill installation management outcome discriminator. + */ + kind: "uninstall-planned"; + } + | { + operation: SkillInstallationOperationStatus; + /** + * Skill installation management outcome discriminator. + */ + kind: "operation"; + } + | { + installation: SkillInstallationSummary; + /** + * Safe reload or reconciliation diagnostics. + */ + diagnostics: string[]; + /** + * Skill installation management outcome discriminator. + */ + kind: "enabled-changed"; + } + | { + reason: SkillInstallationFailureReason; + /** + * Skill installation management outcome discriminator. + */ + kind: "refused"; + }; /** - * The user's response: accept (submitted), decline (rejected), or cancel (dismissed) + * Status snapshot from the original connection, independent of new-work account availability. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UIElicitationResponseAction". + * via the `definition` "SkillInstallationOperationStatus". */ /** @experimental */ -export type UIElicitationResponseAction = - /** The user submitted the requested form values. */ - | "accept" - /** The user explicitly declined to provide the requested input. */ - | "decline" - /** The user dismissed the elicitation request. */ - | "cancel"; -/** - * The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UIExitPlanModeAction". - */ -/** @experimental */ -export type UIExitPlanModeAction = - /** Exit plan mode without starting implementation. */ - | "exit_only" - /** Exit plan mode and continue interactively. */ - | "interactive" - /** Exit plan mode and continue in autopilot mode. */ - | "autopilot" - /** Exit plan mode and continue in autopilot mode with parallel subagent execution. */ - | "autopilot_fleet"; -/** - * User action selected for an exhausted session limit. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UISessionLimitsExhaustedResponseAction". - */ -/** @experimental */ -export type UISessionLimitsExhaustedResponseAction = - /** Increase the current max by an exact AI Credits amount. */ - | "add" - /** Set a new absolute max AI Credits value. */ - | "set" - /** Remove the current session limit. */ - | "unset" - /** Leave the limit unchanged and cancel the blocked model request. */ - | "cancel"; -/** - * Execution-critical workflow storage operation. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowDurableOperation". - */ -/** @experimental */ -export type WorkflowDurableOperation = - /** Creating the durable run and declared phases. */ - | "createRun" - /** Persisting the transition to running. */ - | "markRunStarted" - /** Persisting the terminal run envelope. */ - | "finishRun" - /** Persisting subagent admission accounting. */ - | "reserveAgent" - /** Rolling back an uncommitted subagent admission. */ - | "releaseAgent" - /** Persisting an idempotent model-usage charge. */ - | "chargeCredit" - /** Persisting active execution time. */ - | "addElapsed" - /** Reading the authoritative AI-credit total. */ - | "reconcileCreditTotal" - /** Reading a journal entry without treating storage failure as a cache miss. */ - | "journalGet" - /** Persisting a journal entry before reporting success. */ - | "journalPut" - /** Renewing the durable owner lease that proves this process still owns the run. */ - | "refreshLease"; -/** - * Current or terminal state of a workflow run. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowRunStatus". - */ -/** @experimental */ -export type WorkflowRunStatus = - /** The run was minted and is awaiting approval. */ - | "pending" - /** The run is executing. */ - | "running" - /** The run completed successfully. */ - | "completed" - /** The run was interrupted while resource budget remained. */ - | "halted" - /** The current attempt stopped intentionally and the run may be resumed. */ - | "paused" - /** The run was cancelled before completion. */ - | "cancelled" - /** The workflow body failed or reached a cumulative resource ceiling. */ - | "error"; +export type SkillInstallationOperationStatus = + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation has been requested. + */ + cancellationRequested: boolean; + phase: "preparing"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation has been requested. + */ + cancellationRequested: boolean; + phase: "prepared"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation has been requested. + */ + cancellationRequested: boolean; + phase: "awaiting-confirmation"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation has been requested. + */ + cancellationRequested: boolean; + phase: "revalidating"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation has been requested; already-started effects require recovery. + */ + cancellationRequested: boolean; + phase: "applying"; + } + | { + /** + * Original runtime-issued operation identity. + */ + operationId: string; + /** + * Whether cancellation was requested before the terminal result. + */ + cancellationRequested: boolean; + outcome: SkillInstallationOutcome; + phase: "completed"; + }; /** - * Machine-readable workflow run failure. + * Terminal verified Skill mutation result. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowRunFailure". + * via the `definition` "SkillInstallationOutcome". */ /** @experimental */ -export type WorkflowRunFailure = +export type SkillInstallationOutcome = | { - kind: WorkflowRunFailureKind; + installation: SkillInstallationSummary; /** - * Approved effective ceiling that was reached. + * Terminal Skill installation outcome discriminator. */ - value: number; + kind: "installed"; + } + | { /** - * Suggested larger ceiling when the runtime can derive one safely. + * Removed installation identity. */ - suggestedValue?: number; + installationId: string; /** - * Workflow run identifier. + * Original removal operation identity. */ - runId: string; + operationId: string; /** - * Workflow failure variant discriminator. + * Terminal Skill installation outcome discriminator. */ - type: "workflow_limit_reached"; + kind: "uninstalled"; } | { + reason: SkillInstallationFailureReason; /** - * Workflow run identifier whose changed limits were declined. - */ - runId: string; - /** - * Human-readable reason the resume did not proceed. + * Original operation identity. */ - reason: string; + operationId: string; /** - * Workflow failure variant discriminator. + * Terminal Skill installation outcome discriminator. */ - type: "workflow_resume_declined"; + kind: "rolled-back"; } | { /** - * Stable failure code. - */ - code: string; - operation: WorkflowDurableOperation; - /** - * Workflow run identifier. + * Operation whose durable result must be recovered and inspected. */ - runId: string; + operationId: string; /** - * Workflow failure variant discriminator. + * Terminal Skill installation outcome discriminator. */ - type: "workflow_durable_failure"; + kind: "recovery-required"; } | { /** - * Workflow run identifier. + * Original operation identity. */ - runId: string; + operationId: string; /** - * Confirmed usage in nano-AIU, representing the floor of what the run spent. + * Terminal Skill installation outcome discriminator. */ - drainedNanoAiu: number; + kind: "declined"; + } + | { /** - * Workflow failure variant discriminator. + * Original operation identity. */ - type: "workflow_accounting_incomplete"; + operationId: string; + /** + * Terminal Skill installation outcome discriminator. + */ + kind: "cancelled"; } | { + reason: SkillInstallationFailureReason; /** - * Workflow run identifier. + * Present once an operation has been allocated. */ - runId: string; + operationId?: string; /** - * Workflow failure variant discriminator. + * Terminal Skill installation outcome discriminator. */ - type: "workflow_provider_disconnected"; + kind: "refused"; }; /** - * Cumulative resource ceiling that stopped a workflow run. + * Skill installation management result with the honoured contract, or a typed refusal. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowRunFailureKind". + * via the `definition` "SkillInstallationManagementResult". */ /** @experimental */ -export type WorkflowRunFailureKind = - /** The run admitted the approved maximum total number of subagents. */ - | "maxTotalSubagents" - /** The run reached the approved accumulated active-execution time in seconds. */ - | "timeoutSeconds" - /** The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. */ - | "maxAiCredits"; +export type SkillInstallationManagementResult = + | { + /** + * Discriminator: handled management outcome. + */ + kind: "outcome"; + outcome: SkillInstallationManagementOutcome; + negotiated: CatalogNegotiatedContract; + } + | CatalogNegotiationRefusedError + | CatalogInvalidRequestError; /** - * Durable metadata describing who initiated a workflow pause. + * Skill installation result with the honoured contract, or a typed request/negotiation refusal. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowPauseInfo". + * via the `definition` "SkillInstallationResult". */ /** @experimental */ -export type WorkflowPauseInfo = +export type SkillInstallationResult = | { /** - * Workflow pause initiator discriminator. + * Discriminator: handled operation outcome. */ - type: "user"; + kind: "outcome"; + outcome: SkillInstallationOutcome; + negotiated: CatalogNegotiatedContract; } - | { - /** - * Stable author-defined checkpoint key that initiated the pause. - */ - key: string; - /** - * Workflow pause initiator discriminator. - */ - type: "checkpoint"; - }; + | CatalogNegotiationRefusedError + | CatalogInvalidRequestError; /** - * Kind of workflow progress line. + * Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowLogLineKind". + * via the `definition` "SlashCommandInvocationResult". */ /** @experimental */ -export type WorkflowLogLineKind = - /** A narrator log line. */ - | "log" - /** A named workflow phase marker. */ - | "phase"; +export type SlashCommandInvocationResult = + | SlashCommandTextResult + | SlashCommandAgentPromptResult + | SlashCommandCompletedResult + | SlashCommandSelectSubcommandResult + | SlashCommandAddTimelineEntryResult + | SlashCommandShowDialogResult + | SlashCommandSetModelResult + | SlashCommandSetPlanModelResult; /** - * Action the runtime selected for a durable workflow pause checkpoint. + * Subagent settings to apply, or null to clear the live session override * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowPauseCheckpointAction". + * via the `definition` "SubagentSettings". */ /** @experimental */ -export type WorkflowPauseCheckpointAction = - /** The checkpoint was committed by a prior paused attempt, so execution may continue. */ - | "continue" - /** This attempt claimed the checkpoint and must cooperatively stop. */ - | "pause"; +export type SubagentSettings = { + /** + * Per-agent settings keyed by subagent agent_type + */ + agents?: { + [k: string]: SubagentSettingsEntry | undefined; + }; + /** + * Names of subagents the user has turned off; they cannot be dispatched + */ + disabledSubagents?: string[]; + /** + * Maximum number of subagents that can run concurrently; applies to usage-based billing users only + */ + maxConcurrency?: number; + /** + * Maximum subagent nesting depth; applies to usage-based billing users only + */ + maxDepth?: number; +} | null; /** - * Derived lifecycle state of a workflow phase. + * Context tier override for matching subagents * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkflowPhaseStatus". + * via the `definition` "SubagentSettingsEntryContextTier". */ /** @experimental */ -export type WorkflowPhaseStatus = - /** The phase has not been entered yet. */ - | "pending" - /** The phase is currently entered and accumulating active time. */ - | "active" - /** The phase was entered and has since been closed. */ +export type SubagentSettingsEntryContextTier = + /** Inherit the parent session's effective context tier at dispatch time. */ + | "inherit" + /** Use the model's default context window. */ + | "default" + /** Pin the subagent to the long-context tier when supported. */ + | "long_context"; +/** + * Current lifecycle status of the task + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskStatus". + */ +/** @experimental */ +export type TaskStatus = + /** The task is actively executing. */ + | "running" + /** The task is waiting for additional input. */ + | "idle" + /** The task finished successfully. */ | "completed" - /** The phase was never entered because a later phase was entered or the run reached a terminal state. */ - | "skipped"; + /** The task finished with an error. */ + | "failed" + /** The task was cancelled before completion. */ + | "cancelled"; /** - * Type of change represented by this file diff. + * Whether task execution is synchronously awaited or managed in the background * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspaceDiffFileChangeType". + * via the `definition` "TaskExecutionMode". */ /** @experimental */ -export type WorkspaceDiffFileChangeType = - /** The file was added. */ - | "added" - /** The file was modified. */ - | "modified" - /** The file was deleted. */ - | "deleted" - /** The file was renamed. */ - | "renamed"; +export type TaskExecutionMode = + /** The task was started with synchronous waiting. */ + | "sync" + /** The task is managed in the background. */ + | "background"; /** - * Diff mode requested by the client. + * Active status a client owner may publish with a progress update. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspaceDiffMode". + * via the `definition` "TaskClientActiveStatus". */ /** @experimental */ -export type WorkspaceDiffMode = - /** Return staged, unstaged, and untracked working tree changes. */ - | "unstaged" - /** Return changes compared with the default branch. */ - | "branch" - /** Return the cumulative diff of files Copilot changed this session (used in non-git workspaces). */ - | "session"; +export type TaskClientActiveStatus = + /** The external owner is actively working. */ + | "running" + /** The external owner is connected but waiting. */ + | "idle"; /** - * Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. + * Client-owned tasks always execute outside the runtime in background mode. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspacesWorkspaceDetailsHostType". + * via the `definition` "TaskClientExecutionMode". */ /** @experimental */ -export type WorkspacesWorkspaceDetailsHostType = - /** Workspace repository is hosted on GitHub. */ - | "github" - /** Workspace repository is hosted on Azure DevOps. */ - | "ado"; +export type TaskClientExecutionMode = "background"; +/** + * Discriminator for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientType". + */ +/** @experimental */ +export type TaskClientType = "client"; +/** + * Lifecycle status of a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientStatus". + */ +/** @experimental */ +export type TaskClientStatus = + /** The external owner is actively working. */ + | "running" + /** The external owner is connected but waiting. */ + | "idle" + /** The owner reported successful completion. */ + | "completed" + /** The owner reported failure. */ + | "failed" + /** The owner reported or confirmed cancellation. */ + | "cancelled" + /** The bound owner join disappeared; external executor state is unknown. */ + | "orphaned"; +/** + * Connection class owning a client task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwnerKind". + */ +/** @experimental */ +export type TaskClientOwnerKind = + /** A discovered extension connection owns the task. */ + | "extension" + /** A generic SDK connection owns the task. */ + | "sdk"; +/** + * Presence of the task's bound join. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwnerPresence". + */ +/** @experimental */ +export type TaskClientOwnerPresence = + /** The bound session join is connected. */ + | "connected" + /** The bound session join is disconnected. */ + | "disconnected"; +/** + * Progress or terminal update for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientUpdate". + */ +/** @experimental */ +export type TaskClientUpdate = + | { + status?: TaskClientActiveStatus; + /** + * Optional progress message appended to recent activity when nonempty + */ + message?: string; + /** + * Optional progress phase; null clears the current phase + */ + phase?: string | null; + /** + * Optional completion percentage; null clears the current percentage + */ + percentage?: number | null; + /** + * Client task update variant discriminator. + */ + kind: "progress"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Optional opaque successful terminal result + */ + result?: JsonValue; + /** + * Client task update variant discriminator. + */ + kind: "completed"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Human-readable terminal failure message + */ + error: string; + /** + * Optional owner-supplied terminal failure code + */ + code?: string; + /** + * Client task update variant discriminator. + */ + kind: "failed"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Optional human-readable cancellation reason + */ + reason?: string; + /** + * Client task update variant discriminator. + */ + kind: "cancelled"; + }; +/** + * Tracked task union returned by task APIs, containing an agent, client, or shell task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskInfo". + */ +/** @experimental */ +export type TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo; +/** + * Whether the shell runs inside a managed PTY session or as an independent background process + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskShellInfoAttachmentMode". + */ +/** @experimental */ +export type TaskShellInfoAttachmentMode = + /** The shell runs in a managed PTY session. */ + | "attached" + /** The shell runs as an independent background process. */ + | "detached"; +/** + * Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskProgress". + */ +/** @experimental */ +export type TaskProgress = TaskAgentProgress | TaskClientProgress | TaskShellProgress | null; +/** + * Canonical result returned by a session tool. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ToolResult". + */ +/** @experimental */ +export type ToolResult = string | ToolResultExpanded; +/** + * Execution outcome classification. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ToolResultType". + */ +/** @experimental */ +export type ToolResultType = + /** The tool completed successfully. */ + | "success" + /** The tool failed. */ + | "failure" + /** The tool exceeded its execution timeout. */ + | "timeout" + /** The tool request was rejected before execution. */ + | "rejected" + /** Permission policy denied the tool request. */ + | "denied"; +/** + * User's choice for auto-mode switching: yes (allow this turn), yes_always (allow + persist as setting), or no (decline). + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UIAutoModeSwitchResponse". + */ +/** @experimental */ +export type UIAutoModeSwitchResponse = + /** Allow the automatic mode switch for this turn. */ + | "yes" + /** Allow this mode switch and persist the preference. */ + | "yes_always" + /** Decline the automatic mode switch. */ + | "no"; +/** + * Submitted UI elicitation field value: string, number, boolean, or an array of strings. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UIElicitationFieldValue". + */ +/** @experimental */ +export type UIElicitationFieldValue = string | number | boolean | string[]; +/** + * Definition for a single elicitation form field. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UIElicitationSchemaProperty". + */ +/** @experimental */ +export type UIElicitationSchemaProperty = + | ( + | UIElicitationStringEnumField + | UIElicitationStringOneOfField + | UIElicitationArrayEnumField + | UIElicitationArrayAnyOfField + | UIElicitationSchemaPropertyBoolean + | UIElicitationSchemaPropertyString + | UIElicitationSchemaPropertyNumber + ) + | undefined; +/** + * Optional format hint that constrains the accepted input. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UIElicitationSchemaPropertyStringFormat". + */ +/** @experimental */ +export type UIElicitationSchemaPropertyStringFormat = + /** Email address string format. */ + | "email" + /** URI string format. */ + | "uri" + /** Calendar date string format. */ + | "date" + /** Date-time string format. */ + | "date-time"; +/** + * Numeric type accepted by the field. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UIElicitationSchemaPropertyNumberType". + */ +/** @experimental */ +export type UIElicitationSchemaPropertyNumberType = + /** Any JSON number. */ + | "number" + /** Integer JSON number. */ + | "integer"; +/** + * The user's response: accept (submitted), decline (rejected), or cancel (dismissed) + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UIElicitationResponseAction". + */ +/** @experimental */ +export type UIElicitationResponseAction = + /** The user submitted the requested form values. */ + | "accept" + /** The user explicitly declined to provide the requested input. */ + | "decline" + /** The user dismissed the elicitation request. */ + | "cancel"; +/** + * The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UIExitPlanModeAction". + */ +/** @experimental */ +export type UIExitPlanModeAction = + /** Exit plan mode without starting implementation. */ + | "exit_only" + /** Exit plan mode and continue interactively. */ + | "interactive" + /** Exit plan mode and continue in autopilot mode. */ + | "autopilot" + /** Exit plan mode and continue in autopilot mode with parallel subagent execution. */ + | "autopilot_fleet"; +/** + * User action selected for an exhausted session limit. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "UISessionLimitsExhaustedResponseAction". + */ +/** @experimental */ +export type UISessionLimitsExhaustedResponseAction = + /** Increase the current max by an exact AI Credits amount. */ + | "add" + /** Set a new absolute max AI Credits value. */ + | "set" + /** Remove the current session limit. */ + | "unset" + /** Leave the limit unchanged and cancel the blocked model request. */ + | "cancel"; +/** + * Execution-critical workflow storage operation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowDurableOperation". + */ +/** @experimental */ +export type WorkflowDurableOperation = + /** Creating the durable run and declared phases. */ + | "createRun" + /** Persisting the transition to running. */ + | "markRunStarted" + /** Persisting the terminal run envelope. */ + | "finishRun" + /** Persisting subagent admission accounting. */ + | "reserveAgent" + /** Rolling back an uncommitted subagent admission. */ + | "releaseAgent" + /** Persisting an idempotent model-usage charge. */ + | "chargeCredit" + /** Persisting active execution time. */ + | "addElapsed" + /** Reading the authoritative AI-credit total. */ + | "reconcileCreditTotal" + /** Reading a journal entry without treating storage failure as a cache miss. */ + | "journalGet" + /** Persisting a journal entry before reporting success. */ + | "journalPut" + /** Renewing the durable owner lease that proves this process still owns the run. */ + | "refreshLease"; +/** + * Current or terminal state of a workflow run. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowRunStatus". + */ +/** @experimental */ +export type WorkflowRunStatus = + /** The run was minted and is awaiting approval. */ + | "pending" + /** The run is executing. */ + | "running" + /** The run completed successfully. */ + | "completed" + /** The run was interrupted while resource budget remained. */ + | "halted" + /** The current attempt stopped intentionally and the run may be resumed. */ + | "paused" + /** The run was cancelled before completion. */ + | "cancelled" + /** The workflow body failed or reached a cumulative resource ceiling. */ + | "error"; +/** + * Machine-readable workflow run failure. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowRunFailure". + */ +/** @experimental */ +export type WorkflowRunFailure = + | { + kind: WorkflowRunFailureKind; + /** + * Approved effective ceiling that was reached. + */ + value: number; + /** + * Suggested larger ceiling when the runtime can derive one safely. + */ + suggestedValue?: number; + /** + * Workflow run identifier. + */ + runId: string; + /** + * Workflow failure variant discriminator. + */ + type: "workflow_limit_reached"; + } + | { + /** + * Workflow run identifier whose changed limits were declined. + */ + runId: string; + /** + * Human-readable reason the resume did not proceed. + */ + reason: string; + /** + * Workflow failure variant discriminator. + */ + type: "workflow_resume_declined"; + } + | { + /** + * Stable failure code. + */ + code: string; + operation: WorkflowDurableOperation; + /** + * Workflow run identifier. + */ + runId: string; + /** + * Workflow failure variant discriminator. + */ + type: "workflow_durable_failure"; + } + | { + /** + * Workflow run identifier. + */ + runId: string; + /** + * Confirmed usage in nano-AIU, representing the floor of what the run spent. + */ + drainedNanoAiu: number; + /** + * Workflow failure variant discriminator. + */ + type: "workflow_accounting_incomplete"; + } + | { + /** + * Workflow run identifier. + */ + runId: string; + /** + * Workflow failure variant discriminator. + */ + type: "workflow_provider_disconnected"; + }; +/** + * Cumulative resource ceiling that stopped a workflow run. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowRunFailureKind". + */ +/** @experimental */ +export type WorkflowRunFailureKind = + /** The run admitted the approved maximum total number of subagents. */ + | "maxTotalSubagents" + /** The run reached the approved accumulated active-execution time in seconds. */ + | "timeoutSeconds" + /** The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. */ + | "maxAiCredits"; +/** + * Durable metadata describing who initiated a workflow pause. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowPauseInfo". + */ +/** @experimental */ +export type WorkflowPauseInfo = + | { + /** + * Workflow pause initiator discriminator. + */ + type: "user"; + } + | { + /** + * Stable author-defined checkpoint key that initiated the pause. + */ + key: string; + /** + * Workflow pause initiator discriminator. + */ + type: "checkpoint"; + }; +/** + * Kind of workflow progress line. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowLogLineKind". + */ +/** @experimental */ +export type WorkflowLogLineKind = + /** A narrator log line. */ + | "log" + /** A named workflow phase marker. */ + | "phase"; +/** + * Action the runtime selected for a durable workflow pause checkpoint. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowPauseCheckpointAction". + */ +/** @experimental */ +export type WorkflowPauseCheckpointAction = + /** The checkpoint was committed by a prior paused attempt, so execution may continue. */ + | "continue" + /** This attempt claimed the checkpoint and must cooperatively stop. */ + | "pause"; +/** + * Derived lifecycle state of a workflow phase. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkflowPhaseStatus". + */ +/** @experimental */ +export type WorkflowPhaseStatus = + /** The phase has not been entered yet. */ + | "pending" + /** The phase is currently entered and accumulating active time. */ + | "active" + /** The phase was entered and has since been closed. */ + | "completed" + /** The phase was never entered because a later phase was entered or the run reached a terminal state. */ + | "skipped"; +/** + * Type of change represented by this file diff. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspaceDiffFileChangeType". + */ +/** @experimental */ +export type WorkspaceDiffFileChangeType = + /** The file was added. */ + | "added" + /** The file was modified. */ + | "modified" + /** The file was deleted. */ + | "deleted" + /** The file was renamed. */ + | "renamed"; +/** + * Diff mode requested by the client. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspaceDiffMode". + */ +/** @experimental */ +export type WorkspaceDiffMode = + /** Return staged, unstaged, and untracked working tree changes. */ + | "unstaged" + /** Return changes compared with the default branch. */ + | "branch" + /** Return the cumulative diff of files Copilot changed this session (used in non-git workspaces). */ + | "session"; +/** + * Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspacesWorkspaceDetailsHostType". + */ +/** @experimental */ +export type WorkspacesWorkspaceDetailsHostType = + /** Workspace repository is hosted on GitHub. */ + | "github" + /** Workspace repository is hosted on Azure DevOps. */ + | "ado"; +/** + * List of all authenticated users + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountGetAllUsersResult". + */ +/** @experimental */ +export type AccountGetAllUsersResult = AccountAllUsers[]; +/** + * The number of running background agents (task-registry agents) that were cancelled. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionCancelAllBackgroundAgentsResult". + */ +/** @experimental */ +export type SessionCancelAllBackgroundAgentsResult = number; +/** + * Authentication accounts available to the internal session host. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionGitHubAuthGetAllAuthAvailableResult". + */ +/** @experimental */ +export type SessionGitHubAuthGetAllAuthAvailableResult = SessionAuthStatus[]; +/** + * Whether the current authentication was logged out. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionGitHubAuthLogoutResult". + */ +/** @experimental */ +export type SessionGitHubAuthLogoutResult = boolean; +/** + * Whether the requested authentication was logged out. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionGitHubAuthLogoutUserResult". + */ +/** @experimental */ +export type SessionGitHubAuthLogoutUserResult = boolean; + +/** + * Parameters for aborting the current turn + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AbortRequest". + */ +/** @experimental */ +export interface AbortRequest { + reason?: AbortReason; +} +/** + * Result of aborting the current turn + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AbortResult". + */ +/** @experimental */ +export interface AbortResult { + /** + * Whether the abort completed successfully + */ + success: boolean; + /** + * Error message if the abort failed + */ + error?: string; +} + +/** @experimental */ +export interface AcceptedEnqueueCommandResult { + /** + * True because the command was accepted into the local execution queue. + */ + queued: true; + /** + * Stable opaque ID of the queued command. + */ + queueId: string; +} +/** + * Authenticated account entry returned by `account.getAllUsers`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountAllUsers". + */ +/** @experimental */ +export interface AccountAllUsers { + authInfo: AuthInfo; + /** + * Opaque identifier accepted by account and model selection APIs + */ + selectionId?: string; + /** + * Associated token, if available + */ + token?: string; +} +/** + * Authentication-info input variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HMACAuthInfo". + */ +/** @experimental */ +export interface HMACAuthInfo { + /** + * HMAC-based authentication used by GitHub-internal services. + */ + type: "hmac"; + /** + * Authentication host. HMAC auth always targets the public GitHub host. + */ + host: "https://github.com"; + /** + * HMAC secret used to sign requests. + */ + hmac: string; + copilotUser?: CopilotUserResponse; +} +/** + * 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CopilotUserResponse". + */ +/** @experimental */ +export interface CopilotUserResponse { + /** + * GitHub login of the authenticated user. + */ + login?: string; + /** + * Copilot access SKU identifier (e.g. `free_limited_copilot`, `copilot_for_business_seat_quota`) used to gate model and feature access. + */ + access_type_sku?: string; + /** + * Opaque analytics tracking identifier for the user, forwarded from the Copilot API. + */ + analytics_tracking_id?: string; + /** + * Date the Copilot seat was assigned to the user, if applicable. + */ + assigned_date?: + | ( + | { + [k: string]: unknown | undefined; + } + | string + ) + | null; + /** + * Whether the user is eligible to sign up for the free/limited Copilot tier. + */ + can_signup_for_limited?: boolean; + /** + * Whether Copilot chat is enabled for the user. + */ + chat_enabled?: boolean; + /** + * Copilot plan name for the user (e.g. `individual`, `business`, `enterprise`). + */ + copilot_plan?: string; + /** + * Whether `.copilotignore` content-exclusion support is enabled for the user. + */ + copilotignore_enabled?: boolean; + endpoints?: CopilotUserResponseEndpoints; + /** + * Logins of the organizations the user belongs to. + */ + organization_login_list?: string[]; + /** + * Organizations the user belongs to, each with an optional ID, login, and display name. + */ + organization_list?: + | ( + | { + [k: string]: unknown | undefined; + } + | ({ + /** + * Numeric database ID of the organization. + */ + id?: number; + /** + * GitHub login of the organization. + */ + login?: + | ( + | { + [k: string]: unknown | undefined; + } + | string + ) + | null; + /** + * Display name of the organization. + */ + name?: + | ( + | { + [k: string]: unknown | undefined; + } + | string + ) + | null; + } | null)[] + ) + | null; + /** + * Enterprises that provide the user's Copilot license; malformed entries are normalized to null or ID-less shapes. + */ + enterprise_list?: + | ({ + /** + * JavaScript-safe numeric database ID of the enterprise. + */ + id?: number; + } | null)[] + | null; + /** + * Whether the Codex agent is enabled for the user. + */ + codex_agent_enabled?: boolean; + /** + * Whether MCP (Model Context Protocol) support is enabled for the user. + */ + is_mcp_enabled?: + | ( + | { + [k: string]: unknown | undefined; + } + | boolean + ) + | null; + /** + * Date the user's usage quota next resets, as a raw string from the Copilot API; see `quota_reset_date_utc` for the UTC-normalized value. + */ + quota_reset_date?: string; + quota_snapshots?: CopilotUserResponseQuotaSnapshots; + /** + * Whether the user's telemetry is subject to restricted-data handling. + */ + restricted_telemetry?: boolean; + /** + * Whether the user is a GitHub/Microsoft staff member. + */ + is_staff?: boolean; + /** + * Raw passthrough of the Copilot API `te` flag for the user (an opaque server-side eligibility signal surfaced in telemetry); not otherwise interpreted by the runtime. + */ + te?: boolean; + /** + * Whether the account is on usage-based (token/AI-credit) billing rather than a fixed premium-request quota. + */ + token_based_billing?: boolean; + /** + * Whether the user is able to upgrade their Copilot plan. + */ + can_upgrade_plan?: boolean; + /** + * UTC-normalized form of `quota_reset_date` (the date the user's usage quota next resets). + */ + quota_reset_date_utc?: string; + /** + * Per-category quota allotments for free/limited-tier users, keyed by quota category. + */ + limited_user_quotas?: { + [k: string]: number | undefined; + }; + /** + * Date the free/limited-tier user's quotas next reset, as a raw string from the Copilot API. + */ + limited_user_reset_date?: string; + /** + * Per-category monthly quota allotments, keyed by quota category. + */ + monthly_quotas?: { + [k: string]: number | undefined; + }; + /** + * Whether cloud session storage is enabled for the user. + */ + cloud_session_storage_enabled?: boolean; + /** + * Whether CLI remote control is enabled for the user. + */ + cli_remote_control_enabled?: boolean; +} +/** + * Endpoint URLs from the raw Copilot `/copilot_internal/v2/token` user-response passthrough. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CopilotUserResponseEndpoints". + */ +/** @experimental */ +export interface CopilotUserResponseEndpoints { + /** + * Copilot API endpoint URL. + */ + api?: string; + /** + * Origin-tracker endpoint URL. + */ + "origin-tracker"?: string; + /** + * Copilot proxy endpoint URL. + */ + proxy?: string; + /** + * Copilot telemetry endpoint URL. + */ + telemetry?: string; + /** + * Experimental-service endpoint URL. + */ + exp?: string; +} +/** + * Quota snapshot map from the raw Copilot user-response passthrough, with chat, completions, premium-interactions, and other entries. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CopilotUserResponseQuotaSnapshots". + */ +/** @experimental */ +export interface CopilotUserResponseQuotaSnapshots { + chat?: CopilotUserResponseQuotaSnapshotsChat; + completions?: CopilotUserResponseQuotaSnapshotsCompletions; + premium_interactions?: CopilotUserResponseQuotaSnapshotsPremiumInteractions; + [k: string]: + | ({ + entitlement?: number; + overage_count?: number; + overage_permitted?: boolean; + percent_remaining?: number; + quota_id?: string; + quota_remaining?: number; + remaining?: number; + unlimited?: boolean; + timestamp_utc?: string; + has_quota?: boolean; + quota_reset_at?: number; + token_based_billing?: boolean; + } | null) + | undefined; +} +/** + * Chat quota snapshot from the raw Copilot user-response passthrough, with entitlement, overage, remaining quota, reset, and billing fields. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CopilotUserResponseQuotaSnapshotsChat". + */ +/** @experimental */ +export interface CopilotUserResponseQuotaSnapshotsChat { + /** + * Number of requests/units included in the entitlement for this period; `-1` denotes an unlimited entitlement. + */ + entitlement?: number; + /** + * Count of additional pay-per-request usage consumed this period beyond the entitlement. + */ + overage_count?: number; + /** + * Whether usage may continue at pay-per-request rates once the entitlement is exhausted. + */ + overage_permitted?: boolean; + /** + * Percentage of the entitlement remaining at the snapshot timestamp. + */ + percent_remaining?: number; + /** + * Identifier of the quota bucket this snapshot describes. + */ + quota_id?: string; + /** + * Amount of quota remaining at the snapshot timestamp. + */ + quota_remaining?: number; + /** + * Remaining entitlement/quota amount at the snapshot timestamp. + */ + remaining?: number; + /** + * Whether the entitlement for this category is unlimited. + */ + unlimited?: boolean; + /** + * UTC timestamp when this snapshot was captured. + */ + timestamp_utc?: string; + /** + * Whether the user currently has quota available; when `false` and not unlimited, further requests are blocked until the quota resets. + */ + has_quota?: boolean; + /** + * Unix epoch time, in seconds, when this quota next resets. + */ + quota_reset_at?: number; + /** + * Whether this category uses usage-based (token/AI-credit) billing rather than a fixed premium-request count. + */ + token_based_billing?: boolean; +} /** - * List of all authenticated users + * Completions quota snapshot from the raw Copilot user-response passthrough, with entitlement, overage, remaining quota, reset, and billing fields. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountGetAllUsersResult". + * via the `definition` "CopilotUserResponseQuotaSnapshotsCompletions". */ /** @experimental */ -export type AccountGetAllUsersResult = AccountAllUsers[]; +export interface CopilotUserResponseQuotaSnapshotsCompletions { + /** + * Number of requests/units included in the entitlement for this period; `-1` denotes an unlimited entitlement. + */ + entitlement?: number; + /** + * Count of additional pay-per-request usage consumed this period beyond the entitlement. + */ + overage_count?: number; + /** + * Whether usage may continue at pay-per-request rates once the entitlement is exhausted. + */ + overage_permitted?: boolean; + /** + * Percentage of the entitlement remaining at the snapshot timestamp. + */ + percent_remaining?: number; + /** + * Identifier of the quota bucket this snapshot describes. + */ + quota_id?: string; + /** + * Amount of quota remaining at the snapshot timestamp. + */ + quota_remaining?: number; + /** + * Remaining entitlement/quota amount at the snapshot timestamp. + */ + remaining?: number; + /** + * Whether the entitlement for this category is unlimited. + */ + unlimited?: boolean; + /** + * UTC timestamp when this snapshot was captured. + */ + timestamp_utc?: string; + /** + * Whether the user currently has quota available; when `false` and not unlimited, further requests are blocked until the quota resets. + */ + has_quota?: boolean; + /** + * Unix epoch time, in seconds, when this quota next resets. + */ + quota_reset_at?: number; + /** + * Whether this category uses usage-based (token/AI-credit) billing rather than a fixed premium-request count. + */ + token_based_billing?: boolean; +} /** - * The number of running background agents (task-registry agents) that were cancelled. + * Premium-interactions quota snapshot from the raw Copilot user-response passthrough, with entitlement, overage, remaining quota, reset, and billing fields. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionCancelAllBackgroundAgentsResult". + * via the `definition` "CopilotUserResponseQuotaSnapshotsPremiumInteractions". */ /** @experimental */ -export type SessionCancelAllBackgroundAgentsResult = number; +export interface CopilotUserResponseQuotaSnapshotsPremiumInteractions { + /** + * Number of requests/units included in the entitlement for this period; `-1` denotes an unlimited entitlement. + */ + entitlement?: number; + /** + * Count of additional pay-per-request usage consumed this period beyond the entitlement. + */ + overage_count?: number; + /** + * Whether usage may continue at pay-per-request rates once the entitlement is exhausted. + */ + overage_permitted?: boolean; + /** + * Percentage of the entitlement remaining at the snapshot timestamp. + */ + percent_remaining?: number; + /** + * Identifier of the quota bucket this snapshot describes. + */ + quota_id?: string; + /** + * Amount of quota remaining at the snapshot timestamp. + */ + quota_remaining?: number; + /** + * Remaining entitlement/quota amount at the snapshot timestamp. + */ + remaining?: number; + /** + * Whether the entitlement for this category is unlimited. + */ + unlimited?: boolean; + /** + * UTC timestamp when this snapshot was captured. + */ + timestamp_utc?: string; + /** + * Whether the user currently has quota available; when `false` and not unlimited, further requests are blocked until the quota resets. + */ + has_quota?: boolean; + /** + * Unix epoch time, in seconds, when this quota next resets. + */ + quota_reset_at?: number; + /** + * Whether this category uses usage-based (token/AI-credit) billing rather than a fixed premium-request count. + */ + token_based_billing?: boolean; +} /** - * Authentication accounts available to the internal session host. + * Authentication-info input variant for a token sourced from an environment variable, with host, optional login, token, and env var name. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionGitHubAuthGetAllAuthAvailableResult". + * via the `definition` "EnvAuthInfo". */ /** @experimental */ -export type SessionGitHubAuthGetAllAuthAvailableResult = SessionAuthStatus[]; +export interface EnvAuthInfo { + /** + * Personal access token (PAT) or server-to-server token sourced from an environment variable. + */ + type: "env"; + /** + * Authentication host (e.g. https://github.com or a GHES host). + */ + host: string; + /** + * User login associated with the token. Undefined for server-to-server tokens (those starting with `ghs_`). + */ + login?: string; + /** + * The token value itself. Treat as a secret. + */ + token: string; + /** + * Name of the environment variable the token was sourced from. + */ + envVar: string; + copilotUser?: CopilotUserResponse; +} /** - * Whether the current authentication was logged out. + * Authentication-info input variant for SDK-configured token authentication, carrying host and the secret token value. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionGitHubAuthLogoutResult". + * via the `definition` "TokenAuthInfo". */ /** @experimental */ -export type SessionGitHubAuthLogoutResult = boolean; +export interface TokenAuthInfo { + /** + * SDK-side token authentication; the host configured the token directly via the SDK. + */ + type: "token"; + /** + * Authentication host. + */ + host: string; + /** + * The token value itself. Treat as a secret. + */ + token: string; + /** + * Opaque native GitHub credential registration backing this token identity, when applicable. + */ + registrationId?: string; + copilotUser?: CopilotUserResponse; +} /** - * Whether the requested authentication was logged out. + * Authentication-info variant backed by an SDK GitHub token callback. It carries routing metadata but never a plaintext token. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionGitHubAuthLogoutUserResult". + * via the `definition` "TokenProviderAuthInfo". */ /** @experimental */ -export type SessionGitHubAuthLogoutUserResult = boolean; - +export interface TokenProviderAuthInfo { + /** + * SDK callback-backed GitHub token authentication. + */ + type: "token-provider"; + /** + * Authentication host. + */ + host: string; + /** + * Opaque SDK callback registration identifier. + */ + registrationId: string; + copilotUser?: CopilotUserResponse; +} /** - * Parameters for aborting the current turn + * Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AbortRequest". + * via the `definition` "CopilotApiTokenAuthInfo". */ /** @experimental */ -export interface AbortRequest { - reason?: AbortReason; +export interface CopilotApiTokenAuthInfo { + /** + * Direct Copilot API authentication via the `GITHUB_COPILOT_API_TOKEN` + `COPILOT_API_URL` environment-variable pair. The token itself is read from the environment by the runtime, not carried in this struct. + */ + type: "copilot-api-token"; + /** + * Authentication host (always the public GitHub host). + */ + host: "https://github.com"; + copilotUser?: CopilotUserResponse; } /** - * Result of aborting the current turn + * Authentication-info variant for OAuth user auth, with host and login; the token remains in the runtime secret store. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AbortResult". + * via the `definition` "UserAuthInfo". */ /** @experimental */ -export interface AbortResult { +export interface UserAuthInfo { /** - * Whether the abort completed successfully + * OAuth user authentication. The token itself is held in the runtime's secret token store (keyed by host+login) and is NOT carried in this struct. */ - success: boolean; + type: "user"; /** - * Error message if the abort failed + * Authentication host. */ - error?: string; + host: string; + /** + * OAuth user login. + */ + 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 AcceptedEnqueueCommandResult { +export interface AccountAuthInfo { /** - * True because the command was accepted into the local execution queue. + * Provider-owned account authentication. */ - queued: true; + type: "account"; /** - * Stable opaque ID of the queued command. + * Host coordinate owned by the account's model provider. */ - queueId: string; + host: string; + /** + * Login identifying the provider-owned account. + */ + login: string; } /** - * Authenticated account entry returned by `account.getAllUsers`. + * Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountAllUsers". + * via the `definition` "GhCliAuthInfo". */ /** @experimental */ -export interface AccountAllUsers { - authInfo: AuthInfo; +export interface GhCliAuthInfo { /** - * Opaque identifier accepted by account and model selection APIs + * Authentication via the `gh` CLI's saved credentials. + */ + type: "gh-cli"; + /** + * Authentication host. + */ + host: string; + /** + * User login as reported by `gh auth status`. */ - selectionId?: string; + login: string; /** - * Associated token, if available + * The token returned by `gh auth token`. Treat as a secret. */ - token?: string; + token: string; + copilotUser?: CopilotUserResponse; } /** - * Authentication-info input variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret. + * Authentication-info input variant for API-key authentication to a non-GitHub LLM provider, carrying the secret `apiKey` and host. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HMACAuthInfo". + * via the `definition` "ApiKeyAuthInfo". */ /** @experimental */ -export interface HMACAuthInfo { +export interface ApiKeyAuthInfo { /** - * HMAC-based authentication used by GitHub-internal services. + * API-key authentication for non-GitHub LLM providers (e.g. when running BYOM-style). */ - type: "hmac"; + type: "api-key"; /** - * Authentication host. HMAC auth always targets the public GitHub host. + * The API key. Treat as a secret. */ - host: "https://github.com"; + apiKey: string; /** - * HMAC secret used to sign requests. + * Authentication host. */ - hmac: string; + host: string; copilotUser?: CopilotUserResponse; } /** - * 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. + * Current authentication state * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CopilotUserResponse". + * via the `definition` "AccountGetCurrentAuthResult". */ /** @experimental */ -export interface CopilotUserResponse { - /** - * GitHub login of the authenticated user. - */ - login?: string; - /** - * Copilot access SKU identifier (e.g. `free_limited_copilot`, `copilot_for_business_seat_quota`) used to gate model and feature access. - */ - access_type_sku?: string; - /** - * Opaque analytics tracking identifier for the user, forwarded from the Copilot API. - */ - analytics_tracking_id?: string; +export interface AccountGetCurrentAuthResult { + authInfo?: AuthInfo; /** - * Date the Copilot seat was assigned to the user, if applicable. + * Authentication errors from the last auth attempt, if any */ - assigned_date?: - | ( - | { - [k: string]: unknown | undefined; - } - | string - ) - | null; + authErrors?: string[]; +} + +/** @experimental */ +export interface AccountGetQuotaRequest { /** - * Whether the user is eligible to sign up for the free/limited Copilot tier. + * Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used. */ - can_signup_for_limited?: boolean; + selectionId?: string; /** - * Whether Copilot chat is enabled for the user. + * GitHub token accepted for compatibility with existing SDK clients. When provided, resolves this token instead of using the current account. */ - chat_enabled?: boolean; + gitHubToken?: string; +} +/** + * Quota usage snapshots for the resolved user, keyed by quota type. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountGetQuotaResult". + */ +/** @experimental */ +export interface AccountGetQuotaResult { /** - * Copilot plan name for the user (e.g. `individual`, `business`, `enterprise`). + * Quota snapshots keyed by type (e.g., chat, completions, premium_interactions) */ - copilot_plan?: string; + quotaSnapshots: { + [k: string]: AccountQuotaSnapshot | undefined; + }; +} +/** + * Quota usage snapshot for a Copilot quota type, including entitlement, used requests, overage, reset date, and remaining percentage. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountQuotaSnapshot". + */ +/** @experimental */ +export interface AccountQuotaSnapshot { /** - * Whether `.copilotignore` content-exclusion support is enabled for the user. + * Whether the user has an unlimited usage entitlement */ - copilotignore_enabled?: boolean; - endpoints?: CopilotUserResponseEndpoints; + isUnlimitedEntitlement: boolean; /** - * Logins of the organizations the user belongs to. + * Number of requests included in the entitlement, or -1 for unlimited entitlements */ - organization_login_list?: string[]; + entitlementRequests: number; /** - * Organizations the user belongs to, each with an optional ID, login, and display name. + * Number of requests used so far this period */ - organization_list?: - | ( - | { - [k: string]: unknown | undefined; - } - | ({ - /** - * Numeric database ID of the organization. - */ - id?: number; - /** - * GitHub login of the organization. - */ - login?: - | ( - | { - [k: string]: unknown | undefined; - } - | string - ) - | null; - /** - * Display name of the organization. - */ - name?: - | ( - | { - [k: string]: unknown | undefined; - } - | string - ) - | null; - } | null)[] - ) - | null; + usedRequests: number; /** - * Enterprises that provide the user's Copilot license; malformed entries are normalized to null or ID-less shapes. + * Whether usage is still permitted after quota exhaustion */ - enterprise_list?: - | ({ - /** - * JavaScript-safe numeric database ID of the enterprise. - */ - id?: number; - } | null)[] - | null; + usageAllowedWithExhaustedQuota: boolean; /** - * Whether the Codex agent is enabled for the user. + * Percentage of entitlement remaining */ - codex_agent_enabled?: boolean; + remainingPercentage: number; /** - * Whether MCP (Model Context Protocol) support is enabled for the user. + * Number of additional usage requests made this period */ - is_mcp_enabled?: - | ( - | { - [k: string]: unknown | undefined; - } - | boolean - ) - | null; + overage: number; /** - * Date the user's usage quota next resets, as a raw string from the Copilot API; see `quota_reset_date_utc` for the UTC-normalized value. + * Whether additional usage is allowed when quota is exhausted */ - quota_reset_date?: string; - quota_snapshots?: CopilotUserResponseQuotaSnapshots; + overageAllowedWithExhaustedQuota: boolean; /** - * Whether the user's telemetry is subject to restricted-data handling. + * Date when the quota resets (ISO 8601 string) */ - restricted_telemetry?: boolean; + resetDate?: string; +} +/** + * Credentials to validate and store. Omit login to resolve the authenticated user from the token. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountLoginRequest". + */ +/** @experimental */ +export interface AccountLoginRequest { /** - * Whether the user is a GitHub/Microsoft staff member. + * GitHub host URL */ - is_staff?: boolean; + host: string; /** - * Raw passthrough of the Copilot API `te` flag for the user (an opaque server-side eligibility signal surfaced in telemetry); not otherwise interpreted by the runtime. + * User login/username. When omitted, the runtime validates the token and resolves the login from GitHub. */ - te?: boolean; + login?: string; /** - * Whether the account is on usage-based (token/AI-credit) billing rather than a fixed premium-request quota. + * GitHub authentication token */ - token_based_billing?: boolean; + token: string; +} +/** + * Result of a successful login; throws on failure + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountLoginResult". + */ +/** @experimental */ +export interface AccountLoginResult { /** - * Whether the user is able to upgrade their Copilot plan. + * Whether the credential was persisted to a secure store (system keychain, or the config file when plaintext storage is enabled). False when no secure store was available and the token was not saved, so the consumer can decide how to proceed. */ - can_upgrade_plan?: boolean; + storedInVault: boolean; +} +/** + * Logout result indicating if more users remain + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountLogoutResult". + */ +/** @experimental */ +export interface AccountLogoutResult { /** - * UTC-normalized form of `quota_reset_date` (the date the user's usage quota next resets). + * Whether other authenticated users remain after logout */ - quota_reset_date_utc?: string; + hasMoreUsers: boolean; +} +/** + * Enumerate request carrying the typed collection query. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountsEnumerateRequest". + */ +/** @experimental */ +export interface AccountsEnumerateRequest { + query: AuthEnumerateQuery; +} +/** + * Read request carrying the typed datum query. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountsGetRequest". + */ +/** @experimental */ +export interface AccountsGetRequest { + query: AuthReadQuery; +} +/** + * Mutation request carrying the typed write command. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountsSetRequest". + */ +/** @experimental */ +export interface AccountsSetRequest { + command: AuthWrite; +} +/** + * One signed-in account in the roster forest. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountStatus". + */ +/** @experimental */ +export interface AccountStatus { /** - * Per-category quota allotments for free/limited-tier users, keyed by quota category. + * Authentication host URL. */ - limited_user_quotas?: { - [k: string]: number | undefined; - }; + host: string; /** - * Date the free/limited-tier user's quotas next reset, as a raw string from the Copilot API. + * Authenticated login/username. */ - limited_user_reset_date?: string; + login: string; + kind: AccountKind; /** - * Per-category monthly quota allotments, keyed by quota category. + * 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. */ - monthly_quotas?: { - [k: string]: number | undefined; - }; + derivedFrom?: string; /** - * Whether cloud session storage is enabled for the user. + * Whether this is the active account. */ - cloud_session_storage_enabled?: boolean; + active: boolean; /** - * Whether CLI remote control is enabled for the user. + * Opaque selection id used to switch to, or log out, this account. */ - cli_remote_control_enabled?: boolean; + selectionId: string; } /** - * Endpoint URLs from the raw Copilot `/copilot_internal/v2/token` user-response passthrough. + * Canonical directory where custom agents can be discovered or created, with scope, preference, and optional project path. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CopilotUserResponseEndpoints". + * via the `definition` "AgentDiscoveryPath". */ /** @experimental */ -export interface CopilotUserResponseEndpoints { - /** - * Copilot API endpoint URL. - */ - api?: string; +export interface AgentDiscoveryPath { /** - * Origin-tracker endpoint URL. + * Absolute path of the search/create directory (may not exist on disk yet) */ - "origin-tracker"?: string; + path: string; + scope: AgentDiscoveryPathScope; /** - * Copilot proxy endpoint URL. + * Whether this is the canonical directory to create a new agent in its tier. At most one entry per tier is preferred. */ - proxy?: string; + preferredForCreation: boolean; /** - * Copilot telemetry endpoint URL. + * The input project path this directory was derived from (only for project scope) */ - telemetry?: string; + projectPath?: string; +} +/** + * Canonical locations where custom agents can be created so the runtime will recognize them. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentDiscoveryPathList". + */ +/** @experimental */ +export interface AgentDiscoveryPathList { /** - * Experimental-service endpoint URL. + * Canonical agent create/discovery directories, in priority order */ - exp?: string; + paths: AgentDiscoveryPath[]; } /** - * Quota snapshot map from the raw Copilot user-response passthrough, with chat, completions, premium-interactions, and other entries. + * The currently selected custom agent, or null when using the default agent. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CopilotUserResponseQuotaSnapshots". + * via the `definition` "AgentGetCurrentResult". */ /** @experimental */ -export interface CopilotUserResponseQuotaSnapshots { - chat?: CopilotUserResponseQuotaSnapshotsChat; - completions?: CopilotUserResponseQuotaSnapshotsCompletions; - premium_interactions?: CopilotUserResponseQuotaSnapshotsPremiumInteractions; - [k: string]: - | ({ - entitlement?: number; - overage_count?: number; - overage_permitted?: boolean; - percent_remaining?: number; - quota_id?: string; - quota_remaining?: number; - remaining?: number; - unlimited?: boolean; - timestamp_utc?: string; - has_quota?: boolean; - quota_reset_at?: number; - token_based_billing?: boolean; - } | null) - | undefined; +export interface AgentGetCurrentResult { + /** + * Currently selected custom agent, or null if using the default agent + */ + agent?: AgentInfo | null; } /** - * Chat quota snapshot from the raw Copilot user-response passthrough, with entitlement, overage, remaining quota, reset, and billing fields. + * Agent metadata, including identifiers, display details, source, tools, model, models, reasoning effort, MCP servers, skills, and file path. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CopilotUserResponseQuotaSnapshotsChat". + * via the `definition` "AgentInfo". */ /** @experimental */ -export interface CopilotUserResponseQuotaSnapshotsChat { +export interface AgentInfo { /** - * Number of requests/units included in the entitlement for this period; `-1` denotes an unlimited entitlement. + * Name of the agent. Use `id` as the stable selection identifier. */ - entitlement?: number; + name: string; /** - * Count of additional pay-per-request usage consumed this period beyond the entitlement. + * Human-readable display name */ - overage_count?: number; + displayName: string; /** - * Whether usage may continue at pay-per-request rates once the entitlement is exhausted. + * Description of the agent's purpose */ - overage_permitted?: boolean; + description: string; /** - * Percentage of the entitlement remaining at the snapshot timestamp. + * Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. */ - percent_remaining?: number; + path?: string; /** - * Identifier of the quota bucket this snapshot describes. + * Stable identifier for selection. For most agents this is the same as `name`; for plugin/builtin agents it may differ. Always populated; defaults to `name` when no distinct id was assigned. */ - quota_id?: string; + id: string; + source?: AgentInfoSource; /** - * Amount of quota remaining at the snapshot timestamp. + * Whether the agent can be selected directly by the user. Agents marked `false` are subagent-only. */ - quota_remaining?: number; + userInvocable?: boolean; /** - * Remaining entitlement/quota amount at the snapshot timestamp. + * Whether model-driven invocation is disabled for this agent. */ - remaining?: number; + disableModelInvocation?: boolean; /** - * Whether the entitlement for this category is unlimited. + * Allowed tool names for this agent. Empty array means none; omitted means inherit defaults. */ - unlimited?: boolean; + tools?: string[]; /** - * UTC timestamp when this snapshot was captured. + * Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. */ - timestamp_utc?: string; + model?: string; /** - * Whether the user currently has quota available; when `false` and not unlimited, further requests are blocked until the quota resets. + * Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. */ - has_quota?: boolean; + models?: string[]; + modelPolicy?: AgentModelPolicy; /** - * Unix epoch time, in seconds, when this quota next resets. + * Authored reasoning effort for this agent. Applied on selection to models that support it; omitted means no authored preference. */ - quota_reset_at?: number; + reasoningEffort?: string; /** - * Whether this category uses usage-based (token/AI-credit) billing rather than a fixed premium-request count. + * MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. + * + * @experimental */ - token_based_billing?: boolean; + mcpServers?: { + [k: string]: JsonValue | undefined; + }; + /** + * Skill names preloaded into this agent's context. Omitted means none. + */ + skills?: string[]; + /** + * Authored base prompt for the agent. Runtime prompt assembly may add dynamic context at invocation time. Omitted from `session.agent.list` unless `includePrompt` is true. + */ + prompt?: string; } /** - * Completions quota snapshot from the raw Copilot user-response passthrough, with entitlement, overage, remaining quota, reset, and billing fields. + * Agents available to the session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CopilotUserResponseQuotaSnapshotsCompletions". + * via the `definition` "AgentList". */ /** @experimental */ -export interface CopilotUserResponseQuotaSnapshotsCompletions { - /** - * Number of requests/units included in the entitlement for this period; `-1` denotes an unlimited entitlement. - */ - entitlement?: number; - /** - * Count of additional pay-per-request usage consumed this period beyond the entitlement. - */ - overage_count?: number; - /** - * Whether usage may continue at pay-per-request rates once the entitlement is exhausted. - */ - overage_permitted?: boolean; - /** - * Percentage of the entitlement remaining at the snapshot timestamp. - */ - percent_remaining?: number; - /** - * Identifier of the quota bucket this snapshot describes. - */ - quota_id?: string; - /** - * Amount of quota remaining at the snapshot timestamp. - */ - quota_remaining?: number; +export interface AgentList { /** - * Remaining entitlement/quota amount at the snapshot timestamp. + * Available agents */ - remaining?: number; + agents: AgentInfo[]; +} +/** + * Full registry entry for the spawned child. Lets the controller call `handleLiveTargetSelected(entry)` directly without re-reading the registry (avoids a TOCTOU window). + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentRegistryLiveTargetEntry". + */ +/** @experimental */ +export interface AgentRegistryLiveTargetEntry { /** - * Whether the entitlement for this category is unlimited. + * Registry entry schema version (1 = ui-server, 2 = managed-server) */ - unlimited?: boolean; + schemaVersion: number; + kind: AgentRegistryLiveTargetEntryKind; /** - * UTC timestamp when this snapshot was captured. + * Operating-system pid of the process owning this entry */ - timestamp_utc?: string; + pid: number; /** - * Whether the user currently has quota available; when `false` and not unlimited, further requests are blocked until the quota resets. + * Bind host for the entry's JSON-RPC server */ - has_quota?: boolean; + host: string; /** - * Unix epoch time, in seconds, when this quota next resets. + * TCP port the entry's JSON-RPC server is listening on */ - quota_reset_at?: number; + port: number; /** - * Whether this category uses usage-based (token/AI-credit) billing rather than a fixed premium-request count. + * Session ID of the foreground session for this entry */ - token_based_billing?: boolean; -} -/** - * Premium-interactions quota snapshot from the raw Copilot user-response passthrough, with entitlement, overage, remaining quota, reset, and billing fields. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CopilotUserResponseQuotaSnapshotsPremiumInteractions". - */ -/** @experimental */ -export interface CopilotUserResponseQuotaSnapshotsPremiumInteractions { + sessionId?: string; /** - * Number of requests/units included in the entitlement for this period; `-1` denotes an unlimited entitlement. + * Friendly session name (when set) */ - entitlement?: number; + sessionName?: string; /** - * Count of additional pay-per-request usage consumed this period beyond the entitlement. + * Working directory of the session (when known) */ - overage_count?: number; + cwd?: string; /** - * Whether usage may continue at pay-per-request rates once the entitlement is exhausted. + * Git branch of the session (when known) */ - overage_permitted?: boolean; + branch?: string; /** - * Percentage of the entitlement remaining at the snapshot timestamp. + * Model identifier currently selected for the session */ - percent_remaining?: number; + model?: string; + status?: AgentRegistryLiveTargetEntryStatus; + attentionKind?: AgentRegistryLiveTargetEntryAttentionKind; /** - * Identifier of the quota bucket this snapshot describes. + * Monotonic per-publisher revision counter incremented on every status update. Lets watchers detect transient flips. */ - quota_id?: string; + statusRevision?: number; + lastTerminalEvent?: AgentRegistryLiveTargetEntryLastTerminalEvent; /** - * Amount of quota remaining at the snapshot timestamp. + * ISO 8601 timestamp captured at registration */ - quota_remaining?: number; + startedAt: string; /** - * Remaining entitlement/quota amount at the snapshot timestamp. + * Copilot CLI version that wrote the entry */ - remaining?: number; + copilotVersion: string; /** - * Whether the entitlement for this category is unlimited. + * Wall-clock milliseconds since the watcher last observed this entry (heartbeat freshness) */ - unlimited?: boolean; + lastSeenMs: number; /** - * UTC timestamp when this snapshot was captured. + * Connection token (null when the target is unauthenticated) + * + * @internal */ - timestamp_utc?: string; + token?: string | null; +} +/** + * Canonical process-log discovery outcome; populated from spawnLiveTarget. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentRegistryLogCapture". + */ +/** @experimental */ +export interface AgentRegistryLogCapture { /** - * Whether the user currently has quota available; when `false` and not unlimited, further requests are blocked until the quota resets. + * Whether a canonical process log was discovered for this managed spawn */ - has_quota?: boolean; + enabled: boolean; /** - * Unix epoch time, in seconds, when this quota next resets. + * Absolute path to the managed spawn's process--.log file (only set when enabled) */ - quota_reset_at?: number; + path?: string; /** - * Whether this category uses usage-based (token/AI-credit) billing rather than a fixed premium-request count. + * Why no canonical process log could be opened for this managed spawn (set only when enabled is false) */ - token_based_billing?: boolean; + openError?: string; + openErrorReason?: AgentRegistryLogCaptureOpenErrorReason; } /** - * Authentication-info input variant for a token sourced from an environment variable, with host, optional login, token, and env var name. + * `child_process.spawn` itself failed before the child entered the registry. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EnvAuthInfo". + * via the `definition` "AgentRegistrySpawnError". */ /** @experimental */ -export interface EnvAuthInfo { +export interface AgentRegistrySpawnError { /** - * Personal access token (PAT) or server-to-server token sourced from an environment variable. + * Discriminator: child_process.spawn itself failed */ - type: "env"; + kind: "spawn-error"; /** - * Authentication host (e.g. https://github.com or a GHES host). + * Human-readable error message */ - host: string; + message: string; /** - * User login associated with the token. Undefined for server-to-server tokens (those starting with `ghs_`). + * Underlying errno code (e.g. ENOENT, EACCES) when available */ - login?: string; + code?: string; +} +/** + * Spawn succeeded but the child did not publish a matching managed-server entry within the timeout. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentRegistrySpawnRegistryTimeout". + */ +/** @experimental */ +export interface AgentRegistrySpawnRegistryTimeout { /** - * The token value itself. Treat as a secret. + * Discriminator: spawn succeeded but child never registered */ - token: string; + kind: "registry-timeout"; /** - * Name of the environment variable the token was sourced from. + * Process ID of the orphaned child (so the caller can offer 'kill the pid' guidance) */ - envVar: string; - copilotUser?: CopilotUserResponse; + childPid: number; + logCapture?: AgentRegistryLogCapture; } /** - * Authentication-info input variant for SDK-configured token authentication, carrying host and the secret token value. + * Inputs to spawn a managed-server child via the controller's spawn delegate. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TokenAuthInfo". + * via the `definition` "AgentRegistrySpawnRequest". */ /** @experimental */ -export interface TokenAuthInfo { +export interface AgentRegistrySpawnRequest { /** - * SDK-side token authentication; the host configured the token directly via the SDK. + * Working directory for the spawned child (must be an existing directory) */ - type: "token"; + cwd: string; /** - * Authentication host. + * Custom or built-in agent name (e.g. 'explore'). When omitted, the child uses its own default. */ - host: string; + agentName?: string; /** - * The token value itself. Treat as a secret. + * Model identifier to apply to the new session */ - token: string; + model?: string; /** - * Opaque native GitHub credential registration backing this token identity, when applicable. + * Friendly session name. Must satisfy validateSessionName: non-empty, no leading/trailing whitespace, <=100 chars, no control chars, no double quotes. */ - registrationId?: string; - copilotUser?: CopilotUserResponse; + name?: string; + permissionMode?: AgentRegistrySpawnPermissionMode; + /** + * Optional first user message. Forwarded to the caller (the CLI's spawn wrapper sends it post-attach via the standard LocalRpcSession.send path). + */ + initialPrompt?: string; } /** - * Authentication-info variant backed by an SDK GitHub token callback. It carries routing metadata but never a plaintext token. + * Managed-server child was spawned and registered successfully. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "TokenProviderAuthInfo". + * via the `definition` "AgentRegistrySpawnSpawned". */ /** @experimental */ -export interface TokenProviderAuthInfo { +export interface AgentRegistrySpawnSpawned { /** - * SDK callback-backed GitHub token authentication. + * Discriminator: managed-server child spawned successfully */ - type: "token-provider"; + kind: "spawned"; + entry: AgentRegistryLiveTargetEntry; /** - * Authentication host. + * Whether the delegate already sent the initial prompt. Always omitted in the current wiring: the controller sends the prompt post-attach via the standard LocalRpcSession.send path. */ - host: string; + initialPromptSent?: boolean; /** - * Opaque SDK callback registration identifier. + * If the delegate attempted to send the initial prompt and failed, the categorized error message. */ - registrationId: string; - copilotUser?: CopilotUserResponse; + initialPromptError?: string; + logCapture?: AgentRegistryLogCapture; } /** - * Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host. + * Synchronous pre-validation rejected the spawn request. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CopilotApiTokenAuthInfo". + * via the `definition` "AgentRegistrySpawnValidationError". */ /** @experimental */ -export interface CopilotApiTokenAuthInfo { +export interface AgentRegistrySpawnValidationError { /** - * Direct Copilot API authentication via the `GITHUB_COPILOT_API_TOKEN` + `COPILOT_API_URL` environment-variable pair. The token itself is read from the environment by the runtime, not carried in this struct. + * Discriminator: synchronous pre-validation rejected the request */ - type: "copilot-api-token"; + kind: "validation-error"; + reason: AgentRegistrySpawnValidationErrorReason; + field?: AgentRegistrySpawnValidationErrorField; /** - * Authentication host (always the public GitHub host). + * Human-readable explanation; safe to surface in the UI banner. Never logged to unrestricted telemetry. */ - host: "https://github.com"; - copilotUser?: CopilotUserResponse; + message: string; } /** - * Authentication-info variant for OAuth user auth, with host and login; the token remains in the runtime secret store. + * Custom agents available to the session after reloading definitions from disk. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UserAuthInfo". + * via the `definition` "AgentReloadResult". */ /** @experimental */ -export interface UserAuthInfo { - /** - * OAuth user authentication. The token itself is held in the runtime's secret token store (keyed by host+login) and is NOT carried in this struct. - */ - type: "user"; - /** - * Authentication host. - */ - host: string; +export interface AgentReloadResult { /** - * OAuth user login. + * Reloaded custom agents */ - login: string; - copilotUser?: CopilotUserResponse; + agents: AgentInfo[]; } /** - * Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. + * The models a custom agent asks for, and the models actually available. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "GhCliAuthInfo". + * via the `definition` "AgentsCustomAgentInitialModelDecisionParams". */ /** @experimental */ -export interface GhCliAuthInfo { - /** - * Authentication via the `gh` CLI's saved credentials. - */ - type: "gh-cli"; - /** - * Authentication host. - */ - host: string; +/** @internal */ +export interface AgentsCustomAgentInitialModelDecisionParams { /** - * User login as reported by `gh auth status`. + * The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. */ - login: string; + agentModelsJson: string; /** - * The token returned by `gh auth token`. Treat as a secret. + * The models available to this session, serialized in the shape the model list carries. */ - token: string; - copilotUser?: CopilotUserResponse; + availableModelsJson: string; } /** - * Authentication-info input variant for API-key authentication to a non-GitHub LLM provider, carrying the secret `apiKey` and host. + * The model to switch to, and the warning to show when the agent's preference could not be met. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ApiKeyAuthInfo". + * via the `definition` "AgentsCustomAgentInitialModelDecisionResult". */ /** @experimental */ -export interface ApiKeyAuthInfo { +/** @internal */ +export interface AgentsCustomAgentInitialModelDecisionResult { /** - * API-key authentication for non-GitHub LLM providers (e.g. when running BYOM-style). + * The first available model that matches the agent's preferences. Absent when none of the requested models is available. */ - type: "api-key"; + targetModel?: string; /** - * The API key. Treat as a secret. + * The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. */ - apiKey: string; + reasoningEffort?: string; /** - * Authentication host. + * What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. */ - host: string; - copilotUser?: CopilotUserResponse; + warning?: string; } /** - * Current authentication state + * Optional project paths to include in agent discovery. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountGetCurrentAuthResult". + * via the `definition` "AgentsDiscoverRequest". */ /** @experimental */ -export interface AccountGetCurrentAuthResult { - authInfo?: AuthInfo; - /** - * Authentication errors from the last auth attempt, if any - */ - authErrors?: string[]; -} - -/** @experimental */ -export interface AccountGetQuotaRequest { +export interface AgentsDiscoverRequest { /** - * Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used. + * Optional list of project directory paths to scan for project-scoped agents. When omitted or empty, only user/plugin/remote-independent agents are returned (no project scan). */ - selectionId?: string; + projectPaths?: string[]; /** - * GitHub token accepted for compatibility with existing SDK clients. When provided, resolves this token instead of using the current account. + * When true, omit the host's agents (the user-level agent directory and all plugin agents), leaving only project and remote agents. For multitenant deployments. */ - gitHubToken?: string; + excludeHostAgents?: boolean; } /** - * Quota usage snapshots for the resolved user, keyed by quota type. + * Name of the custom agent to select for subsequent turns. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountGetQuotaResult". + * via the `definition` "AgentSelectRequest". */ /** @experimental */ -export interface AccountGetQuotaResult { +export interface AgentSelectRequest { /** - * Quota snapshots keyed by type (e.g., chat, completions, premium_interactions) + * Name of the custom agent to select */ - quotaSnapshots: { - [k: string]: AccountQuotaSnapshot | undefined; - }; + name: string; } /** - * Quota usage snapshot for a Copilot quota type, including entitlement, used requests, overage, reset date, and remaining percentage. + * The newly selected custom agent. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountQuotaSnapshot". + * via the `definition` "AgentSelectResult". */ /** @experimental */ -export interface AccountQuotaSnapshot { - /** - * Whether the user has an unlimited usage entitlement - */ - isUnlimitedEntitlement: boolean; - /** - * Number of requests included in the entitlement, or -1 for unlimited entitlements - */ - entitlementRequests: number; - /** - * Number of requests used so far this period - */ - usedRequests: number; +export interface AgentSelectResult { + agent: AgentInfo; +} +/** + * An in-memory authored prompt override for an available agent. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentSetPromptRequest". + */ +/** @experimental */ +export interface AgentSetPromptRequest { /** - * Whether usage is still permitted after quota exhaustion + * Stable effective agent id. Plugin namespace separators are normalized. */ - usageAllowedWithExhaustedQuota: boolean; + id: string; /** - * Percentage of entitlement remaining + * Replacement authored prompt. Empty text is valid. */ - remainingPercentage: number; + prompt: string; +} +/** + * The feature flags to evaluate shipped agents against. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentsGetAvailableBuiltinsRequest". + */ +/** @experimental */ +/** @internal */ +export interface AgentsGetAvailableBuiltinsRequest { /** - * Number of additional usage requests made this period + * Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. */ - overage: number; + featureFlags?: { + [k: string]: unknown | undefined; + } | null; /** - * Whether additional usage is allowed when quota is exhausted + * Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. */ - overageAllowedWithExhaustedQuota: boolean; + overrides?: { + [k: string]: unknown | undefined; + } | null; /** - * Date when the quota resets (ISO 8601 string) + * The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. */ - resetDate?: string; + context?: string | null; } /** - * Credentials to validate and store. Omit login to resolve the authenticated user from the token. + * The shipped agents available under the requested flags. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountLoginRequest". + * via the `definition` "AgentsGetAvailableBuiltinsResult". */ /** @experimental */ -export interface AccountLoginRequest { - /** - * GitHub host URL - */ - host: string; - /** - * User login/username. When omitted, the runtime validates the token and resolves the login from GitHub. - */ - login?: string; +/** @internal */ +export interface AgentsGetAvailableBuiltinsResult { /** - * GitHub authentication token + * Available shipped agents, in the runtime's own order. */ - token: string; + agents: BuiltinAgentSummary[]; } /** - * Result of a successful login; throws on failure + * A shipped agent, named and described. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountLoginResult". + * via the `definition` "BuiltinAgentSummary". */ /** @experimental */ -export interface AccountLoginResult { +/** @internal */ +export interface BuiltinAgentSummary { /** - * Whether the credential was persisted to a secure store (system keychain, or the config file when plaintext storage is enabled). False when no secure store was available and the token was not saved, so the consumer can decide how to proceed. + * The agent name, as it appears in `getBuiltins`. */ - storedInVault: boolean; + name: string; + /** + * One-line description of what the agent does. + */ + description: string; } /** - * Logout result indicating if more users remain + * The shipped agent whose definition to load. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AccountLogoutResult". + * via the `definition` "AgentsGetBuiltinDefinitionRequest". */ /** @experimental */ -export interface AccountLogoutResult { +/** @internal */ +export interface AgentsGetBuiltinDefinitionRequest { /** - * Whether other authenticated users remain after logout + * The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. */ - hasMoreUsers: boolean; + name: string; } /** - * Canonical directory where custom agents can be discovered or created, with scope, preference, and optional project path. + * One shipped agent's definition. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentDiscoveryPath". + * via the `definition` "AgentsGetBuiltinDefinitionResult". */ /** @experimental */ -export interface AgentDiscoveryPath { - /** - * Absolute path of the search/create directory (may not exist on disk yet) - */ - path: string; - scope: AgentDiscoveryPathScope; - /** - * Whether this is the canonical directory to create a new agent in its tier. At most one entry per tier is preferred. - */ - preferredForCreation: boolean; +/** @internal */ +export interface AgentsGetBuiltinDefinitionResult { /** - * The input project path this directory was derived from (only for project scope) + * The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. */ - projectPath?: string; + definitionJson: string; } /** - * Canonical locations where custom agents can be created so the runtime will recognize them. + * The shipped agent whose listing entry to load. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentDiscoveryPathList". + * via the `definition` "AgentsGetBuiltinListingDefinitionRequest". */ /** @experimental */ -export interface AgentDiscoveryPathList { +/** @internal */ +export interface AgentsGetBuiltinListingDefinitionRequest { /** - * Canonical agent create/discovery directories, in priority order + * The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. */ - paths: AgentDiscoveryPath[]; + name: string; } /** - * The currently selected custom agent, or null when using the default agent. + * One shipped agent, projected for a listing. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentGetCurrentResult". + * via the `definition` "AgentsGetBuiltinListingDefinitionResult". */ /** @experimental */ -export interface AgentGetCurrentResult { +/** @internal */ +export interface AgentsGetBuiltinListingDefinitionResult { /** - * Currently selected custom agent, or null if using the default agent + * The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. */ - agent?: AgentInfo | null; + definitionJson: string; } /** - * Agent metadata, including identifiers, display details, source, tools, model, models, reasoning effort, MCP servers, skills, and file path. + * The agents this runtime ships, named so a consumer can tell them apart from authored ones. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentInfo". + * via the `definition` "AgentsGetBuiltinsResult". */ /** @experimental */ -export interface AgentInfo { - /** - * Name of the agent. Use `id` as the stable selection identifier. - */ - name: string; - /** - * Human-readable display name - */ - displayName: string; - /** - * Description of the agent's purpose - */ - description: string; +/** @internal */ +export interface AgentsGetBuiltinsResult { /** - * Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. + * Every agent name this runtime ships. */ - path?: string; + names: string[]; /** - * Stable identifier for selection. For most agents this is the same as `name`; for plugin/builtin agents it may differ. Always populated; defaults to `name` when no distinct id was assigned. + * The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. */ - id: string; - source?: AgentInfoSource; + disableableNames: string[]; /** - * Whether the agent can be selected directly by the user. Agents marked `false` are subagent-only. + * The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. */ - userInvocable?: boolean; + yamlBasedNames: string[]; +} +/** + * Optional project paths to include when enumerating agent discovery directories. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentsGetDiscoveryPathsRequest". + */ +/** @experimental */ +export interface AgentsGetDiscoveryPathsRequest { /** - * Whether model-driven invocation is disabled for this agent. + * Optional list of project directory paths. When omitted or empty, only the user-level directory is returned. */ - disableModelInvocation?: boolean; + projectPaths?: string[]; /** - * Allowed tool names for this agent. Empty array means none; omitted means inherit defaults. + * When true, omit the host's user-level agent directory, leaving only project directories. For multitenant deployments (mirrors `discover`'s `excludeHostAgents`). */ - tools?: string[]; + excludeHostAgents?: boolean; +} +/** + * A provider offered for interactive login. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderDescriptor". + */ +/** @experimental */ +export interface ProviderDescriptor { + kind: LoginProviderKind; /** - * Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. + * Human-readable menu label, owned by the runtime so every consumer renders identical text. */ - model?: string; + label: string; /** - * Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + * Whether this provider is currently available to sign in with. */ - models?: string[]; - modelPolicy?: AgentModelPolicy; + available: boolean; +} +/** + * Credential-free authentication identity safe to expose to hosts and user interfaces. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthIdentity". + */ +/** @experimental */ +export interface AuthIdentity { + type: AuthInfoType; /** - * Authored reasoning effort for this agent. Applied on selection to models that support it; omitted means no authored preference. + * Authentication host */ - reasoningEffort?: string; + host: string; /** - * MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. - * - * @experimental + * Authenticated login, when available */ - mcpServers?: { - [k: string]: JsonValue | undefined; - }; + login?: string; /** - * Skill names preloaded into this agent's context. Omitted means none. + * Name of the environment variable that supplied the credential, when applicable */ - skills?: string[]; + envVar?: string; /** - * Authored base prompt for the agent. Runtime prompt assembly may add dynamic context at invocation time. Omitted from `session.agent.list` unless `includePrompt` is true. + * Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. */ - prompt?: string; + registrationId?: string; + copilotUser?: CopilotUserResponse; } /** - * Agents available to the session. + * Credential-free identity metadata. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentList". + * via the `definition` "AuthIdentityMetadata". */ /** @experimental */ -export interface AgentList { +export interface AuthIdentityMetadata { + type: AuthInfoType; /** - * Available agents + * Identity host. */ - agents: AgentInfo[]; + host: string; + /** + * User login. + */ + login: string; } /** - * Full registry entry for the spawned child. Lets the controller call `handleLiveTargetSelected(entry)` directly without re-reading the registry (avoids a TOCTOU window). + * A credential-free account choice after sign-in. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentRegistryLiveTargetEntry". + * via the `definition` "AuthLoginAccount". */ /** @experimental */ -export interface AgentRegistryLiveTargetEntry { +export interface AuthLoginAccount { /** - * Registry entry schema version (1 = ui-server, 2 = managed-server) + * Opaque identifier supplied to the next login step to select this account. */ - schemaVersion: number; - kind: AgentRegistryLiveTargetEntryKind; + selectionId: string; /** - * Operating-system pid of the process owning this entry + * Host coordinate owned by the selected account's provider. */ - pid: number; + host: string; /** - * Bind host for the entry's JSON-RPC server + * Human-readable login for the account choice. */ - host: string; + login: string; + kind: AccountKind; +} +/** + * Advance an in-flight login flow, optionally fulfilling an input-required step. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthLoginAdvanceRequest". + */ +/** @experimental */ +export interface AuthLoginAdvanceRequest { /** - * TCP port the entry's JSON-RPC server is listening on + * Opaque flow id from begin. */ - port: number; + flowId: string; /** - * Session ID of the foreground session for this entry + * Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. */ - sessionId?: string; + input?: string; +} +/** + * Begin an interactive login flow for a provider kind. Dispatch is kind-only. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthLoginBeginRequest". + */ +/** @experimental */ +export interface AuthLoginBeginRequest { + kind: LoginProviderKind; +} +/** + * A started login flow: its opaque id and first step. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthLoginBegun". + */ +/** @experimental */ +export interface AuthLoginBegun { /** - * Friendly session name (when set) + * Opaque flow id used to advance or cancel this login. */ - sessionName?: string; + flowId: string; + step: AuthLoginStep; +} +/** + * 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". + */ +/** @experimental */ +export interface AuthLoginResultDto { + status: AuthLoginResultStatus; /** - * Working directory of the session (when known) + * Host that was signed in, when completed. */ - cwd?: string; + host?: string; /** - * Git branch of the session (when known) + * Login that was signed in, when completed. */ - branch?: string; + login?: string; /** - * Model identifier currently selected for the session + * Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. */ - model?: string; - status?: AgentRegistryLiveTargetEntryStatus; - attentionKind?: AgentRegistryLiveTargetEntryAttentionKind; + accounts?: AuthLoginAccount[]; +} +/** + * Cancel an in-flight login flow. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthLoginCancelRequest". + */ +/** @experimental */ +export interface AuthLoginCancelRequest { /** - * Monotonic per-publisher revision counter incremented on every status update. Lets watchers detect transient flips. + * Opaque flow id from begin. */ - statusRevision?: number; - lastTerminalEvent?: AgentRegistryLiveTargetEntryLastTerminalEvent; + flowId: string; +} +/** + * Neutral authentication status summary. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthStatusDto". + */ +/** @experimental */ +export interface AuthStatusDto { /** - * ISO 8601 timestamp captured at registration + * Whether the session has resolved authentication. */ - startedAt: string; + isAuthenticated: boolean; /** - * Copilot CLI version that wrote the entry + * Active account login, if authenticated. */ - copilotVersion: string; + activeLogin?: string; /** - * Wall-clock milliseconds since the watcher last observed this entry (heartbeat freshness) + * Active account host, if authenticated. */ - lastSeenMs: number; + activeHost?: string; /** - * Connection token (null when the target is unauthenticated) - * - * @internal + * Copilot plan tier of the active account, if known. */ - token?: string | null; + copilotPlan?: string; + /** + * Number of signed-in accounts in the roster. + */ + accountCount: number; } /** - * Canonical process-log discovery outcome; populated from spawnLiveTarget. + * Validation error from an authentication attempt. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentRegistryLogCapture". + * via the `definition` "AuthValidationError". */ /** @experimental */ -export interface AgentRegistryLogCapture { +export interface AuthValidationError { /** - * Whether a canonical process log was discovered for this managed spawn + * Authentication validation error message */ - enabled: boolean; + message: string; + /** + * Optional message returned by GitHub + */ + githubMessage?: string; +} +/** + * Result of a non-interactive accounts mutation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthWriteResult". + */ +/** @experimental */ +export interface AuthWriteResult { /** - * Absolute path to the managed spawn's process--.log file (only set when enabled) + * Whether the mutation was applied. */ - path?: string; + ok: boolean; /** - * Why no canonical process log could be opened for this managed spawn (set only when enabled is false) + * For a logout, whether other signed-in accounts remain. */ - openError?: string; - openErrorReason?: AgentRegistryLogCaptureOpenErrorReason; + moreUsers?: boolean; } /** - * `child_process.spawn` itself failed before the child entered the registry. + * Current per-window credit limit and consumption for an autopilot objective. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentRegistrySpawnError". + * via the `definition` "AutopilotObjectiveCreditLimit". */ /** @experimental */ -export interface AgentRegistrySpawnError { +export interface AutopilotObjectiveCreditLimit { /** - * Discriminator: child_process.spawn itself failed + * Configured AI-credit cap, when one is set. */ - kind: "spawn-error"; + credits?: number; /** - * Human-readable error message + * Window consumption in fractional AI credits, for display. */ - message: string; + creditsUsed: number; /** - * Underlying errno code (e.g. ENOENT, EACCES) when available + * Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. */ - code?: string; + creditsUsedNanoAiu: string; } /** - * Spawn succeeded but the child did not publish a matching managed-server entry within the timeout. + * Canonical runtime state for the session's current autopilot objective. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentRegistrySpawnRegistryTimeout". + * via the `definition` "AutopilotObjectiveGetStateResult". */ /** @experimental */ -export interface AgentRegistrySpawnRegistryTimeout { - /** - * Discriminator: spawn succeeded but child never registered - */ - kind: "registry-timeout"; +export interface AutopilotObjectiveGetStateResult { /** - * Process ID of the orphaned child (so the caller can offer 'kill the pid' guidance) + * Current objective state, or `null` when the session has no objective. */ - childPid: number; - logCapture?: AgentRegistryLogCapture; + state: AutopilotObjectiveState | null; } /** - * Inputs to spawn a managed-server child via the controller's spawn delegate. + * Public, persistence-independent projection of an autopilot objective. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentRegistrySpawnRequest". + * via the `definition` "AutopilotObjectiveState". */ /** @experimental */ -export interface AgentRegistrySpawnRequest { +export interface AutopilotObjectiveState { /** - * Working directory for the spawned child (must be an existing directory) + * Session-local objective identifier. */ - cwd: string; + id: number; /** - * Custom or built-in agent name (e.g. 'explore'). When omitted, the child uses its own default. + * User-provided objective text. */ - agentName?: string; + objective: string; + status: AutopilotObjectiveStatus; /** - * Model identifier to apply to the new session + * Number of objective turns started. */ - model?: string; + turnCount: number; /** - * Friendly session name. Must satisfy validateSessionName: non-empty, no leading/trailing whitespace, <=100 chars, no control chars, no double quotes. + * Optional reason the objective is paused. */ - name?: string; - permissionMode?: AgentRegistrySpawnPermissionMode; + pauseReason?: string; /** - * Optional first user message. Forwarded to the caller (the CLI's spawn wrapper sends it post-attach via the standard LocalRpcSession.send path). + * Optional summary recorded when the objective completed. */ - initialPrompt?: string; + completionSummary?: string; + /** + * Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + */ + creditCountNanoAiu: string; + creditLimit?: AutopilotObjectiveCreditLimit; } /** - * Managed-server child was spawned and registered successfully. + * The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentRegistrySpawnSpawned". + * via the `definition` "BuiltInModelCatalog". */ /** @experimental */ -export interface AgentRegistrySpawnSpawned { - /** - * Discriminator: managed-server child spawned successfully - */ - kind: "spawned"; - entry: AgentRegistryLiveTargetEntry; +export interface BuiltInModelCatalog { /** - * Whether the delegate already sent the initial prompt. Always omitted in the current wiring: the controller sends the prompt post-attach via the standard LocalRpcSession.send path. + * Built-in model entries. */ - initialPromptSent?: boolean; + models: BuiltInModelCatalogEntry[]; +} +/** + * A well-known model in the runtime's built-in catalog. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "BuiltInModelCatalogEntry". + */ +/** @experimental */ +export interface BuiltInModelCatalogEntry { /** - * If the delegate attempted to send the initial prompt and failed, the categorized error message. + * Well-known runtime model ID suitable for provider or provider-model metadata. This is not necessarily the provider-facing deployment or model name and does not indicate CAPI entitlement or provider availability. */ - initialPromptError?: string; - logCapture?: AgentRegistryLogCapture; + id: string; } /** - * Synchronous pre-validation rejected the spawn request. + * Rust-owned metadata and input schema for a built-in tool. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentRegistrySpawnValidationError". + * via the `definition` "BuiltinToolDescriptor". */ /** @experimental */ -export interface AgentRegistrySpawnValidationError { +export interface BuiltinToolDescriptor { /** - * Discriminator: synchronous pre-validation rejected the request + * Stable name used to invoke the built-in tool. */ - kind: "validation-error"; - reason: AgentRegistrySpawnValidationErrorReason; - field?: AgentRegistrySpawnValidationErrorField; + name: string; /** - * Human-readable explanation; safe to surface in the UI banner. Never logged to unrestricted telemetry. + * Optional human-readable title for the tool. */ - message: string; + title: string | null; + /** + * Model-facing description of the tool's behavior. + */ + description: string; + /** + * JSON Schema for the tool input, or null when the tool uses a custom format. + */ + inputSchema: BuiltinToolInputSchema | null; + /** + * Optional supplemental usage instructions for the tool. + */ + instructions: string | null; + /** + * Optional tool category discriminator. + */ + type: string | null; + /** + * Optional custom input format used instead of a JSON Schema. + */ + format: BuiltinToolFormat | null; + safeForTelemetry: BuiltinToolSafeForTelemetry; + /** + * Whether the tool executes commands in a terminal. + */ + isTerminal: boolean; + /** + * Whether the tool provides a specialized intention summary. + */ + hasSummariseIntention: boolean; } /** - * Custom agents available to the session after reloading definitions from disk. + * JSON Schema object accepted by a built-in tool. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentReloadResult". + * via the `definition` "BuiltinToolInputSchema". */ /** @experimental */ -export interface AgentReloadResult { - /** - * Reloaded custom agents - */ - agents: AgentInfo[]; +export interface BuiltinToolInputSchema { + type: BuiltinToolInputSchemaType; + [k: string]: JsonValue | undefined; } /** - * Optional project paths to include in agent discovery. + * Custom grammar input format accepted by a built-in tool. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentsDiscoverRequest". + * via the `definition` "BuiltinToolFormat". */ /** @experimental */ -export interface AgentsDiscoverRequest { +export interface BuiltinToolFormat { + type: BuiltinToolFormatType; /** - * Optional list of project directory paths to scan for project-scoped agents. When omitted or empty, only user/plugin/remote-independent agents are returned (no project scan). + * Grammar syntax used by the format definition. */ - projectPaths?: string[]; + syntax: string; /** - * When true, omit the host's agents (the user-level agent directory and all plugin agents), leaving only project and remote agents. For multitenant deployments. + * Grammar definition accepted by the tool. */ - excludeHostAgents?: boolean; + definition: string; } /** - * Name of the custom agent to select for subsequent turns. + * Per-field telemetry-safety policy for a built-in tool. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentSelectRequest". + * via the `definition` "BuiltinToolSafeTelemetryFields". */ /** @experimental */ -export interface AgentSelectRequest { +export interface BuiltinToolSafeTelemetryFields { /** - * Name of the custom agent to select + * Whether the tool name may be included in telemetry without obfuscation. */ - name: string; + name?: boolean; + /** + * Whether tool input names may be included in telemetry without obfuscation. + */ + inputsNames?: boolean; } /** - * The newly selected custom agent. + * Cancellation result for a user-requested shell command. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentSelectResult". + * via the `definition` "CancelUserRequestedShellCommandResult". */ /** @experimental */ -export interface AgentSelectResult { - agent: AgentInfo; +export interface CancelUserRequestedShellCommandResult { + /** + * Whether an in-flight execution was found and signalled to cancel + */ + cancelled: boolean; } /** - * An in-memory authored prompt override for an available agent. + * Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentSetPromptRequest". + * via the `definition` "CanvasAction". */ /** @experimental */ -export interface AgentSetPromptRequest { +export interface CanvasAction { /** - * Stable effective agent id. Plugin namespace separators are normalized. + * Action name exposed by the canvas provider */ - id: string; + name: string; /** - * Replacement authored prompt. Empty text is valid. + * Description of the action */ - prompt: string; + description?: string; + inputSchema?: CanvasJsonSchema; } /** - * Optional project paths to include when enumerating agent discovery directories. + * Canvas action invocation parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AgentsGetDiscoveryPathsRequest". + * via the `definition` "CanvasActionInvokeRequest". */ /** @experimental */ -export interface AgentsGetDiscoveryPathsRequest { +export interface CanvasActionInvokeRequest { /** - * Optional list of project directory paths. When omitted or empty, only the user-level directory is returned. + * Open canvas instance identifier */ - projectPaths?: string[]; + instanceId: string; /** - * When true, omit the host's user-level agent directory, leaving only project directories. For multitenant deployments (mirrors `discover`'s `excludeHostAgents`). + * Action name to invoke */ - excludeHostAgents?: boolean; + actionName: string; + /** + * Action input + */ + input?: JsonValue; } /** - * Credential-free authentication identity safe to expose to hosts and user interfaces. + * Canvas close parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AuthIdentity". + * via the `definition` "CanvasCloseRequest". */ /** @experimental */ -export interface AuthIdentity { - type: AuthInfoType; - /** - * Authentication host - */ - host: string; - /** - * Authenticated login, when available - */ - login?: string; - /** - * Name of the environment variable that supplied the credential, when applicable - */ - envVar?: string; +export interface CanvasCloseRequest { /** - * Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. - */ - registrationId?: string; - copilotUser?: CopilotUserResponse; + * Open canvas instance identifier + */ + instanceId: string; } /** - * Validation error from an authentication attempt. + * Host context supplied by the runtime. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AuthValidationError". + * via the `definition` "CanvasHostContext". */ /** @experimental */ -export interface AuthValidationError { - /** - * Authentication validation error message - */ - message: string; - /** - * Optional message returned by GitHub - */ - githubMessage?: string; +export interface CanvasHostContext { + capabilities?: CanvasHostContextCapabilities; } /** - * Current per-window credit limit and consumption for an autopilot objective. + * Host capabilities * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AutopilotObjectiveCreditLimit". + * via the `definition` "CanvasHostContextCapabilities". */ /** @experimental */ -export interface AutopilotObjectiveCreditLimit { - /** - * Configured AI-credit cap, when one is set. - */ - credits?: number; - /** - * Window consumption in fractional AI credits, for display. - */ - creditsUsed: number; +export interface CanvasHostContextCapabilities { /** - * Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + * Whether canvas rendering is supported */ - creditsUsedNanoAiu: string; + canvases?: boolean; } /** - * Canonical runtime state for the session's current autopilot objective. + * Declared canvases available in this session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AutopilotObjectiveGetStateResult". + * via the `definition` "CanvasList". */ /** @experimental */ -export interface AutopilotObjectiveGetStateResult { +export interface CanvasList { /** - * Current objective state, or `null` when the session has no objective. + * Declared canvases available in this session */ - state: AutopilotObjectiveState | null; + canvases: DiscoveredCanvas[]; } /** - * Public, persistence-independent projection of an autopilot objective. + * Canvas available in the current session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "AutopilotObjectiveState". + * via the `definition` "DiscoveredCanvas". */ /** @experimental */ -export interface AutopilotObjectiveState { +export interface DiscoveredCanvas { /** - * Session-local objective identifier. + * Human-readable canvas name */ - id: number; + displayName: string; /** - * User-provided objective text. + * Short, single-sentence description shown to the agent in canvas catalogs. */ - objective: string; - status: AutopilotObjectiveStatus; + description: string; /** - * Number of objective turns started. + * Host-local PNG path for the canvas icon, when supplied */ - turnCount: number; + icon?: string; + inputSchema?: CanvasJsonSchema; /** - * Optional reason the objective is paused. + * Actions the agent or host may invoke on an open instance */ - pauseReason?: string; + actions?: CanvasAction[]; /** - * Optional summary recorded when the objective completed. + * Owning provider identifier */ - completionSummary?: string; + extensionId: string; /** - * Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + * Owning extension display name, when available */ - creditCountNanoAiu: string; - creditLimit?: AutopilotObjectiveCreditLimit; -} -/** - * The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "BuiltInModelCatalog". - */ -/** @experimental */ -export interface BuiltInModelCatalog { + extensionName?: string; /** - * Built-in model entries. + * Provider-local canvas identifier */ - models: BuiltInModelCatalogEntry[]; + canvasId: string; } /** - * A well-known model in the runtime's built-in catalog. + * Live open-canvas snapshot. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "BuiltInModelCatalogEntry". + * via the `definition` "CanvasListOpenResult". */ /** @experimental */ -export interface BuiltInModelCatalogEntry { +export interface CanvasListOpenResult { /** - * Well-known runtime model ID suitable for provider or provider-model metadata. This is not necessarily the provider-facing deployment or model name and does not indicate CAPI entitlement or provider availability. + * Currently open canvas instances */ - id: string; + openCanvases: OpenCanvasInstance[]; } /** - * Rust-owned metadata and input schema for a built-in tool. + * Open canvas instance snapshot. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "BuiltinToolDescriptor". + * via the `definition` "OpenCanvasInstance". */ /** @experimental */ -export interface BuiltinToolDescriptor { +export interface OpenCanvasInstance { /** - * Stable name used to invoke the built-in tool. + * Stable caller-supplied canvas instance identifier */ - name: string; + instanceId: string; /** - * Optional human-readable title for the tool. + * Owning provider identifier */ - title: string | null; + extensionId: string; /** - * Model-facing description of the tool's behavior. + * Owning extension display name, when available */ - description: string; + extensionName?: string; /** - * JSON Schema for the tool input, or null when the tool uses a custom format. + * Provider-local canvas identifier */ - inputSchema: BuiltinToolInputSchema | null; + canvasId: string; /** - * Optional supplemental usage instructions for the tool. + * Host-local PNG path for the canvas icon, when supplied */ - instructions: string | null; + icon?: string; /** - * Optional tool category discriminator. + * Rendered title */ - type: string | null; + title?: string; /** - * Optional custom input format used instead of a JSON Schema. + * Provider-supplied status text */ - format: BuiltinToolFormat | null; - safeForTelemetry: BuiltinToolSafeForTelemetry; + status?: string; /** - * Whether the tool executes commands in a terminal. + * URL for web-rendered canvases */ - isTerminal: boolean; + url?: string; /** - * Whether the tool provides a specialized intention summary. + * Input supplied when the instance was opened */ - hasSummariseIntention: boolean; + input?: JsonValue; } /** - * JSON Schema object accepted by a built-in tool. + * Canvas open parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "BuiltinToolInputSchema". + * via the `definition` "CanvasOpenRequest". */ /** @experimental */ -export interface BuiltinToolInputSchema { - type: BuiltinToolInputSchemaType; - [k: string]: JsonValue | undefined; +export interface CanvasOpenRequest { + /** + * Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. + */ + extensionId?: string; + /** + * Provider-local canvas identifier + */ + canvasId: string; + /** + * Caller-supplied stable instance identifier + */ + instanceId: string; + /** + * Canvas open input + */ + input?: JsonValue; } /** - * Custom grammar input format accepted by a built-in tool. + * Canvas close parameters sent to the provider. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "BuiltinToolFormat". + * via the `definition` "CanvasProviderCloseRequest". */ /** @experimental */ -export interface BuiltinToolFormat { - type: BuiltinToolFormatType; +export interface CanvasProviderCloseRequest { /** - * Grammar syntax used by the format definition. + * Target session identifier */ - syntax: string; + sessionId: string; /** - * Grammar definition accepted by the tool. + * Owning provider identifier */ - definition: string; + extensionId: string; + /** + * Provider-local canvas identifier + */ + canvasId: string; + /** + * Canvas instance identifier + */ + instanceId: string; + host?: CanvasHostContext; + session?: CanvasSessionContext; } /** - * Per-field telemetry-safety policy for a built-in tool. + * Session context supplied by the runtime. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "BuiltinToolSafeTelemetryFields". + * via the `definition` "CanvasSessionContext". */ /** @experimental */ -export interface BuiltinToolSafeTelemetryFields { - /** - * Whether the tool name may be included in telemetry without obfuscation. - */ - name?: boolean; +export interface CanvasSessionContext { /** - * Whether tool input names may be included in telemetry without obfuscation. + * Active session working directory, when known. */ - inputsNames?: boolean; + workingDirectory?: string; } /** - * Cancellation result for a user-requested shell command. + * Canvas action invocation parameters sent to the provider. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CancelUserRequestedShellCommandResult". + * via the `definition` "CanvasProviderInvokeActionRequest". */ /** @experimental */ -export interface CancelUserRequestedShellCommandResult { +export interface CanvasProviderInvokeActionRequest { /** - * Whether an in-flight execution was found and signalled to cancel + * Target session identifier */ - cancelled: boolean; + sessionId: string; + /** + * Owning provider identifier + */ + extensionId: string; + /** + * Provider-local canvas identifier + */ + canvasId: string; + /** + * Canvas instance identifier + */ + instanceId: string; + /** + * Action name to invoke + */ + actionName: string; + /** + * Action input + */ + input?: JsonValue; + host?: CanvasHostContext; + session?: CanvasSessionContext; } /** - * Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. + * Canvas open parameters sent to the provider. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasAction". + * via the `definition` "CanvasProviderOpenRequest". */ /** @experimental */ -export interface CanvasAction { +export interface CanvasProviderOpenRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Owning provider identifier + */ + extensionId: string; + /** + * Provider-local canvas identifier + */ + canvasId: string; /** - * Action name exposed by the canvas provider + * Stable caller-supplied canvas instance identifier */ - name: string; + instanceId: string; /** - * Description of the action + * Canvas open input */ - description?: string; - inputSchema?: CanvasJsonSchema; + input?: JsonValue; + host?: CanvasHostContext; + session?: CanvasSessionContext; } /** - * Canvas action invocation parameters. + * Canvas open result returned by the provider. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasActionInvokeRequest". + * via the `definition` "CanvasProviderOpenResult". */ /** @experimental */ -export interface CanvasActionInvokeRequest { +export interface CanvasProviderOpenResult { /** - * Open canvas instance identifier + * URL for web-rendered canvases */ - instanceId: string; + url?: string; /** - * Action name to invoke + * Provider-supplied title */ - actionName: string; + title?: string; /** - * Action input + * Provider-supplied status text */ - input?: JsonValue; + status?: string; } /** - * Canvas close parameters. + * Internal canvas provider registration parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasCloseRequest". + * via the `definition` "CanvasProviderRegisterRequest". */ /** @experimental */ -export interface CanvasCloseRequest { +export interface CanvasProviderRegisterRequest { /** - * Open canvas instance identifier + * Connection identifier for callback routing */ - instanceId: string; + connectionId: string; + /** + * Provider metadata supplied by the host + */ + info: JsonValue; + /** + * Canvas contributions supplied by the provider + */ + canvases: JsonValue[]; } /** - * Host context supplied by the runtime. + * Internal canvas provider unregistration parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasHostContext". + * via the `definition` "CanvasProviderUnregisterRequest". */ /** @experimental */ -export interface CanvasHostContext { - capabilities?: CanvasHostContextCapabilities; +export interface CanvasProviderUnregisterRequest { + /** + * Connection identifier to unregister + */ + connectionId: string; } /** - * Host capabilities + * Options scoped to the built-in CAPI (Copilot API) provider. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasHostContextCapabilities". + * via the `definition` "CapiSessionOptions". */ /** @experimental */ -export interface CanvasHostContextCapabilities { +export interface CapiSessionOptions { + autoTier?: AutoTier; /** - * Whether canvas rendering is supported + * 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. */ - canvases?: boolean; + enableWebSocketResponses?: boolean; } /** - * Declared canvases available in this session. + * Semantic digest of a strictly parsed and schema-validated JSON MCP card. Both URL-backed and embedded cards are canonicalised with RFC 8785 JSON Canonicalization Scheme, encoded as UTF-8, and hashed with SHA-256. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasList". + * via the `definition` "CardDigest". */ /** @experimental */ -export interface CanvasList { - /** - * Declared canvases available in this session - */ - canvases: DiscoveredCanvas[]; +export interface CardDigest { + algorithm: CardDigestAlgorithm; + value: CardDigestValue; } /** - * Canvas available in the current session. + * An inert Agent Plugin catalog result. Its canonical catalog identity, declared version, repository source claim, and explicit compatibility tags are safe to correlate, while its descriptor, URL, raw data, and installed-plugin state remain runtime-private. This contract-only variant does not mint or expose a candidate handle. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiscoveredCanvas". + * via the `definition` "CatalogAgentPluginCandidate". */ /** @experimental */ -export interface DiscoveredCanvas { +export interface CatalogAgentPluginCandidate { + kind: CatalogAgentPluginCandidateKind; + mediaType: CatalogAgentPluginMediaType; + identity: CatalogResourceIdentity; + version?: CatalogResourceVersion; /** - * Human-readable canvas name + * Display name taken verbatim from the card. Inert untrusted text. */ displayName: string; /** - * Short, single-sentence description shown to the agent in canvas catalogs. - */ - description: string; - /** - * Host-local PNG path for the canvas icon, when supplied - */ - icon?: string; - inputSchema?: CanvasJsonSchema; - /** - * Actions the agent or host may invoke on an open instance - */ - actions?: CanvasAction[]; - /** - * Owning provider identifier + * Description taken verbatim from the card. Inert untrusted text. */ - extensionId: string; + description?: string; /** - * Owning extension display name, when available + * Publisher taken verbatim from the card. Inert untrusted text. */ - extensionName?: string; + publisher?: string; + source: CatalogPluginRepositorySource; /** - * Provider-local canvas identifier + * Explicit validated compatibility tags, in canonical order. An empty list means the source declared no recognised compatibility; clients must not infer compatibility from other fields. `canvas-only` requires both `canvas` and `github-copilot`. + * + * @maxItems 3 */ - canvasId: string; + compatibilityTags: + | ({ + [k: string]: unknown | undefined; + } & []) + | [CatalogAgentPluginCompatibilityTag] + | [CatalogAgentPluginCompatibilityTag, CatalogAgentPluginCompatibilityTag] + | [CatalogAgentPluginCompatibilityTag, CatalogAgentPluginCompatibilityTag, CatalogAgentPluginCompatibilityTag]; + provenance: CatalogAgentPluginCandidateProvenance; + trust?: CatalogTrustSnapshot; } /** - * Live open-canvas snapshot. + * Syntactically validated GitHub repository provenance declared by catalog metadata. This is a source claim rather than proof that the descriptor URL resolves to the repository. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasListOpenResult". + * via the `definition` "CatalogPluginRepositorySource". */ /** @experimental */ -export interface CanvasListOpenResult { +export interface CatalogPluginRepositorySource { /** - * Currently open canvas instances + * Canonical lowercase owner/repository name derived from metadata.sourceSet. */ - openCanvases: OpenCanvasInstance[]; + repository: string; + /** + * Case-preserving safe relative POSIX path derived from metadata.repoPath. + */ + path: string; } /** - * Open canvas instance snapshot. + * Where and when an Agent Plugin catalog reference was observed. Discovery provenance deliberately carries no descriptor URL, raw data, candidate handle, or content digest. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "OpenCanvasInstance". + * via the `definition` "CatalogAgentPluginCandidateProvenance". */ /** @experimental */ -export interface OpenCanvasInstance { - /** - * Stable caller-supplied canvas instance identifier - */ - instanceId: string; - /** - * Owning provider identifier - */ - extensionId: string; - /** - * Owning extension display name, when available - */ - extensionName?: string; - /** - * Provider-local canvas identifier - */ - canvasId: string; - /** - * Host-local PNG path for the canvas icon, when supplied - */ - icon?: string; - /** - * Rendered title - */ - title?: string; - /** - * Provider-supplied status text - */ - status?: string; +export interface CatalogAgentPluginCandidateProvenance { /** - * URL for web-rendered canvases + * Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */ - url?: string; + authority: string; /** - * Input supplied when the instance was opened + * ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */ - input?: JsonValue; + observedAt: string; + mediaType: CatalogAgentPluginMediaType; } /** - * Canvas open parameters. + * A recognised current Agent Finder T1 or T2 trust tier. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasOpenRequest". + * via the `definition` "CatalogTrustSnapshotCurrent". */ /** @experimental */ -export interface CanvasOpenRequest { - /** - * Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. - */ - extensionId?: string; - /** - * Provider-local canvas identifier - */ - canvasId: string; - /** - * Caller-supplied stable instance identifier - */ - instanceId: string; +export interface CatalogTrustSnapshotCurrent { + schemaVersion: CatalogTrustSnapshotSchemaVersion; + status: CatalogTrustSnapshotCurrentStatus; + tier: CatalogTrustTier; + eligibility: CatalogTrustEligibility; + provenance: CatalogTrustProvenance; +} +/** + * Where and when the runtime observed the trust metadata. Observation time is not the authority's evaluation time and must not be used to infer staleness. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogTrustProvenance". + */ +/** @experimental */ +export interface CatalogTrustProvenance { + source: CatalogTrustSource; /** - * Canvas open input + * ISO 8601 timestamp with a timezone offset at which the runtime observed the search result carrying this trust field. */ - input?: JsonValue; + observedAt: string; +} +/** + * Discriminator: the authority omitted trust metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogTrustSnapshotAbsent". + */ +/** @experimental */ +export interface CatalogTrustSnapshotAbsent { + schemaVersion: CatalogTrustSnapshotSchemaVersion; + status: CatalogTrustSnapshotAbsentStatus; + eligibility: CatalogTrustEligibility; + provenance: CatalogTrustProvenance; +} +/** + * Discriminator: the authority explicitly marked the assessment stale. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogTrustSnapshotStale". + */ +/** @experimental */ +export interface CatalogTrustSnapshotStale { + schemaVersion: CatalogTrustSnapshotSchemaVersion; + status: CatalogTrustSnapshotStaleStatus; + eligibility: CatalogTrustEligibility; + provenance: CatalogTrustProvenance; +} +/** + * Discriminator: the authority explicitly reported a downgraded assessment. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogTrustSnapshotDowngraded". + */ +/** @experimental */ +export interface CatalogTrustSnapshotDowngraded { + schemaVersion: CatalogTrustSnapshotSchemaVersion; + status: CatalogTrustSnapshotDowngradedStatus; + eligibility: CatalogTrustEligibility; + provenance: CatalogTrustProvenance; } /** - * Canvas close parameters sent to the provider. + * Discriminator: the authority explicitly revoked the assessment. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasProviderCloseRequest". + * via the `definition` "CatalogTrustSnapshotRevoked". */ /** @experimental */ -export interface CanvasProviderCloseRequest { - /** - * Target session identifier - */ - sessionId: string; - /** - * Owning provider identifier - */ - extensionId: string; - /** - * Provider-local canvas identifier - */ - canvasId: string; - /** - * Canvas instance identifier - */ - instanceId: string; - host?: CanvasHostContext; - session?: CanvasSessionContext; +export interface CatalogTrustSnapshotRevoked { + schemaVersion: CatalogTrustSnapshotSchemaVersion; + status: CatalogTrustSnapshotRevokedStatus; + eligibility: CatalogTrustEligibility; + provenance: CatalogTrustProvenance; } /** - * Session context supplied by the runtime. + * Discriminator: the authority supplied a bounded trust value this runtime does not understand. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasSessionContext". + * via the `definition` "CatalogTrustSnapshotUnsupported". */ /** @experimental */ -export interface CanvasSessionContext { - /** - * Active session working directory, when known. - */ - workingDirectory?: string; +export interface CatalogTrustSnapshotUnsupported { + schemaVersion: CatalogTrustSnapshotSchemaVersion; + status: CatalogTrustSnapshotUnsupportedStatus; + eligibility: CatalogTrustEligibility; + provenance: CatalogTrustProvenance; } /** - * Canvas action invocation parameters sent to the provider. + * Discriminator: the trust field was empty, unbounded, or had the wrong JSON type. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasProviderInvokeActionRequest". + * via the `definition` "CatalogTrustSnapshotMalformed". */ /** @experimental */ -export interface CanvasProviderInvokeActionRequest { - /** - * Target session identifier - */ - sessionId: string; - /** - * Owning provider identifier - */ - extensionId: string; - /** - * Provider-local canvas identifier - */ - canvasId: string; - /** - * Canvas instance identifier - */ - instanceId: string; - /** - * Action name to invoke - */ - actionName: string; - /** - * Action input - */ - input?: JsonValue; - host?: CanvasHostContext; - session?: CanvasSessionContext; +export interface CatalogTrustSnapshotMalformed { + schemaVersion: CatalogTrustSnapshotSchemaVersion; + status: CatalogTrustSnapshotMalformedStatus; + eligibility: CatalogTrustEligibility; + provenance: CatalogTrustProvenance; } /** - * Canvas open parameters sent to the provider. + * An inert AI skill catalog result. Verified Skill candidates may be installable only when the runtime reports installability and the selected session is permitted to plan installation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasProviderOpenRequest". + * via the `definition` "CatalogAiSkillCandidate". */ /** @experimental */ -export interface CanvasProviderOpenRequest { +export interface CatalogAiSkillCandidate { /** - * Target session identifier + * Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. */ - sessionId: string; + handle: string; /** - * Owning provider identifier + * ISO 8601 timestamp after which the handle is stale and will be rejected. */ - extensionId: string; + handleExpiresAt: string; + kind: CatalogAiSkillCandidateKind; + mediaType: CatalogAiSkillMediaType; + installability: CatalogAiSkillInstallability; /** - * Provider-local canvas identifier + * Display name taken verbatim from the card. Inert untrusted text. */ - canvasId: string; + displayName: string; /** - * Stable caller-supplied canvas instance identifier + * Description taken verbatim from the card. Inert untrusted text. */ - instanceId: string; + description?: string; /** - * Canvas open input + * Publisher taken verbatim from the card. Inert untrusted text. */ - input?: JsonValue; - host?: CanvasHostContext; - session?: CanvasSessionContext; + publisher?: string; + source: CatalogCandidateSource; + provenance: CatalogAiSkillCandidateProvenance; + trust?: CatalogTrustSnapshot; } /** - * Canvas open result returned by the provider. + * Candidate whose card is retrieved from a URL through the runtime's hardened fetch boundary. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasProviderOpenResult". + * via the `definition` "CatalogCandidateSourceUrl". */ /** @experimental */ -export interface CanvasProviderOpenResult { - /** - * URL for web-rendered canvases - */ - url?: string; +export interface CatalogCandidateSourceUrl { /** - * Provider-supplied title + * Discriminator: the card is URL-backed, and carries no embedded data */ - title?: string; + kind: "url"; /** - * Provider-supplied status text + * Card URL as advertised. Inert untrusted data: the runtime retrieves it only through its own hardened boundary, and it is never logged. */ - status?: string; + url: string; } /** - * Internal canvas provider registration parameters. + * Candidate whose card reference arrived inline. The document and its content-derived properties stay behind the runtime boundary. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasProviderRegisterRequest". + * via the `definition` "CatalogCandidateSourceEmbedded". */ /** @experimental */ -export interface CanvasProviderRegisterRequest { - /** - * Connection identifier for callback routing - */ - connectionId: string; - /** - * Provider metadata supplied by the host - */ - info: JsonValue; +export interface CatalogCandidateSourceEmbedded { /** - * Canvas contributions supplied by the provider + * Discriminator: the card is embedded, and carries no URL */ - canvases: JsonValue[]; + kind: "embedded"; } /** - * Internal canvas provider unregistration parameters. + * Where and when an AI skill catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CanvasProviderUnregisterRequest". + * via the `definition` "CatalogAiSkillCandidateProvenance". */ /** @experimental */ -export interface CanvasProviderUnregisterRequest { +export interface CatalogAiSkillCandidateProvenance { /** - * Connection identifier to unregister + * Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */ - connectionId: string; -} -/** - * Options scoped to the built-in CAPI (Copilot API) provider. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CapiSessionOptions". - */ -/** @experimental */ -export interface CapiSessionOptions { - autoTier?: AutoTier; + authority: string; /** - * 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. + * ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */ - enableWebSocketResponses?: boolean; + observedAt: string; + mediaType: CatalogAiSkillMediaType; } /** - * Semantic digest of a strictly parsed and schema-validated JSON MCP card. Both URL-backed and embedded cards are canonicalised with RFC 8785 JSON Canonicalization Scheme, encoded as UTF-8, and hashed with SHA-256. + * An optional catalog authentication exchange did not establish the caller's identity. Anonymous search remains supported; this refusal is reserved for an operation that cannot continue after the attempted exchange. It is distinct from `policy-rejected` and from a network failure, and the reason identifies the recovery action. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CardDigest". + * via the `definition` "CatalogAuthenticationRequiredError". */ /** @experimental */ -export interface CardDigest { - algorithm: CardDigestAlgorithm; - value: CardDigestValue; +export interface CatalogAuthenticationRequiredError { + /** + * Discriminator: the caller is not authenticated + */ + kind: "authentication-required"; + reason: CatalogAuthenticationRequiredReason; + /** + * Human-readable explanation, safe to surface. Never contains a credential or token, nor a query, URL, handle, or secret. + */ + message: string; } /** - * An inert Agent Plugin catalog result. Its canonical catalog identity, declared version, repository source claim, and explicit compatibility tags are safe to correlate, while its descriptor, URL, raw data, and installed-plugin state remain runtime-private. This contract-only variant does not mint or expose a candidate handle. + * An inert MCP server catalog result. Every free-text field is untrusted external data and must never be treated as an instruction, and the handle is the only way to refer to the candidate in a later operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogAgentPluginCandidate". + * via the `definition` "CatalogMcpServerCandidate". */ /** @experimental */ -export interface CatalogAgentPluginCandidate { - kind: CatalogAgentPluginCandidateKind; - mediaType: CatalogAgentPluginMediaType; - identity: CatalogResourceIdentity; - version?: CatalogResourceVersion; +export interface CatalogMcpServerCandidate { + /** + * Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. + */ + handle: string; + /** + * ISO 8601 timestamp after which the handle is stale and will be rejected. + */ + handleExpiresAt: string; + kind: CatalogMcpServerCandidateKind; + mediaType: McpServerCardMediaType; + installability: CatalogMcpServerInstallability; /** * Display name taken verbatim from the card. Inert untrusted text. */ @@ -6799,2264 +8783,2563 @@ export interface CatalogAgentPluginCandidate { * Publisher taken verbatim from the card. Inert untrusted text. */ publisher?: string; - source: CatalogPluginRepositorySource; + source: CatalogCandidateSource; + provenance: CatalogMcpServerCandidateProvenance; + trust?: CatalogTrustSnapshot; +} +/** + * Where and when an MCP server catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogMcpServerCandidateProvenance". + */ +/** @experimental */ +export interface CatalogMcpServerCandidateProvenance { /** - * Explicit validated compatibility tags, in canonical order. An empty list means the source declared no recognised compatibility; clients must not infer compatibility from other fields. `canvas-only` requires both `canvas` and `github-copilot`. + * Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + */ + authority: string; + /** + * ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + */ + observedAt: string; + mediaType: McpServerCardMediaType; +} +/** + * The protocol version and capability set a caller requires, supplied on every catalog request so negotiation cannot be skipped by omission. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogClientContract". + */ +/** @experimental */ +export interface CatalogClientContract { + /** + * SDK protocol version the caller was generated against. A caller below the runtime's minimum supported version is refused rather than served a partial result. + */ + protocolVersion: number; + /** + * Wire features the caller requires the runtime to understand. Identifiers are bounded but extensible so a newer caller can negotiate with an older runtime. Requiring an unknown feature yields a typed refusal listing what is understood, never a partial grant. A grant does not promise that a deployment has enabled the operation; typed unavailable results report that separately. * - * @maxItems 3 + * @maxItems 32 */ - compatibilityTags: - | ({ - [k: string]: unknown | undefined; - } & []) - | [CatalogAgentPluginCompatibilityTag] - | [CatalogAgentPluginCompatibilityTag, CatalogAgentPluginCompatibilityTag] - | [CatalogAgentPluginCompatibilityTag, CatalogAgentPluginCompatibilityTag, CatalogAgentPluginCompatibilityTag]; - provenance: CatalogAgentPluginCandidateProvenance; - trust?: CatalogTrustSnapshot; + requiredCapabilities: CatalogCapabilityId[]; } /** - * Syntactically validated GitHub repository provenance declared by catalog metadata. This is a source claim rather than proof that the descriptor URL resolves to the repository. + * An upstream catalog response broke the wire contract. Most importantly, every result must carry exactly one of a URL or embedded data: a result carrying both, or neither, is refused here rather than being guessed at. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogPluginRepositorySource". + * via the `definition` "CatalogContractViolationError". */ /** @experimental */ -export interface CatalogPluginRepositorySource { +export interface CatalogContractViolationError { /** - * Canonical lowercase owner/repository name derived from metadata.sourceSet. + * Discriminator: the upstream response broke the contract */ - repository: string; + kind: "contract-violation"; + reason: CatalogContractViolationReason; /** - * Case-preserving safe relative POSIX path derived from metadata.repoPath. + * Human-readable explanation, safe to surface. Never echoes response content, nor a query, URL, handle, or secret. */ - path: string; + message: string; } /** - * Where and when an Agent Plugin catalog reference was observed. Discovery provenance deliberately carries no descriptor URL, raw data, candidate handle, or content digest. + * A presented handle was not accepted. Handles are runtime-instance scoped, TTL-bound, and single-use, so each way of failing is reported distinctly. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogAgentPluginCandidateProvenance". + * via the `definition` "CatalogHandleRejectedError". */ /** @experimental */ -export interface CatalogAgentPluginCandidateProvenance { +export interface CatalogHandleRejectedError { /** - * Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + * Discriminator: a handle was rejected */ - authority: string; + kind: "handle-rejected"; + handleType: CatalogHandleType; + reason: CatalogHandleRejectionReason; /** - * ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + * Human-readable explanation, safe to surface. Never contains the handle itself, nor a query, URL, or secret. */ - observedAt: string; - mediaType: CatalogAgentPluginMediaType; + message: string; } /** - * A recognised current Agent Finder T1 or T2 trust tier. + * The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustSnapshotCurrent". + * via the `definition` "CatalogInvalidRequestError". */ /** @experimental */ -export interface CatalogTrustSnapshotCurrent { - schemaVersion: CatalogTrustSnapshotSchemaVersion; - status: CatalogTrustSnapshotCurrentStatus; - tier: CatalogTrustTier; - eligibility: CatalogTrustEligibility; - provenance: CatalogTrustProvenance; +export interface CatalogInvalidRequestError { + /** + * Discriminator: the request itself was invalid + */ + kind: "invalid-request"; + field: CatalogInvalidRequestField; + /** + * Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. + */ + message: string; } /** - * Where and when the runtime observed the trust metadata. Observation time is not the authority's evaluation time and must not be used to infer staleness. + * A card could not be parsed or did not satisfy its declared media type's schema. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustProvenance". + * via the `definition` "CatalogMalformedCardError". */ /** @experimental */ -export interface CatalogTrustProvenance { - source: CatalogTrustSource; +export interface CatalogMalformedCardError { /** - * ISO 8601 timestamp with a timezone offset at which the runtime observed the search result carrying this trust field. + * Discriminator: the card was malformed */ - observedAt: string; + kind: "malformed-card"; + reason: CatalogMalformedCardReason; + mediaType?: CatalogMediaType; + /** + * Human-readable explanation, safe to surface. Never echoes card content, nor a query, URL, handle, or secret. + */ + message: string; } /** - * Discriminator: the authority omitted trust metadata. + * The protocol version and capability set the runtime actually honoured for a successful catalog operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustSnapshotAbsent". + * via the `definition` "CatalogNegotiatedContract". */ /** @experimental */ -export interface CatalogTrustSnapshotAbsent { - schemaVersion: CatalogTrustSnapshotSchemaVersion; - status: CatalogTrustSnapshotAbsentStatus; - eligibility: CatalogTrustEligibility; - provenance: CatalogTrustProvenance; +export interface CatalogNegotiatedContract { + /** + * Protocol version of the runtime that served the request. + */ + runtimeProtocolVersion: number; + /** + * Wire features the runtime understood for this operation. Includes the five original catalog capabilities and only explicitly requested supported additions, in supported order without duplicates. Capabilities that introduce new success-union variants or operations are therefore included only when explicitly required, preserving older protocol-v3 clients. Always a superset of the caller's required features, because any shortfall is a refusal instead. Operation availability remains a separate typed result. + */ + grantedCapabilities: CatalogCapability[]; } /** - * Discriminator: the authority explicitly marked the assessment stale. + * The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustSnapshotStale". + * via the `definition` "CatalogNegotiationRefusedError". */ /** @experimental */ -export interface CatalogTrustSnapshotStale { - schemaVersion: CatalogTrustSnapshotSchemaVersion; - status: CatalogTrustSnapshotStaleStatus; - eligibility: CatalogTrustEligibility; - provenance: CatalogTrustProvenance; +export interface CatalogNegotiationRefusedError { + /** + * Discriminator: capability or protocol-version negotiation failed + */ + kind: "negotiation-refused"; + reason: CatalogNegotiationRefusedReason; + /** + * Protocol version of the runtime that refused the request. + */ + runtimeProtocolVersion: number; + /** + * Lowest caller protocol version this runtime will serve. + */ + minimumSupportedProtocolVersion: number; + /** + * Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. + * + * @maxItems 32 + */ + supportedCapabilities: CatalogCapabilityId[]; + /** + * The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. + * + * @maxItems 32 + */ + unsupportedCapabilities: CatalogCapabilityId[]; + /** + * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + */ + message: string; } /** - * Discriminator: the authority explicitly reported a downgraded assessment. + * The runtime could not reach the catalog authority or retrieve a card. Covers being offline as well as transport-level failure. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustSnapshotDowngraded". + * via the `definition` "CatalogNetworkFailureError". */ /** @experimental */ -export interface CatalogTrustSnapshotDowngraded { - schemaVersion: CatalogTrustSnapshotSchemaVersion; - status: CatalogTrustSnapshotDowngradedStatus; - eligibility: CatalogTrustEligibility; - provenance: CatalogTrustProvenance; +export interface CatalogNetworkFailureError { + /** + * Discriminator: the network operation failed + */ + kind: "network-failure"; + reason: CatalogNetworkFailureReason; + /** + * HTTP status code, when the failure was a rejected response. + */ + statusCode?: number; + /** + * Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + */ + retryAfterSeconds?: number; + /** + * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + */ + message: string; } /** - * Discriminator: the authority explicitly revoked the assessment. + * The candidate is discoverable but cannot be installed. `application/ai-skill` resolves here, because it stays searchable while remaining typed non-installable. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustSnapshotRevoked". + * via the `definition` "CatalogNotInstallableError". */ /** @experimental */ -export interface CatalogTrustSnapshotRevoked { - schemaVersion: CatalogTrustSnapshotSchemaVersion; - status: CatalogTrustSnapshotRevokedStatus; - eligibility: CatalogTrustEligibility; - provenance: CatalogTrustProvenance; +export interface CatalogNotInstallableError { + /** + * Discriminator: the candidate cannot be installed + */ + kind: "not-installable"; + reason: CatalogNotInstallableReason; + /** + * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + */ + message: string; } /** - * Discriminator: the authority supplied a bounded trust value this runtime does not understand. + * Registry or enterprise policy refused the operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustSnapshotUnsupported". + * via the `definition` "CatalogPolicyRejectedError". */ /** @experimental */ -export interface CatalogTrustSnapshotUnsupported { - schemaVersion: CatalogTrustSnapshotSchemaVersion; - status: CatalogTrustSnapshotUnsupportedStatus; - eligibility: CatalogTrustEligibility; - provenance: CatalogTrustProvenance; +export interface CatalogPolicyRejectedError { + /** + * Discriminator: policy refused the operation + */ + kind: "policy-rejected"; + source: McpPlanPolicySource; + /** + * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + */ + message: string; } /** - * Discriminator: the trust field was empty, unbounded, or had the wrong JSON type. + * An explicit numbered-page request. SDK consumers treat the token as opaque. For bound search, the runtime unwraps an expiring owner-bound reference to the private authority token; only the runtime changes the authority token's targetPage. Legacy unbound navigation keeps its authority-issued token semantics. No snapshot stability is promised. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogTrustSnapshotMalformed". + * via the `definition` "CatalogSearchPage". */ /** @experimental */ -export interface CatalogTrustSnapshotMalformed { - schemaVersion: CatalogTrustSnapshotSchemaVersion; - status: CatalogTrustSnapshotMalformedStatus; - eligibility: CatalogTrustEligibility; - provenance: CatalogTrustProvenance; +export interface CatalogSearchPage { + /** + * Opaque pagination token from an earlier response, owner-bound when session-bound search was requested. Never decode, modify or log it in an SDK consumer. Expired or foreign bound references require a fresh bound search, not a legacy retry. + */ + token: string; + /** + * Requested one-based page. Must not exceed either the token's signed pageCount or the navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly available pages beyond that signed pageCount. + */ + number: number; } /** - * An inert AI skill catalog result. AI skills are discovery-only and cannot be represented as installable through this surface. + * Authority-reported navigation metadata, returned only to callers requiring catalog-search-pagination and only when a supported token is present. Tokenless first-page and continuation responses omit this object; no counts are inferred from candidates. The opaque token may be retained for previous or numbered navigation even when hasNextPage is false. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogAiSkillCandidate". + * via the `definition` "CatalogSearchPagination". */ /** @experimental */ -export interface CatalogAiSkillCandidate { - /** - * Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. - */ - handle: string; +export interface CatalogSearchPagination { /** - * ISO 8601 timestamp after which the handle is stale and will be rejected. + * Opaque pagination token. Session-bound search returns an expiring runtime-owned reference retaining the exact private authority token, original search and authority. Legacy unbound search returns the authority token unchanged, without a runtime-created expiry. Only the runtime unwraps tokens or changes targetPage; SDK consumers must not decode, modify or log them. */ - handleExpiresAt: string; - kind: CatalogAiSkillCandidateKind; - mediaType: CatalogAiSkillMediaType; - installability: CatalogAiSkillInstallability; + token: string; /** - * Display name taken verbatim from the card. Inert untrusted text. + * One-based page returned by the authority. */ - displayName: string; + currentPage: number; /** - * Description taken verbatim from the card. Inert untrusted text. + * Page size bound to the search, equal to the effective request limit. */ - description?: string; + pageSize: number; /** - * Publisher taken verbatim from the card. Inert untrusted text. + * Backend-reported count for this response, not the number of returned candidates. Its relationship to the full query result set is unknown. */ - publisher?: string; - source: CatalogCandidateSource; - provenance: CatalogAiSkillCandidateProvenance; - trust?: CatalogTrustSnapshot; -} -/** - * Candidate whose card is retrieved from a URL through the runtime's hardened fetch boundary. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogCandidateSourceUrl". - */ -/** @experimental */ -export interface CatalogCandidateSourceUrl { + totalCount: number; + totalCountRelation: CatalogSearchTotalCountRelation; /** - * Discriminator: the card is URL-backed, and carries no embedded data + * Backend-reported page count, which may exceed maxPage. Present pagination metadata always describes a multi-page result; zero- and single-page responses omit pagination. Navigation targets must also be within the signed pageCount carried by the supplied token. */ - kind: "url"; + pageCount: number; /** - * Card URL as advertised. Inert untrusted data: the runtime retrieves it only through its own hardened boundary, and it is never logged. + * Navigation window ceiling ceil(1000 / pageSize), not the number of existing pages. Legal targets must not exceed this ceiling or the token's signed pageCount. */ - url: string; -} -/** - * Candidate whose card reference arrived inline. The document and its content-derived properties stay behind the runtime boundary. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogCandidateSourceEmbedded". - */ -/** @experimental */ -export interface CatalogCandidateSourceEmbedded { + maxPage: number; /** - * Discriminator: the card is embedded, and carries no URL + * Whether the authority token advertises a valid next target within the navigation window. Not inferred from token presence, truncated, or currentPage being less than pageCount. */ - kind: "embedded"; + hasNextPage: boolean; } /** - * Where and when an AI skill catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogAiSkillCandidateProvenance". + * via the `definition` "CatalogSearchRequest". */ /** @experimental */ -export interface CatalogAiSkillCandidateProvenance { +export interface CatalogSearchRequest { + contract: CatalogClientContract; /** - * Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + * Select an existing attached local session. Requires authenticated, session-bound search. + * The runtime never creates, resumes or reconfigures a session to honour this selector. */ - authority: string; + policySessionId?: string; /** - * ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + * Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ - observedAt: string; - mediaType: CatalogAiSkillMediaType; + query: string; + /** + * Maximum number of candidates to return. Defaults to 10 when omitted. + */ + limit?: number; + /** + * Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. + * + * @minItems 1 + * @maxItems 3 + */ + kinds?: + | [CatalogCandidateKind] + | [CatalogCandidateKind, CatalogCandidateKind] + | [CatalogCandidateKind, CatalogCandidateKind, CatalogCandidateKind]; + page?: CatalogSearchPage; } /** - * An optional catalog authentication exchange did not establish the caller's identity. Anonymous search remains supported; this refusal is reserved for an operation that cannot continue after the attempted exchange. It is distinct from `policy-rejected` and from a network failure, and the reason identifies the recovery action. + * A completed catalog search containing inert candidate summaries. MCP server and AI skill variants carry a single-use handle; the Agent Plugin variant is handleless. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogAuthenticationRequiredError". + * via the `definition` "CatalogSearchSucceeded". */ /** @experimental */ -export interface CatalogAuthenticationRequiredError { +export interface CatalogSearchSucceeded { /** - * Discriminator: the caller is not authenticated + * Discriminator: the search completed */ - kind: "authentication-required"; - reason: CatalogAuthenticationRequiredReason; + kind: "succeeded"; /** - * Human-readable explanation, safe to surface. Never contains a credential or token, nor a query, URL, handle, or secret. + * Pseudonymous identifier for this search, issued by the runtime or by the catalog authority it queried and never by the caller, so it cannot be forged or replayed to attribute an install to a search that never happened. Always present on a success, so a result set can be tied to the installs it leads to. It identifies a search rather than a person: it is derived from no user, account, device, or query data, and must never be joined with user identity to re-identify anyone. */ - message: string; + searchId: string; + /** + * Matching candidates, never more than the requested limit. All text is inert untrusted data. + * + * @maxItems 50 + */ + candidates: CatalogCandidate[]; + /** + * Legacy indication that the authority returned a page token. Preserved for compatibility; this is not a has-next-page indicator. Use pagination.hasNextPage when pagination metadata is present. + */ + truncated: boolean; + negotiated: CatalogNegotiatedContract; + pagination?: CatalogSearchPagination; } /** - * An inert MCP server catalog result. Every free-text field is untrusted external data and must never be treated as an instruction, and the handle is the only way to refer to the candidate in a later operation. + * The request asked for a candidate kind this runtime does not serve. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogMcpServerCandidate". + * via the `definition` "CatalogUnsupportedKindError". */ /** @experimental */ -export interface CatalogMcpServerCandidate { - /** - * Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. - */ - handle: string; +export interface CatalogUnsupportedKindError { /** - * ISO 8601 timestamp after which the handle is stale and will be rejected. + * Discriminator: an unsupported candidate kind was requested */ - handleExpiresAt: string; - kind: CatalogMcpServerCandidateKind; - mediaType: McpServerCardMediaType; - installability: CatalogMcpServerInstallability; + kind: "unsupported-kind"; /** - * Display name taken verbatim from the card. Inert untrusted text. + * The kinds from the request that are not supported. */ - displayName: string; + requestedKinds: CatalogCandidateKind[]; /** - * Description taken verbatim from the card. Inert untrusted text. + * Every candidate kind this runtime can serve. */ - description?: string; + supportedKinds: CatalogCandidateKind[]; /** - * Publisher taken verbatim from the card. Inert untrusted text. + * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ - publisher?: string; - source: CatalogCandidateSource; - provenance: CatalogMcpServerCandidateProvenance; - trust?: CatalogTrustSnapshot; + message: string; } /** - * Where and when an MCP server catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. + * Retrieval was refused by the runtime's hardened fetch boundary before any request left the process, or before a redirect was followed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogMcpServerCandidateProvenance". + * via the `definition` "CatalogUnsafeRetrievalError". */ /** @experimental */ -export interface CatalogMcpServerCandidateProvenance { +export interface CatalogUnsafeRetrievalError { /** - * Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + * Discriminator: retrieval was refused as unsafe */ - authority: string; + kind: "unsafe-retrieval"; + reason: CatalogUnsafeRetrievalReason; /** - * ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + * Human-readable explanation, safe to surface. Never contains the refused URL, nor a query, handle, or secret. */ - observedAt: string; - mediaType: McpServerCardMediaType; + message: string; } /** - * The protocol version and capability set a caller requires, supplied on every catalog request so negotiation cannot be skipped by omission. + * The operation is not available on this runtime. Distinct from a network failure: nothing was attempted. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogClientContract". + * via the `definition` "CatalogUnavailableError". */ /** @experimental */ -export interface CatalogClientContract { +export interface CatalogUnavailableError { /** - * SDK protocol version the caller was generated against. A caller below the runtime's minimum supported version is refused rather than served a partial result. + * Discriminator: the operation is not available */ - protocolVersion: number; + kind: "unavailable"; + reason: CatalogUnavailableReason; /** - * Wire features the caller requires the runtime to understand. Identifiers are bounded but extensible so a newer caller can negotiate with an older runtime. Requiring an unknown feature yields a typed refusal listing what is understood, never a partial grant. A grant does not promise that a deployment has enabled the operation; typed unavailable results report that separately. - * - * @maxItems 32 + * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ - requiredCapabilities: CatalogCapabilityId[]; + message: string; } /** - * An upstream catalog response broke the wire contract. Most importantly, every result must carry exactly one of a URL or embedded data: a result carrying both, or neither, is refused here rather than being guessed at. + * The caller cancelled the selection interaction and the retained search state was released. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogContractViolationError". + * via the `definition` "CatalogSelectionCancelled". */ /** @experimental */ -export interface CatalogContractViolationError { +export interface CatalogSelectionCancelled { /** - * Discriminator: the upstream response broke the contract + * Discriminator: selection was cancelled */ - kind: "contract-violation"; - reason: CatalogContractViolationReason; + kind: "cancelled"; /** - * Human-readable explanation, safe to surface. Never echoes response content, nor a query, URL, handle, or secret. + * The search identifier privately bound to the released selection group. */ - message: string; + searchId: string; } /** - * A presented handle was not accepted. Handles are runtime-instance scoped, TTL-bound, and single-use, so each way of failing is reported distinctly. + * The caller explicitly declined every candidate and the retained search state was released. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogHandleRejectedError". + * via the `definition` "CatalogSelectionDeclined". */ /** @experimental */ -export interface CatalogHandleRejectedError { +export interface CatalogSelectionDeclined { /** - * Discriminator: a handle was rejected + * Discriminator: the candidates were declined */ - kind: "handle-rejected"; - handleType: CatalogHandleType; - reason: CatalogHandleRejectionReason; + kind: "declined"; /** - * Human-readable explanation, safe to surface. Never contains the handle itself, nor a query, URL, or secret. + * The search identifier privately bound to the released selection group. */ - message: string; + searchId: string; } /** - * The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. + * The selection reference belongs to another runtime instance or session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogInvalidRequestError". + * via the `definition` "CatalogSelectionForeign". */ /** @experimental */ -export interface CatalogInvalidRequestError { +export interface CatalogSelectionForeign { /** - * Discriminator: the request itself was invalid + * Discriminator for this typed selection rejection */ - kind: "invalid-request"; - field: CatalogInvalidRequestField; + kind: "foreign"; /** - * Human-readable explanation, safe to surface. Never echoes the offending value, nor a query, URL, handle, or secret. + * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. */ message: string; } /** - * A card could not be parsed or did not satisfy its declared media type's schema. + * The selection reference was malformed or unknown. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogMalformedCardError". + * via the `definition` "CatalogSelectionInvalid". */ /** @experimental */ -export interface CatalogMalformedCardError { +export interface CatalogSelectionInvalid { /** - * Discriminator: the card was malformed + * Discriminator for this typed selection rejection */ - kind: "malformed-card"; - reason: CatalogMalformedCardReason; - mediaType?: CatalogMediaType; + kind: "invalid"; /** - * Human-readable explanation, safe to surface. Never echoes card content, nor a query, URL, handle, or secret. + * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. */ message: string; } /** - * The protocol version and capability set the runtime actually honoured for a successful catalog operation. + * The selection group was already terminated or its pending host hand-off was already claimed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogNegotiatedContract". + * via the `definition` "CatalogSelectionReplayed". */ /** @experimental */ -export interface CatalogNegotiatedContract { +export interface CatalogSelectionReplayed { /** - * Protocol version of the runtime that served the request. + * Discriminator for this typed selection rejection */ - runtimeProtocolVersion: number; + kind: "replayed"; /** - * Wire features the runtime understood for this operation. Includes the five original catalog capabilities and only explicitly requested supported additions, in supported order without duplicates. Capabilities that introduce new success-union variants or operations are therefore included only when explicitly required, preserving older protocol-v3 clients. Always a superset of the caller's required features, because any shortfall is a refusal instead. Operation availability remains a separate typed result. + * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. */ - grantedCapabilities: CatalogCapability[]; + message: string; } /** - * The caller's protocol version or required capabilities cannot be honoured. Returned instead of a partial or ambiguous success. + * Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogNegotiationRefusedError". + * via the `definition` "CatalogSelectionRequest". */ /** @experimental */ -export interface CatalogNegotiationRefusedError { - /** - * Discriminator: capability or protocol-version negotiation failed - */ - kind: "negotiation-refused"; - reason: CatalogNegotiationRefusedReason; +export interface CatalogSelectionRequest { + contract: CatalogClientContract; /** - * Protocol version of the runtime that refused the request. + * Locally owned root session whose retained search state is being resolved. */ - runtimeProtocolVersion: number; + sessionId: string; /** - * Lowest caller protocol version this runtime will serve. + * Opaque runtime-instance scoped reference to one visible candidate. For a non-selected outcome, any candidate reference from the same search closes that search's retained group. */ - minimumSupportedProtocolVersion: number; + selectionRef: string; + outcome: CatalogSelectionDecision; +} +/** + * The chosen candidate was transferred into a fresh bounded single-use handle for a later explicit planning request. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogSelectionSelected". + */ +/** @experimental */ +export interface CatalogSelectionSelected { /** - * Capabilities this runtime can safely advertise to this caller. The complete five-capability protocol-3 legacy set is always present; every capability added after that baseline appears only when the caller required it, so an older closed-enum decoder can still consume a refusal. This list does not imply that every deployment has enabled every operation. - * - * @maxItems 32 + * Discriminator: one candidate was selected */ - supportedCapabilities: CatalogCapabilityId[]; + kind: "selected"; /** - * The subset of the caller's bounded extensible capability identifiers this runtime cannot honour. - * - * @maxItems 32 + * Fresh single-use candidate handle accepted by mcp.planInstall. Returned only to the native host and never included in model-tool output. */ - unsupportedCapabilities: CatalogCapabilityId[]; + candidateHandle: string; /** - * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + * The exact search identifier privately bound to the selected candidate. */ - message: string; + searchId: string; } /** - * The runtime could not reach the catalog authority or retrieve a card. Covers being offline as well as transport-level failure. + * The host declared that its live selection interaction timed out, and the retained search state was released. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogNetworkFailureError". + * via the `definition` "CatalogSelectionTimedOut". */ /** @experimental */ -export interface CatalogNetworkFailureError { +export interface CatalogSelectionTimedOut { /** - * Discriminator: the network operation failed + * Discriminator: the host's live interaction timed out */ - kind: "network-failure"; - reason: CatalogNetworkFailureReason; + kind: "timed-out"; /** - * HTTP status code, when the failure was a rejected response. + * The search identifier privately bound to the released selection group. */ - statusCode?: number; + searchId: string; +} +/** + * The runtime-enforced selection reference lifetime elapsed before the request arrived. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CatalogSelectionStale". + */ +/** @experimental */ +export interface CatalogSelectionStale { /** - * Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + * Discriminator for this typed selection rejection */ - retryAfterSeconds?: number; + kind: "stale"; /** - * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. */ message: string; } /** - * The candidate is discoverable but cannot be installed. `application/ai-skill` resolves here, because it stays searchable while remaining typed non-installable. + * The presented opaque handle was issued for another catalog operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogNotInstallableError". + * via the `definition` "CatalogSelectionWrongKind". */ /** @experimental */ -export interface CatalogNotInstallableError { +export interface CatalogSelectionWrongKind { /** - * Discriminator: the candidate cannot be installed + * Discriminator for this typed selection rejection */ - kind: "not-installable"; - reason: CatalogNotInstallableReason; + kind: "wrong-kind"; /** - * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. */ message: string; } /** - * Registry or enterprise policy refused the operation. + * No transport this runtime can use is available for the requested server. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogPolicyRejectedError". + * via the `definition` "CatalogUnavailableTransportError". */ /** @experimental */ -export interface CatalogPolicyRejectedError { +export interface CatalogUnavailableTransportError { /** - * Discriminator: policy refused the operation + * Discriminator: no usable transport is available */ - kind: "policy-rejected"; - source: McpPlanPolicySource; + kind: "unavailable-transport"; + reason: CatalogUnavailableTransportReason; /** * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ message: string; } /** - * An explicit numbered-page request. The SDK treats the token as opaque; only the runtime decodes it and changes its targetPage. Authority validation binds navigation to the original search. No snapshot stability or token TTL is promised. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSearchPage". + * via the `definition` "ClientMetadata". */ /** @experimental */ -export interface CatalogSearchPage { +export interface ClientMetadata { + [k: string]: string | undefined; +} +/** + * Runtime-to-owner cancellation request for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ClientTaskCancelRequest". + */ +/** @experimental */ +export interface ClientTaskCancelRequest { /** - * Opaque authority-issued pagination token from an earlier response. Never decode, modify or log it in an SDK consumer. + * Session that owns the client task */ - token: string; + sessionId: string; /** - * Requested one-based page. Must not exceed either the token's signed pageCount or the navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly available pages beyond that signed pageCount. + * Canonical runtime-generated task identifier */ - number: number; + id: string; + /** + * Owner-scoped task key included for correlation + */ + clientTaskId: string; + /** + * Opaque identifier shared by coalesced cancellation callers + */ + cancellationId: string; + reason: ClientTaskCancelReason; } /** - * Authority-reported navigation metadata, returned only to callers requiring catalog-search-pagination and only when a supported token is present. Tokenless first-page and continuation responses omit this object; no counts are inferred from candidates. The opaque token may be retained for previous or numbered navigation even when hasNextPage is false. + * Whether the client authoritatively confirmed its external work stopped. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSearchPagination". + * via the `definition` "ClientTaskCancelResult". */ /** @experimental */ -export interface CatalogSearchPagination { +export interface ClientTaskCancelResult { /** - * Opaque authority-issued pagination token. Only the runtime decodes it or changes targetPage; SDK consumers must not decode, modify or log it. It has no runtime-created expiry or cache. + * True only when the owner confirms that external work stopped before responding */ - token: string; + cancelled: boolean; +} +/** + * Slash commands available in the session, after applying any include/exclude filters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CommandList". + */ +/** @experimental */ +export interface CommandList { /** - * One-based page returned by the authority. + * Commands available in this session */ - currentPage: number; + commands: SlashCommandInfo[]; +} +/** + * Slash-command metadata with name, aliases, description, kind, input hint, execution allowance, and schedulability. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SlashCommandInfo". + */ +/** @experimental */ +export interface SlashCommandInfo { /** - * Page size bound to the search, equal to the effective request limit. + * Canonical command name without a leading slash */ - pageSize: number; + name: string; /** - * Backend-reported count for this response, not the number of returned candidates. Its relationship to the full query result set is unknown. + * Canonical aliases without leading slashes */ - totalCount: number; - totalCountRelation: CatalogSearchTotalCountRelation; + aliases?: string[]; /** - * Backend-reported page count, which may exceed maxPage. Present pagination metadata always describes a multi-page result; zero- and single-page responses omit pagination. Navigation targets must also be within the signed pageCount carried by the supplied token. + * Human-readable command description */ - pageCount: number; + description: string; + kind: SlashCommandKind; + input?: SlashCommandInput; /** - * Navigation window ceiling ceil(1000 / pageSize), not the number of existing pages. Legal targets must not exceed this ceiling or the token's signed pageCount. + * Whether the command may run while an agent turn is active */ - maxPage: number; + allowDuringAgentExecution: boolean; /** - * Whether the authority token advertises a valid next target within the navigation window. Not inferred from token presence, truncated, or currentPage being less than pageCount. + * Whether the command is experimental */ - hasNextPage: boolean; + experimental?: boolean; + /** + * Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt. + */ + schedulable?: boolean; } /** - * 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. + * Optional unstructured input hint * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSearchRequest". + * via the `definition` "SlashCommandInput". */ /** @experimental */ -export interface CatalogSearchRequest { - contract: CatalogClientContract; +export interface SlashCommandInput { /** - * Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. + * Hint to display when command input has not been provided */ - query: string; + hint: string; /** - * Maximum number of candidates to return. Defaults to 10 when omitted. + * Optional literal choices the input accepts, each with a human-facing description; clients may render these as selectable options */ - limit?: number; + choices?: SlashCommandInputChoice[]; /** - * Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. - * - * @minItems 1 - * @maxItems 3 + * When true, the command requires non-empty input; clients should render the input hint as required */ - kinds?: - | [CatalogCandidateKind] - | [CatalogCandidateKind, CatalogCandidateKind] - | [CatalogCandidateKind, CatalogCandidateKind, CatalogCandidateKind]; - page?: CatalogSearchPage; + required?: boolean; + completion?: SlashCommandInputCompletion; + /** + * When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace + */ + preserveMultilineInput?: boolean; } /** - * A completed catalog search containing inert candidate summaries. MCP server and AI skill variants carry a single-use handle; the Agent Plugin variant is handleless. + * A literal choice the command input accepts, with a human-facing description * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSearchSucceeded". + * via the `definition` "SlashCommandInputChoice". */ /** @experimental */ -export interface CatalogSearchSucceeded { - /** - * Discriminator: the search completed - */ - kind: "succeeded"; +export interface SlashCommandInputChoice { /** - * Pseudonymous identifier for this search, issued by the runtime or by the catalog authority it queried and never by the caller, so it cannot be forged or replayed to attribute an install to a search that never happened. Always present on a success, so a result set can be tied to the installs it leads to. It identifies a search rather than a person: it is derived from no user, account, device, or query data, and must never be joined with user identity to re-identify anyone. + * The literal choice value (e.g. 'on', 'off', 'show') */ - searchId: string; + name: string; /** - * Matching candidates, never more than the requested limit. All text is inert untrusted data. - * - * @maxItems 50 + * Human-readable description shown alongside the choice */ - candidates: CatalogCandidate[]; + description: string; +} +/** + * The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CommandsFinalizeInvocationEffectRequest". + */ +/** @experimental */ +export interface CommandsFinalizeInvocationEffectRequest { /** - * Legacy indication that the authority returned a page token. Preserved for compatibility; this is not a has-next-page indicator. Use pagination.hasNextPage when pagination metadata is present. + * The slash-command result object that produced the pending effect, echoed back unchanged. */ - truncated: boolean; - negotiated: CatalogNegotiatedContract; - pagination?: CatalogSearchPagination; + effect: {}; + outcome: CommandsInvocationEffectOutcome; } /** - * The request asked for a candidate kind this runtime does not serve. + * Whether finalizing the invocation effect succeeded, and the failure reason when it did not. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogUnsupportedKindError". + * via the `definition` "CommandsFinalizeInvocationEffectResult". */ /** @experimental */ -export interface CatalogUnsupportedKindError { +export interface CommandsFinalizeInvocationEffectResult { /** - * Discriminator: an unsupported candidate kind was requested + * Whether the pending invocation effect was finalized successfully. */ - kind: "unsupported-kind"; + success: boolean; /** - * The kinds from the request that are not supported. + * Failure reason when the invocation effect could not be finalized. */ - requestedKinds: CatalogCandidateKind[]; + error?: string; +} +/** + * Pending command request ID and an optional error if the client handler failed. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CommandsHandlePendingCommandRequest". + */ +/** @experimental */ +export interface CommandsHandlePendingCommandRequest { /** - * Every candidate kind this runtime can serve. + * Request ID from the command invocation event */ - supportedKinds: CatalogCandidateKind[]; + requestId: string; + /** + * Error message if the command handler failed + */ + error?: string; +} +/** + * Indicates whether the pending client-handled command was completed successfully. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CommandsHandlePendingCommandResult". + */ +/** @experimental */ +export interface CommandsHandlePendingCommandResult { /** - * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + * Whether the command was handled successfully */ - message: string; + success: boolean; } /** - * Retrieval was refused by the runtime's hardened fetch boundary before any request left the process, or before a redirect was followed. + * Slash command name and optional raw input string to invoke. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogUnsafeRetrievalError". + * via the `definition` "CommandsInvokeRequest". */ /** @experimental */ -export interface CatalogUnsafeRetrievalError { +export interface CommandsInvokeRequest { /** - * Discriminator: retrieval was refused as unsafe + * Command name. Leading slashes are stripped and the name is matched case-insensitively. */ - kind: "unsafe-retrieval"; - reason: CatalogUnsafeRetrievalReason; + name: string; /** - * Human-readable explanation, safe to surface. Never contains the refused URL, nor a query, handle, or secret. + * Raw input after the command name */ - message: string; + input?: string; + origin?: CommandsInvocationOrigin; } /** - * The operation is not available on this runtime. Distinct from a network failure: nothing was attempted. + * Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogUnavailableError". + * via the `definition` "CommandsRespondToQueuedCommandRequest". */ /** @experimental */ -export interface CatalogUnavailableError { - /** - * Discriminator: the operation is not available - */ - kind: "unavailable"; - reason: CatalogUnavailableReason; +export interface CommandsRespondToQueuedCommandRequest { /** - * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + * Request ID from the `command.queued` event the host is responding to. */ - message: string; + requestId: string; + result: QueuedCommandResult; } /** - * The caller cancelled the selection interaction and the retained search state was released. + * Queued-command response indicating the host executed the command, with an optional flag to stop queue processing. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionCancelled". + * via the `definition` "QueuedCommandHandled". */ /** @experimental */ -export interface CatalogSelectionCancelled { +export interface QueuedCommandHandled { /** - * Discriminator: selection was cancelled + * The host actually executed the queued command. */ - kind: "cancelled"; + handled: true; /** - * The search identifier privately bound to the released selection group. + * When true, the runtime will not process subsequent queued commands until a new request comes in. */ - searchId: string; + stopProcessingQueue?: boolean; } /** - * The caller explicitly declined every candidate and the retained search state was released. + * Queued-command response indicating the host did not execute the command and the queue may continue. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionDeclined". + * via the `definition` "QueuedCommandNotHandled". */ /** @experimental */ -export interface CatalogSelectionDeclined { - /** - * Discriminator: the candidates were declined - */ - kind: "declined"; +export interface QueuedCommandNotHandled { /** - * The search identifier privately bound to the released selection group. + * The host did not execute the queued command. Unblocks the queue without claiming the command was processed (e.g. when the handler threw before completing). */ - searchId: string; + handled: false; } /** - * The selection reference belongs to another runtime instance or session. + * Indicates whether the queued-command response was matched to a pending request. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionForeign". + * via the `definition` "CommandsRespondToQueuedCommandResult". */ /** @experimental */ -export interface CatalogSelectionForeign { - /** - * Discriminator for this typed selection rejection - */ - kind: "foreign"; +export interface CommandsRespondToQueuedCommandResult { /** - * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + * Whether a pending queued command with the given request ID was found and resolved. False when the request was already resolved, cancelled, or unknown. */ - message: string; + success: boolean; } /** - * The selection reference was malformed or unknown. + * 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`). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionInvalid". + * via the `definition` "CompletionsGetTriggerCharactersResult". */ /** @experimental */ -export interface CatalogSelectionInvalid { - /** - * Discriminator for this typed selection rejection - */ - kind: "invalid"; +export interface CompletionsGetTriggerCharactersResult { /** - * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + * Trigger characters advertised by the host (e.g. `["@", "#"]`). Empty disables host-driven completions for the session. */ - message: string; + triggerCharacters: string[]; } /** - * The selection group was already terminated or its pending host hand-off was already claimed. + * Request host-driven completions for the current composer input. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionReplayed". + * via the `definition` "CompletionsRequestRequest". */ /** @experimental */ -export interface CatalogSelectionReplayed { +export interface CompletionsRequestRequest { /** - * Discriminator for this typed selection rejection + * The full composed composer input. */ - kind: "replayed"; + text: string; /** - * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + * Cursor offset within `text`, in UTF-16 code units. */ - message: string; + offset: number; } /** - * Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + * Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionRequest". + * via the `definition` "CompletionsRequestResult". */ /** @experimental */ -export interface CatalogSelectionRequest { - contract: CatalogClientContract; - /** - * Locally owned root session whose retained search state is being resolved. - */ - sessionId: string; +export interface CompletionsRequestResult { /** - * Opaque runtime-instance scoped reference to one visible candidate. For a non-selected outcome, any candidate reference from the same search closes that search's retained group. + * Completion items in host-ranked order. */ - selectionRef: string; - outcome: CatalogSelectionDecision; + items: SessionCompletionItem[]; } /** - * The chosen candidate was transferred into a fresh bounded single-use handle for a later explicit planning request. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionSelected". + * via the `definition` "SessionCompletionItem". */ /** @experimental */ -export interface CatalogSelectionSelected { +export interface SessionCompletionItem { /** - * Discriminator: one candidate was selected + * Text spliced into the composer when the item is accepted. */ - kind: "selected"; + insertText: string; /** - * Fresh single-use candidate handle accepted by mcp.planInstall. Returned only to the native host and never included in model-tool output. + * Start of the replacement range in `text`, in UTF-16 code units. */ - candidateHandle: string; + rangeStart?: number; /** - * The exact search identifier privately bound to the selected candidate. + * End (exclusive) of the replacement range in `text`, in UTF-16 code units. */ - searchId: string; -} -/** - * The host declared that its live selection interaction timed out, and the retained search state was released. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionTimedOut". - */ -/** @experimental */ -export interface CatalogSelectionTimedOut { + rangeEnd?: number; /** - * Discriminator: the host's live interaction timed out + * Primary display label for the picker row. Falls back to `insertText` when absent. */ - kind: "timed-out"; + label?: string; /** - * The search identifier privately bound to the released selection group. + * Render-kind hint for the picker row (e.g. `"document"`, `"directory"`), derived from the host's display kind. */ - searchId: string; + kind?: string; } /** - * The runtime-enforced selection reference lifetime elapsed before the request arrived. + * Params to attach or detach an in-process ExtensionController delegate. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionStale". + * via the `definition` "ConfigureSessionExtensionsParams". */ /** @experimental */ -export interface CatalogSelectionStale { +/** @internal */ +export interface ConfigureSessionExtensionsParams { /** - * Discriminator for this typed selection rejection + * Session to attach the extension controller delegate to. */ - kind: "stale"; + sessionId: string; /** - * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + * 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. + * + * @internal + * + * @internal */ - message: string; + controller?: OpaqueInProcessValue; } /** - * The presented opaque handle was issued for another catalog operation. + * Identity of the integrating host, declared once on the `server.connect` handshake so telemetry from this connection is attributed to a single, consistent surface. All fields are optional; omit them to keep the default attribution. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogSelectionWrongKind". + * via the `definition` "ConnectClientInfo". */ /** @experimental */ -export interface CatalogSelectionWrongKind { +/** @internal */ +export interface ConnectClientInfo { /** - * Discriminator for this typed selection rejection + * Name of the host editor, e.g. `"vscode"`. */ - kind: "wrong-kind"; + editorName?: string; /** - * Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + * Version of the host editor, e.g. `"1.124.2"`. Ignored unless it looks like a version string. */ - message: string; -} -/** - * No transport this runtime can use is available for the requested server. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CatalogUnavailableTransportError". - */ -/** @experimental */ -export interface CatalogUnavailableTransportError { + editorVersion?: string; /** - * Discriminator: no usable transport is available + * Name of the Copilot extension within the host, e.g. `"copilot-chat"`. */ - kind: "unavailable-transport"; - reason: CatalogUnavailableTransportReason; + extensionName?: string; /** - * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + * Version of the Copilot extension within the host, e.g. `"0.54.0"`. Ignored unless it looks like a version string. */ - message: string; + extensionVersion?: string; } /** - * 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. + * Metadata for a connected remote session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ClientMetadata". + * via the `definition` "ConnectedRemoteSessionMetadata". */ /** @experimental */ -export interface ClientMetadata { - [k: string]: string | undefined; +export interface ConnectedRemoteSessionMetadata { + /** + * SDK session ID for the connected remote session. + */ + sessionId: string; + /** + * Optional friendly session name. + */ + name?: string; + /** + * Optional session summary. + */ + summary?: string; + /** + * Session start time as an ISO 8601 string. + */ + startTime: string; + /** + * Last session update time as an ISO 8601 string. + */ + modifiedTime: string; + repository: ConnectedRemoteSessionMetadataRepository; + /** + * Pull request number associated with the session. + */ + pullRequestNumber?: number; + /** + * Original remote resource identifier. + */ + resourceId?: string; + kind: ConnectedRemoteSessionMetadataKind; + /** + * Remote session staleness deadline as an ISO 8601 string. + */ + staleAt?: string; + /** + * Remote session state returned by the backing service. + */ + state?: string; } /** - * Runtime-to-owner cancellation request for a client-owned task. + * Repository associated with the connected remote session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ClientTaskCancelRequest". + * via the `definition` "ConnectedRemoteSessionMetadataRepository". */ /** @experimental */ -export interface ClientTaskCancelRequest { - /** - * Session that owns the client task - */ - sessionId: string; +export interface ConnectedRemoteSessionMetadataRepository { /** - * Canonical runtime-generated task identifier + * Repository owner or organization login. */ - id: string; + owner: string; /** - * Owner-scoped task key included for correlation + * Repository name. */ - clientTaskId: string; + name: string; /** - * Opaque identifier shared by coalesced cancellation callers + * Branch associated with the remote session. */ - cancellationId: string; - reason: ClientTaskCancelReason; + branch: string; } /** - * Whether the client authoritatively confirmed its external work stopped. + * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ClientTaskCancelResult". + * via the `definition` "ConnectorAccountRequest". */ /** @experimental */ -export interface ClientTaskCancelResult { +export interface ConnectorAccountRequest { /** - * True only when the owner confirms that external work stopped before responding + * Opaque account selection ID previously returned by an account discovery API. */ - cancelled: boolean; + accountId: string; } /** - * Slash commands available in the session, after applying any include/exclude filters. + * Account-targeted authorization update required by the Connector service. The account ID is an opaque host routing identifier; no credential is included. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandList". + * via the `definition` "ConnectorAuthorizationRequirement". */ /** @experimental */ -export interface CommandList { +export interface ConnectorAuthorizationRequirement { /** - * Commands available in this session + * Exact opaque account selection that made the Connector request. */ - commands: SlashCommandInfo[]; + accountId: string; + scope: ConnectorAuthorizationScope; } /** - * Slash-command metadata with name, aliases, description, kind, input hint, execution allowance, and schedulability. + * Feature detection and hard polling limits for the EXPERIMENTAL session connector API. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SlashCommandInfo". + * via the `definition` "ConnectorCapabilities". */ /** @experimental */ -export interface SlashCommandInfo { +export interface ConnectorCapabilities { /** - * Canonical command name without a leading slash + * Connector API contract version. */ - name: string; + apiVersion: number; + availability: ConnectorAvailability; /** - * Canonical aliases without leading slashes + * Whether connect and reconnect can return an opaque continuation for bounded consent polling. */ - aliases?: string[]; + consentContinuation: boolean; /** - * Human-readable command description + * Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. */ - description: string; - kind: SlashCommandKind; - input?: SlashCommandInput; + opaqueAccountSelection: boolean; /** - * Whether the command may run while an agent turn is active + * Whether getAccount is supported. Absence means false. */ - allowDuringAgentExecution: boolean; + sessionAccountSelection?: boolean; /** - * Whether the command is experimental + * Whether reconcile accepts forceConnectorName. Absence means false. */ - experimental?: boolean; + targetedReconcile?: boolean; /** - * Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt. + * Maximum accepted polling attempts for one continuation call. */ - schedulable?: boolean; + maxPollAttempts: number; + /** + * Maximum accepted delay in milliseconds between polling attempts. + */ + maxPollIntervalMs: number; + /** + * Maximum accepted wall-clock deadline in milliseconds for one continuation call. + */ + maxDeadlineMs: number; } /** - * Optional unstructured input hint + * Credential-free Connector catalog entry. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SlashCommandInput". + * via the `definition` "ConnectorCatalogEntry". */ /** @experimental */ -export interface SlashCommandInput { +export interface ConnectorCatalogEntry { /** - * Hint to display when command input has not been provided + * Canonical Connector name used by lifecycle methods. */ - hint: string; + name: string; /** - * Optional literal choices the input accepts, each with a human-facing description; clients may render these as selectable options + * Untrusted display label from the service. */ - choices?: SlashCommandInputChoice[]; + displayName: string; /** - * When true, the command requires non-empty input; clients should render the input hint as required + * Untrusted service description, when present. */ - required?: boolean; - completion?: SlashCommandInputCompletion; + description?: string; /** - * When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace + * Optional catalog logo. */ - preserveMultilineInput?: boolean; -} -/** - * A literal choice the command input accepts, with a human-facing description - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SlashCommandInputChoice". - */ -/** @experimental */ -export interface SlashCommandInputChoice { + logo?: string; /** - * The literal choice value (e.g. 'on', 'off', 'show') + * Optional catalog tier. */ - name: string; + tier?: string; /** - * Human-readable description shown alongside the choice + * Optional catalog release tag. */ - description: string; -} -/** - * The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandsFinalizeInvocationEffectRequest". - */ -/** @experimental */ -export interface CommandsFinalizeInvocationEffectRequest { + releaseTag?: string; + status: ConnectorCatalogStatus; /** - * The slash-command result object that produced the pending effect, echoed back unchanged. + * Opaque stable runtime IDs currently projected into the session for this Connector. */ - effect: {}; - outcome: CommandsInvocationEffectOutcome; + runtimeServerIds: string[]; } /** - * Whether finalizing the invocation effect succeeded, and the failure reason when it did not. + * Validated Connector catalog snapshot cached by the session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandsFinalizeInvocationEffectResult". + * via the `definition` "ConnectorCatalogResult". */ /** @experimental */ -export interface CommandsFinalizeInvocationEffectResult { +export interface ConnectorCatalogResult { /** - * Whether the pending invocation effect was finalized successfully. + * Monotonically increasing session-local catalog revision. */ - success: boolean; + revision: number; /** - * Failure reason when the invocation effect could not be finalized. + * Unix epoch milliseconds when this snapshot was accepted. */ - error?: string; + refreshedAtMs: number; + /** + * Validated catalog entries in service order. + */ + connectors: ConnectorCatalogEntry[]; } /** - * Pending command request ID and an optional error if the client handler failed. + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandsHandlePendingCommandRequest". + * via the `definition` "ConnectorConnectRequest". */ /** @experimental */ -export interface CommandsHandlePendingCommandRequest { +export interface ConnectorConnectRequest { /** - * Request ID from the command invocation event + * Opaque account selection ID. It must match the account already pinned to the session, if any. */ - requestId: string; + accountId: string; /** - * Error message if the command handler failed + * Canonical Connector name from the current catalog. */ - error?: string; + connectorName: string; } /** - * Indicates whether the pending client-handled command was completed successfully. + * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandsHandlePendingCommandResult". + * via the `definition` "ConnectorStatus". */ /** @experimental */ -export interface CommandsHandlePendingCommandResult { +export interface ConnectorStatus { /** - * Whether the command was handled successfully + * Connector API contract version. */ - success: boolean; + apiVersion: number; + availability: ConnectorAvailability; + /** + * Opaque account selection pinned to this session, when one has been selected. + */ + accountId?: string; + catalog?: ConnectorCatalogResult; + authorizationRequirement?: ConnectorAuthorizationRequirement; + /** + * Live MCP status for every Connector-owned runtime server. + */ + runtimeServers: ConnectorRuntimeStatus[]; + /** + * Number of active opaque connection continuations. + */ + pendingConnections: number; } /** - * Slash command name and optional raw input string to invoke. + * Live status of one session-owned MCP projection. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandsInvokeRequest". + * via the `definition` "ConnectorRuntimeStatus". */ /** @experimental */ -export interface CommandsInvokeRequest { +export interface ConnectorRuntimeStatus { /** - * Command name. Leading slashes are stripped and the name is matched case-insensitively. + * Opaque runtime server ID. */ - name: string; + runtimeServerId: string; /** - * Raw input after the command name + * Canonical Connector name that owns this server. */ - input?: string; - origin?: CommandsInvocationOrigin; + connectorName: string; + status: ConnectorMcpStatus; } /** - * Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). + * Explicitly bounded continuation of a pending Connector connection. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandsRespondToQueuedCommandRequest". + * via the `definition` "ConnectorContinueRequest". */ /** @experimental */ -export interface CommandsRespondToQueuedCommandRequest { +export interface ConnectorContinueRequest { /** - * Request ID from the `command.queued` event the host is responding to. + * Opaque continuation ID returned by connect, reconnect, or an earlier continuation. */ - requestId: string; - result: QueuedCommandResult; + continuationId: string; + /** + * Maximum catalog requests made by this call. Must be between one and the capability limit. + */ + maxAttempts: number; + /** + * Delay in milliseconds between attempts. Must not exceed the capability limit. + */ + pollIntervalMs: number; + /** + * Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. + */ + deadlineMs: number; } /** - * Queued-command response indicating the host executed the command, with an optional flag to stop queue processing. + * Authoritative result after disconnect and MCP reconciliation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "QueuedCommandHandled". + * via the `definition` "ConnectorDisconnectResult". */ /** @experimental */ -export interface QueuedCommandHandled { - /** - * The host actually executed the queued command. - */ - handled: true; +export interface ConnectorDisconnectResult { /** - * When true, the runtime will not process subsequent queued commands until a new request comes in. + * Whether the service accepted the idempotent disconnect. */ - stopProcessingQueue?: boolean; + disconnected: boolean; + status: ConnectorStatus; } /** - * Queued-command response indicating the host did not execute the command and the queue may continue. + * Eligible account. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "QueuedCommandNotHandled". + * via the `definition` "ConnectorDiscoveryAccount". */ /** @experimental */ -export interface QueuedCommandNotHandled { +export interface ConnectorDiscoveryAccount { /** - * The host did not execute the queued command. Unblocks the queue without claiming the command was processed (e.g. when the handler threw before completing). + * Opaque account ID. */ - handled: false; + accountId: string; + authInfo: ConnectorDiscoveryAuthInfo; } /** - * Indicates whether the queued-command response was matched to a pending request. + * Account metadata. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CommandsRespondToQueuedCommandResult". + * via the `definition` "ConnectorDiscoveryAuthInfo". */ /** @experimental */ -export interface CommandsRespondToQueuedCommandResult { +export interface ConnectorDiscoveryAuthInfo { + type: AuthInfoType; /** - * Whether a pending queued command with the given request ID was found and resolved. False when the request was already resolved, cancelled, or unknown. + * Host. */ - success: boolean; + host: string; + /** + * Login. + */ + login: string; } /** - * 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`). + * Eligible accounts. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CompletionsGetTriggerCharactersResult". + * via the `definition` "ConnectorDiscoveryAccountList". */ /** @experimental */ -export interface CompletionsGetTriggerCharactersResult { +export interface ConnectorDiscoveryAccountList { + availability: ConnectorDiscoveryAvailability; /** - * Trigger characters advertised by the host (e.g. `["@", "#"]`). Empty disables host-driven completions for the session. + * Eligible accounts. */ - triggerCharacters: string[]; + accounts: ConnectorDiscoveryAccount[]; } /** - * Request host-driven completions for the current composer input. + * Selected account. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CompletionsRequestRequest". + * via the `definition` "ConnectorDiscoveryAccountRequest". */ /** @experimental */ -export interface CompletionsRequestRequest { - /** - * The full composed composer input. - */ - text: string; +export interface ConnectorDiscoveryAccountRequest { /** - * Cursor offset within `text`, in UTF-16 code units. + * Opaque account ID. */ - offset: number; + accountId: string; } /** - * Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. + * Feature availability. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CompletionsRequestResult". + * via the `definition` "ConnectorDiscoveryCapabilities". */ /** @experimental */ -export interface CompletionsRequestResult { +export interface ConnectorDiscoveryCapabilities { /** - * Completion items in host-ranked order. + * API version. */ - items: SessionCompletionItem[]; + apiVersion: number; + availability: ConnectorDiscoveryAvailability; + /** + * Whether accounts are selected by opaque ID. + */ + opaqueAccountSelection: boolean; + /** + * Whether results are cached. + */ + conditionalCache: boolean; } /** - * 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. + * Entry. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionCompletionItem". + * via the `definition` "ConnectorDiscoveryCatalogEntry". */ /** @experimental */ -export interface SessionCompletionItem { +export interface ConnectorDiscoveryCatalogEntry { /** - * Text spliced into the composer when the item is accepted. + * Name. */ - insertText: string; + name: string; /** - * Start of the replacement range in `text`, in UTF-16 code units. + * Display name. */ - rangeStart?: number; + displayName: string; /** - * End (exclusive) of the replacement range in `text`, in UTF-16 code units. + * Description. */ - rangeEnd?: number; + description?: string; /** - * Primary display label for the picker row. Falls back to `insertText` when absent. + * Logo. */ - label?: string; + logo?: string; /** - * Render-kind hint for the picker row (e.g. `"document"`, `"directory"`), derived from the host's display kind. + * Tier. */ - kind?: string; + tier?: string; + /** + * Release tag. + */ + releaseTag?: string; + status: ConnectorCatalogStatus; } /** - * Params to attach or detach an in-process ExtensionController delegate. + * Entries for the selected account. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConfigureSessionExtensionsParams". + * via the `definition` "ConnectorDiscoveryCatalogResult". */ /** @experimental */ -/** @internal */ -export interface ConfigureSessionExtensionsParams { +export interface ConnectorDiscoveryCatalogResult { /** - * Session to attach the extension controller delegate to. + * Opaque account ID. */ - sessionId: string; + accountId: string; /** - * 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. - * - * @internal - * - * @internal + * Revision. */ - controller?: OpaqueInProcessValue; + revision: number; + /** + * Refresh time in Unix epoch milliseconds. + */ + refreshedAtMs: number; + /** + * Entries. + */ + connectors: ConnectorDiscoveryCatalogEntry[]; } /** - * Identity of the integrating host, declared once on the `server.connect` handshake so telemetry from this connection is attributed to a single, consistent surface. All fields are optional; omit them to keep the default attribution. + * Requests authoritative Connector-to-MCP reconciliation for the pinned account. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectClientInfo". + * via the `definition` "ConnectorReconcileRequest". */ /** @experimental */ -/** @internal */ -export interface ConnectClientInfo { +export interface ConnectorReconcileRequest { /** - * Name of the host editor, e.g. `"vscode"`. + * Opaque account selection ID. It must match the account already pinned to the session, if any. */ - editorName?: string; + accountId: string; /** - * Version of the host editor, e.g. `"1.124.2"`. Ignored unless it looks like a version string. + * When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. */ - editorVersion?: string; + refreshCatalog?: boolean; /** - * Name of the Copilot extension within the host, e.g. `"copilot-chat"`. + * Optional Connector name to reinitialize. Requires the targetedReconcile capability. */ - extensionName?: string; + forceConnectorName?: string; +} +/** + * Session account selection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorSessionAccount". + */ +/** @experimental */ +export interface ConnectorSessionAccount { /** - * Version of the Copilot extension within the host, e.g. `"0.54.0"`. Ignored unless it looks like a version string. + * Opaque session-scoped account selection ID. */ - extensionVersion?: string; + accountId: string; + authInfo: AuthIdentityMetadata; } /** - * Metadata for a connected remote session. + * Remote session connection parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectedRemoteSessionMetadata". + * via the `definition` "ConnectRemoteSessionParams". */ /** @experimental */ -export interface ConnectedRemoteSessionMetadata { +export interface ConnectRemoteSessionParams { /** - * SDK session ID for the connected remote session. + * Session ID to connect to. */ sessionId: string; +} +/** + * Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectRequest". + */ +/** @experimental */ +/** @internal */ +export interface ConnectRequest { /** - * Optional friendly session name. + * 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. */ - name?: string; + enableGitHubTelemetryForwarding?: boolean; + clientInfo?: ConnectClientInfo; /** - * Optional session summary. + * Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. */ - summary?: string; + supportedTaskKinds?: TaskKind[]; /** - * Session start time as an ISO 8601 string. + * Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ - startTime: string; + token?: string; +} +/** + * Handshake result reporting the server's protocol version and package version on success. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectResult". + */ +/** @experimental */ +/** @internal */ +export interface ConnectResult { /** - * Last session update time as an ISO 8601 string. + * Always true on success */ - modifiedTime: string; - repository: ConnectedRemoteSessionMetadataRepository; + ok: true; /** - * Pull request number associated with the session. + * Server protocol version number */ - pullRequestNumber?: number; + protocolVersion: number; /** - * Original remote resource identifier. + * Server package version */ - resourceId?: string; - kind: ConnectedRemoteSessionMetadataKind; + version: string; /** - * Remote session staleness deadline as an ISO 8601 string. + * Task kinds the server may return to this connection. */ - staleAt?: string; + taskKinds?: TaskKind[]; +} +/** + * Local file system absolute paths within the session working directory to check against its content-exclusion policy. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ContentExclusionCheckPathsRequest". + */ +/** @experimental */ +export interface ContentExclusionCheckPathsRequest { /** - * Remote session state returned by the backing service. + * Local file system absolute paths within the session working directory to check. Results are returned in the same order, including duplicates. */ - state?: string; + paths: string[]; } /** - * Repository associated with the connected remote session. + * Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectedRemoteSessionMetadataRepository". + * via the `definition` "ContentExclusionCheckPathsResult". */ /** @experimental */ -export interface ConnectedRemoteSessionMetadataRepository { +export interface ContentExclusionCheckPathsResult { /** - * Repository owner or organization login. + * 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. */ - owner: string; + available: boolean; /** - * Repository name. + * Per-path decisions in request order. Empty when available is false. */ - name: string; + checks: ContentExclusionPathCheck[]; +} +/** + * Content-exclusion decision for one requested path. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ContentExclusionPathCheck". + */ +/** @experimental */ +export interface ContentExclusionPathCheck { /** - * Branch associated with the remote session. + * The path supplied by the caller. */ - branch: string; + path: string; + /** + * Whether the session's complete content-exclusion policy excludes the path. + */ + excluded: boolean; } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * A single large message currently in context. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorAccountRequest". + * via the `definition` "ContextHeaviestMessage". */ /** @experimental */ -export interface ConnectorAccountRequest { +export interface ContextHeaviestMessage { /** - * Opaque account selection ID previously returned by an account discovery API. + * Stable identifier for this message within the snapshot. */ - accountId: string; + id: string; + /** + * Human-readable source label, e.g. `tool: bash` or `skill: tmux`. Presentation-only. + */ + label: string; + /** + * Role of the chat message (`user`, `assistant`, or `tool`). + */ + role: string; + /** + * Token count currently in context for this individual message. + */ + tokens: number; } /** - * Account-targeted authorization update required by the Connector service. The account ID is an opaque host routing identifier; no credential is included. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorAuthorizationRequirement". + * via the `definition` "CurrentModel". */ /** @experimental */ -export interface ConnectorAuthorizationRequirement { +export interface CurrentModel { /** - * Exact opaque account selection that made the Connector request. + * Currently active model identifier */ - accountId: string; - scope: ConnectorAuthorizationScope; + 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. + */ + reasoningEffort?: string; + contextTier?: ContextTier; + autoTier?: AutoTier; + /** + * Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + */ + pendingAutoTier?: AutoTier | null; + /** + * Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + */ + activatingAutoTier?: AutoTier | null; } /** - * Feature detection and hard polling limits for the EXPERIMENTAL session connector API. + * Lightweight metadata for a currently initialized session tool * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorCapabilities". + * via the `definition` "CurrentToolMetadata". */ /** @experimental */ -export interface ConnectorCapabilities { +export interface CurrentToolMetadata { /** - * Connector API contract version. + * Model-facing tool name */ - apiVersion: number; - availability: ConnectorAvailability; + name: string; + /** + * Optional MCP/config namespaced tool name + */ + namespacedName?: string; /** - * Whether connect and reconnect can return an opaque continuation for bounded consent polling. + * MCP server name for MCP-backed tools */ - consentContinuation: boolean; + mcpServerName?: string; /** - * Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + * Raw MCP tool name for MCP-backed tools */ - opaqueAccountSelection: boolean; + mcpToolName?: string; /** - * Maximum accepted polling attempts for one continuation call. + * Tool description */ - maxPollAttempts: number; + description: string; /** - * Maximum accepted delay in milliseconds between polling attempts. + * JSON Schema for tool input */ - maxPollIntervalMs: number; + input_schema?: { + [k: string]: JsonValue | undefined; + }; /** - * Maximum accepted wall-clock deadline in milliseconds for one continuation call. + * Whether the tool is loaded on demand via tool search */ - maxDeadlineMs: number; + deferLoading?: boolean; } /** - * Credential-free Connector catalog entry. + * Result of one customization reload component. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorCatalogEntry". + * via the `definition` "CustomizationReloadOutcome". */ /** @experimental */ -export interface ConnectorCatalogEntry { - /** - * Canonical Connector name used by lifecycle methods. - */ - name: string; - /** - * Untrusted display label from the service. - */ - displayName: string; - /** - * Untrusted service description, when present. - */ - description?: string; - status: ConnectorCatalogStatus; +export interface CustomizationReloadOutcome { + subsystem: CustomizationReloadSubsystem; + status: CustomizationReloadStatus; /** - * Opaque stable runtime IDs currently projected into the session for this Connector. + * Reason for a skipped component or description of a failure, when available */ - runtimeServerIds: string[]; + detail?: string | null; } /** - * Validated Connector catalog snapshot cached by the session. + * 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` "ConnectorCatalogResult". + * via the `definition` "CustomizationsReloadResult". */ /** @experimental */ -export interface ConnectorCatalogResult { +export interface CustomizationsReloadResult { /** - * Monotonically increasing session-local catalog revision. + * Warnings from skill discovery */ - revision: number; + warnings: string[]; /** - * Unix epoch milliseconds when this snapshot was accepted. + * Errors from any component that could not be refreshed */ - refreshedAtMs: number; + errors: string[]; /** - * Validated catalog entries in service order. + * Outcome of each component in reload order; a skipped component was not configured or loaded */ - connectors: ConnectorCatalogEntry[]; + outcomes: CustomizationReloadOutcome[]; } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * A file included in the session debug bundle. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorConnectRequest". + * via the `definition` "DebugCollectLogsCollectedEntry". */ /** @experimental */ -export interface ConnectorConnectRequest { +export interface DebugCollectLogsCollectedEntry { /** - * Opaque account selection ID. It must match the account already pinned to the session, if any. + * Relative path of the file in the staged bundle/archive. */ - accountId: string; + bundlePath: string; + source: DebugCollectLogsSource; /** - * Canonical Connector name from the current catalog. + * Redacted output size in bytes. */ - connectorName: string; + sizeBytes: number; } /** - * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + * A caller-provided server-local file or directory to include in the debug bundle. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorStatus". + * via the `definition` "DebugCollectLogsEntry". */ /** @experimental */ -export interface ConnectorStatus { - /** - * Connector API contract version. - */ - apiVersion: number; - availability: ConnectorAvailability; +export interface DebugCollectLogsEntry { + kind: DebugCollectLogsEntryKind; /** - * Opaque account selection pinned to this session, when one has been selected. + * Server-local source path to read. */ - accountId?: string; - catalog?: ConnectorCatalogResult; - authorizationRequirement?: ConnectorAuthorizationRequirement; + path: string; /** - * Live MCP status for every Connector-owned runtime server. + * Relative path to use inside the staged bundle/archive. */ - runtimeServers: ConnectorRuntimeStatus[]; + bundlePath: string; + redaction?: DebugCollectLogsRedaction; /** - * Number of active opaque connection continuations. + * When true, collection fails if this entry cannot be read. Defaults to false, which records the entry in `skippedEntries`. */ - pendingConnections: number; + required?: boolean; } /** - * Live status of one session-owned MCP projection. + * Built-in session diagnostics to include in the bundle. Omitted fields default to true. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorRuntimeStatus". + * via the `definition` "DebugCollectLogsInclude". */ /** @experimental */ -export interface ConnectorRuntimeStatus { +export interface DebugCollectLogsInclude { /** - * Opaque runtime server ID. + * Include the session event log (`events.jsonl`). Defaults to true. */ - runtimeServerId: string; + events?: boolean; /** - * Canonical Connector name that owns this server. + * Include process logs for the session. Defaults to true. */ - connectorName: string; - status: ConnectorMcpStatus; -} -/** - * Explicitly bounded continuation of a pending Connector connection. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorContinueRequest". - */ -/** @experimental */ -export interface ConnectorContinueRequest { + processLogs?: boolean; /** - * Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + * Include interactive shell logs written under the session's `shell-logs` directory. Defaults to true. */ - continuationId: string; + shellLogs?: boolean; /** - * Maximum catalog requests made by this call. Must be between one and the capability limit. + * Server-local path to the session's events.jsonl file. Internal callers normally omit this and let the runtime derive it from the session. */ - maxAttempts: number; + eventsPath?: string; /** - * Delay in milliseconds between attempts. Must not exceed the capability limit. + * 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. */ - pollIntervalMs: number; + currentProcessLogPath?: string; /** - * Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. + * Server-local process log directory to search when `currentProcessLogPath` is unavailable, useful for collecting logs for inactive sessions. */ - deadlineMs: number; + processLogDirectory?: string; + /** + * Maximum number of previous process logs to include. Defaults to 5. + */ + previousProcessLogLimit?: number; } /** - * Authoritative result after disconnect and MCP reconciliation. + * Options for collecting a session debug bundle with configurable redaction. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorDisconnectResult". + * via the `definition` "DebugCollectLogsRequest". */ /** @experimental */ -export interface ConnectorDisconnectResult { +export interface DebugCollectLogsRequest { + destination: DebugCollectLogsDestination; + include?: DebugCollectLogsInclude; /** - * Whether the service accepted the idempotent disconnect. + * 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. */ - disconnected: boolean; - status: ConnectorStatus; + additionalEntries?: DebugCollectLogsEntry[]; } /** - * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * Result of collecting a session debug bundle. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectorReconcileRequest". + * via the `definition` "DebugCollectLogsResult". */ /** @experimental */ -export interface ConnectorReconcileRequest { +export interface DebugCollectLogsResult { + kind: DebugCollectLogsResultKind; /** - * Opaque account selection ID. It must match the account already pinned to the session, if any. + * 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. */ - accountId: string; + path: string; /** - * When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + * Files included in the bundle. */ - refreshCatalog?: boolean; -} -/** - * Remote session connection parameters. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectRemoteSessionParams". - */ -/** @experimental */ -export interface ConnectRemoteSessionParams { + entries: DebugCollectLogsCollectedEntry[]; /** - * Session ID to connect to. + * Optional files or directories that could not be included. */ - sessionId: string; + skippedEntries?: DebugCollectLogsSkippedEntry[]; } /** - * Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch. + * An optional debug bundle entry that could not be included. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectRequest". + * via the `definition` "DebugCollectLogsSkippedEntry". */ /** @experimental */ -/** @internal */ -export interface ConnectRequest { +export interface DebugCollectLogsSkippedEntry { /** - * 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. + * Relative path requested for this bundle entry. */ - enableGitHubTelemetryForwarding?: boolean; - clientInfo?: ConnectClientInfo; + bundlePath: string; /** - * Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. + * Server-local source path that could not be read. */ - supportedTaskKinds?: TaskKind[]; + path?: string; /** - * Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN + * Reason the entry was skipped. */ - token?: string; + reason: string; } /** - * Handshake result reporting the server's protocol version and package version on success. + * MCP-specific detail for a source-discriminated diagnostic entry. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ConnectResult". + * via the `definition` "McpDiagnosticDetails". */ /** @experimental */ -/** @internal */ -export interface ConnectResult { - /** - * Always true on success - */ - ok: true; +export interface McpDiagnosticDetails { + kind: McpDiagnosticKind; /** - * Server protocol version number + * Configured MCP server name. */ - protocolVersion: number; + serverName: string; /** - * Server package version + * Fresh identifier for the MCP connection attempt, including failed starts. */ - version: string; + connectionId: string; + direction?: McpDiagnosticDirection; /** - * Task kinds the server may return to this connection. + * Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. */ - taskKinds?: TaskKind[]; + data?: string; } /** - * Local file system absolute paths within the session working directory to check against its content-exclusion policy. + * Per-source session diagnostics configuration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ContentExclusionCheckPathsRequest". + * via the `definition` "DiagnosticsConfiguration". */ /** @experimental */ -export interface ContentExclusionCheckPathsRequest { - /** - * Local file system absolute paths within the session working directory to check. Results are returned in the same order, including duplicates. - */ - paths: string[]; +export interface DiagnosticsConfiguration { + sources: DiagnosticSourcesConfiguration; } /** - * Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. + * Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiagnosticSourcesConfiguration". + */ +/** @experimental */ +export interface DiagnosticSourcesConfiguration { + mcp?: McpDiagnosticSourceConfiguration; +} +/** + * MCP diagnostic source configuration. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpDiagnosticSourceConfiguration". + */ +/** @experimental */ +export interface McpDiagnosticSourceConfiguration { + level: DiagnosticLogLevel; +} +/** + * Patch session diagnostic thresholds for explicitly supplied sources. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ContentExclusionCheckPathsResult". + * via the `definition` "DiagnosticsConfigureRequest". */ /** @experimental */ -export interface ContentExclusionCheckPathsResult { - /** - * 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. - */ - available: boolean; - /** - * Per-path decisions in request order. Empty when available is false. - */ - checks: ContentExclusionPathCheck[]; +export interface DiagnosticsConfigureRequest { + sources: DiagnosticSourcesConfiguration; } /** - * Content-exclusion decision for one requested path. + * Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ContentExclusionPathCheck". + * via the `definition` "DiagnosticsReadRequest". */ /** @experimental */ -export interface ContentExclusionPathCheck { +export interface DiagnosticsReadRequest { /** - * The path supplied by the caller. + * Nonempty selection of sources to read. Each source may be listed once. */ - path: string; + sources: DiagnosticSource[]; /** - * Whether the session's complete content-exclusion policy excludes the path. + * Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. */ - excluded: boolean; + cursor?: string; + /** + * Maximum number of records to return, from 1 through 500. Omit for 100. + */ + max?: number; + /** + * Maximum time in milliseconds to wait for a new record, from 0 through 30000. + */ + waitMs?: number; } /** - * A single large message currently in context. + * One cursor-addressed page of retained session diagnostics. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ContextHeaviestMessage". + * via the `definition` "DiagnosticsReadResult". */ /** @experimental */ -export interface ContextHeaviestMessage { +export interface DiagnosticsReadResult { /** - * Stable identifier for this message within the snapshot. + * Retained records beginning at the requested cursor. */ - id: string; + entries: DiagnosticEntry[]; /** - * Human-readable source label, e.g. `tool: bash` or `skill: tmux`. Presentation-only. + * Opaque cursor for the next independent read. */ - label: string; + cursor: string; + cursorStatus: DiagnosticCursorStatus; /** - * Role of the chat message (`user`, `assistant`, or `tool`). + * Number of records lost before this page when known. Omitted when a buffer generation change makes the count unknowable. */ - role: string; + droppedCount?: number; /** - * Token count currently in context for this individual message. + * Whether additional retained records follow this page. */ - tokens: number; + hasMore: boolean; } /** - * 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. + * Discovered extension metadata and persistent enablement state. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CurrentModel". + * via the `definition` "DiscoveredExtension". */ /** @experimental */ -export interface CurrentModel { +export interface DiscoveredExtension { /** - * Currently active model identifier + * Source-qualified ID accepted by both server and session extension enablement methods */ - modelId?: string; + id: 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. + * Human-readable extension name */ - reasoningEffort?: string; - contextTier?: ContextTier; - autoTier?: AutoTier; + name: string; /** - * Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + * Absolute path to the extension entry module, suitable for revealing it in a file manager */ - pendingAutoTier?: AutoTier | null; + path: string; + source: DiscoveredExtensionSource; /** - * Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + * Whether this extension's persistent per-ID preference is enabled */ - activatingAutoTier?: AutoTier | null; + enabled: boolean; + plugin?: DiscoveredExtensionPlugin; } /** - * Lightweight metadata for a currently initialized session tool + * Installed plugin that contributes a discovered extension. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "CurrentToolMetadata". + * via the `definition` "DiscoveredExtensionPlugin". */ /** @experimental */ -export interface CurrentToolMetadata { +export interface DiscoveredExtensionPlugin { /** - * Model-facing tool name + * Installed plugin name */ name: string; - /** - * Optional MCP/config namespaced tool name - */ - namespacedName?: string; - /** - * MCP server name for MCP-backed tools - */ - mcpServerName?: string; - /** - * Raw MCP tool name for MCP-backed tools - */ - mcpToolName?: string; - /** - * Tool description - */ - description: string; - /** - * JSON Schema for tool input - */ - input_schema?: { - [k: string]: JsonValue | undefined; - }; - /** - * Whether the tool is loaded on demand via tool search - */ - deferLoading?: boolean; } /** - * A file included in the session debug bundle. + * Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DebugCollectLogsCollectedEntry". + * via the `definition` "DiscoveredExtensions". */ /** @experimental */ -export interface DebugCollectLogsCollectedEntry { - /** - * Relative path of the file in the staged bundle/archive. - */ - bundlePath: string; - source: DebugCollectLogsSource; +export interface DiscoveredExtensions { /** - * Redacted output size in bytes. + * Discovered user and enabled installed-plugin extensions from persisted Copilot home state */ - sizeBytes: number; + extensions: DiscoveredExtension[]; + mode: DiscoveredExtensionMode; } /** - * A caller-provided server-local file or directory to include in the debug bundle. + * Source-qualified extension identifiers to persistently disable for future sessions. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DebugCollectLogsEntry". + * via the `definition` "DiscoveredExtensionsDisableRequest". */ /** @experimental */ -export interface DebugCollectLogsEntry { - kind: DebugCollectLogsEntryKind; - /** - * Server-local source path to read. - */ - path: string; +export interface DiscoveredExtensionsDisableRequest { /** - * Relative path to use inside the staged bundle/archive. + * Source-qualified user or plugin extension IDs to disable */ - bundlePath: string; - redaction?: DebugCollectLogsRedaction; + ids: string[]; +} +/** + * Source-qualified extension identifiers to persistently enable for future sessions. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredExtensionsEnableRequest". + */ +/** @experimental */ +export interface DiscoveredExtensionsEnableRequest { /** - * When true, collection fails if this entry cannot be read. Defaults to false, which records the entry in `skippedEntries`. + * Source-qualified user or plugin extension IDs to enable */ - required?: boolean; + ids: string[]; } /** - * Built-in session diagnostics to include in the bundle. Omitted fields default to true. + * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DebugCollectLogsInclude". + * via the `definition` "DiscoveredHook". */ /** @experimental */ -export interface DebugCollectLogsInclude { +export interface DiscoveredHook { /** - * Include the session event log (`events.jsonl`). Defaults to true. + * Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. */ - events?: boolean; + id: string; + hookType: HookType; + origin: HookOrigin; /** - * Include process logs for the session. Defaults to true. + * Human-readable source label, such as a hook file path, settings source, or plugin name. */ - processLogs?: boolean; + source?: string; /** - * Include interactive shell logs written under the session's `shell-logs` directory. Defaults to true. + * Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. */ - shellLogs?: boolean; + projectPath?: string; /** - * Server-local path to the session's events.jsonl file. Internal callers normally omit this and let the runtime derive it from the session. + * Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. */ - eventsPath?: string; + enabled: boolean; /** - * 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. + * Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. */ - currentProcessLogPath?: string; + disableKey?: string; +} +/** + * MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredMcpServer". + */ +/** @experimental */ +export interface DiscoveredMcpServer { /** - * Server-local process log directory to search when `currentProcessLogPath` is unavailable, useful for collecting logs for inactive sessions. + * Server name (config key) */ - processLogDirectory?: string; + name: string; + type?: DiscoveredMcpServerType; + source: McpServerSource; /** - * Maximum number of previous process logs to include. Defaults to 5. + * Plugin name that provided this server, when source is plugin. */ - previousProcessLogLimit?: number; + sourcePlugin?: string; + /** + * Plugin version that provided this server, when source is plugin. + */ + sourcePluginVersion?: string; + effectiveSource?: McpSourceRef; + /** + * Whether the server is enabled (not in the disabled list) + */ + enabled: boolean; } /** - * Options for collecting a session debug bundle with configurable redaction. + * Canonical identity and location of the effective MCP server declaration. The declaration is uniquely addressed by this source id together with the discovered server name. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DebugCollectLogsRequest". + * via the `definition` "McpSourceRef". */ /** @experimental */ -export interface DebugCollectLogsRequest { - destination: DebugCollectLogsDestination; - include?: DebugCollectLogsInclude; +export interface McpSourceRef { /** - * 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. + * Open source-kind identifier. Known values include user, workspace, invocation, plugin, builtin, and device-registry. */ - additionalEntries?: DebugCollectLogsEntry[]; + kind: string; + /** + * Opaque stable identity for the configuration source. Clients must not parse this value. + */ + id: string; + /** + * Open semantic editability identifier. Known values are editable and read-only. + */ + editability: string; + file?: McpSourceFile; + plugin?: McpSourcePlugin; } /** - * Result of collecting a session debug bundle. + * Concrete configuration file containing an MCP server declaration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DebugCollectLogsResult". + * via the `definition` "McpSourceFile". */ /** @experimental */ -export interface DebugCollectLogsResult { - kind: DebugCollectLogsResultKind; - /** - * 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. - */ - path: string; +export interface McpSourceFile { /** - * Files included in the bundle. + * Canonical file URI for the configuration document */ - entries: DebugCollectLogsCollectedEntry[]; + uri: string; /** - * Optional files or directories that could not be included. + * RFC 6901 JSON Pointer to the server declaration, when known */ - skippedEntries?: DebugCollectLogsSkippedEntry[]; + jsonPointer?: string; } /** - * An optional debug bundle entry that could not be included. + * Plugin identity associated with an MCP server declaration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DebugCollectLogsSkippedEntry". + * via the `definition` "McpSourcePlugin". */ /** @experimental */ -export interface DebugCollectLogsSkippedEntry { +export interface McpSourcePlugin { /** - * Relative path requested for this bundle entry. + * Canonical plugin identity */ - bundlePath: string; + id: string; /** - * Server-local source path that could not be read. + * Human-readable plugin name, when available */ - path?: string; + name?: string; /** - * Reason the entry was skipped. + * Plugin version, when available */ - reason: string; + version?: string; } /** - * MCP-specific detail for a source-discriminated diagnostic entry. + * 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` "McpDiagnosticDetails". + * via the `definition` "DiscoveredModel". */ /** @experimental */ -export interface McpDiagnosticDetails { - kind: McpDiagnosticKind; +export interface DiscoveredModel { /** - * Configured MCP server name. + * Provider-native model identifier. */ - serverName: string; + id: string; /** - * Fresh identifier for the MCP connection attempt, including failed starts. + * Provider-reported display name. */ - connectionId: string; - direction?: McpDiagnosticDirection; + name?: string; + provenance: ModelProviderProvenance; /** - * Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. + * Provider-reported artifact digest. */ - data?: string; -} -/** - * Per-source session diagnostics configuration. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiagnosticsConfiguration". - */ -/** @experimental */ -export interface DiagnosticsConfiguration { - sources: DiagnosticSourcesConfiguration; + 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[]; } /** - * Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. + * Attribution for the adapter that produced a provider row. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiagnosticSourcesConfiguration". + * via the `definition` "ModelProviderProvenance". */ /** @experimental */ -export interface DiagnosticSourcesConfiguration { - mcp?: McpDiagnosticSourceConfiguration; +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; } /** - * MCP diagnostic source configuration. + * Provider-reported model artifact metadata. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpDiagnosticSourceConfiguration". + * via the `definition` "ModelArtifactDetails". */ /** @experimental */ -export interface McpDiagnosticSourceConfiguration { - level: DiagnosticLogLevel; +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; } /** - * Patch session diagnostic thresholds for explicitly supplied sources. + * Model capabilities and limits * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiagnosticsConfigureRequest". + * via the `definition` "ModelCapabilities". */ /** @experimental */ -export interface DiagnosticsConfigureRequest { - sources: DiagnosticSourcesConfiguration; +export interface ModelCapabilities { + supports?: ModelCapabilitiesSupports; + limits?: ModelCapabilitiesLimits; } /** - * Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). + * Feature flags indicating what the model supports * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiagnosticsReadRequest". + * via the `definition` "ModelCapabilitiesSupports". */ /** @experimental */ -export interface DiagnosticsReadRequest { +export interface ModelCapabilitiesSupports { /** - * Nonempty selection of sources to read. Each source may be listed once. + * Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. */ - sources: DiagnosticSource[]; + thinking?: boolean; /** - * Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. + * Whether this model supports vision/image input */ - cursor?: string; + vision?: boolean; /** - * Maximum number of records to return, from 1 through 500. Omit for 100. + * Whether this model supports canonical tool calling */ - max?: number; + toolCalls?: boolean; /** - * Maximum time in milliseconds to wait for a new record, from 0 through 30000. + * Whether this model supports reasoning effort configuration */ - waitMs?: number; + reasoningEffort?: boolean; + adaptive_thinking?: AdaptiveThinkingSupport; } /** - * One cursor-addressed page of retained session diagnostics. + * Token limits for prompts, outputs, and context window * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiagnosticsReadResult". + * via the `definition` "ModelCapabilitiesLimits". */ /** @experimental */ -export interface DiagnosticsReadResult { - /** - * Retained records beginning at the requested cursor. - */ - entries: DiagnosticEntry[]; +export interface ModelCapabilitiesLimits { /** - * Opaque cursor for the next independent read. + * Maximum number of prompt/input tokens */ - cursor: string; - cursorStatus: DiagnosticCursorStatus; + max_prompt_tokens?: number; /** - * Number of records lost before this page when known. Omitted when a buffer generation change makes the count unknowable. + * Maximum number of output/completion tokens */ - droppedCount?: number; + max_output_tokens?: number; /** - * Whether additional retained records follow this page. + * Maximum total context window size in tokens */ - hasMore: boolean; + max_context_window_tokens?: number; + vision?: ModelCapabilitiesLimitsVision; } /** - * Discovered extension metadata and persistent enablement state. + * Vision-specific limits * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiscoveredExtension". + * via the `definition` "ModelCapabilitiesLimitsVision". */ /** @experimental */ -export interface DiscoveredExtension { - /** - * Source-qualified ID accepted by both server and session extension enablement methods - */ - id: string; +export interface ModelCapabilitiesLimitsVision { /** - * Human-readable extension name + * MIME types the model accepts */ - name: string; + supported_media_types: string[]; /** - * Absolute path to the extension entry module, suitable for revealing it in a file manager + * Maximum number of images per prompt */ - path: string; - source: DiscoveredExtensionSource; + max_prompt_images: number; /** - * Whether this extension's persistent per-ID preference is enabled + * Maximum image size in bytes */ - enabled: boolean; - plugin?: DiscoveredExtensionPlugin; + max_prompt_image_size: number; } /** - * Installed plugin that contributes a discovered extension. + * A non-fatal provider observation warning. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiscoveredExtensionPlugin". + * via the `definition` "ModelProviderWarning". */ /** @experimental */ -export interface DiscoveredExtensionPlugin { +export interface ModelProviderWarning { /** - * Installed plugin name + * Machine-readable warning code. */ - name: string; -} -/** - * Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiscoveredExtensions". - */ -/** @experimental */ -export interface DiscoveredExtensions { + code: string; /** - * Discovered user and enabled installed-plugin extensions from persisted Copilot home state + * Human-readable warning message. */ - extensions: DiscoveredExtension[]; - mode: DiscoveredExtensionMode; + message: string; } /** - * Source-qualified extension identifiers to persistently disable for future sessions. + * 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` "DiscoveredExtensionsDisableRequest". + * via the `definition` "DiscoveredModelList". */ /** @experimental */ -export interface DiscoveredExtensionsDisableRequest { +export interface DiscoveredModelList { + outcome: ModelProviderOperationOutcome; /** - * Source-qualified user or plugin extension IDs to disable + * Provider-native models in provider order. */ - ids: string[]; + models: DiscoveredModel[]; } /** - * Source-qualified extension identifiers to persistently enable for future sessions. + * 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` "DiscoveredExtensionsEnableRequest". + * via the `definition` "ModelProviderOperationOutcome". */ /** @experimental */ -export interface DiscoveredExtensionsEnableRequest { +export interface ModelProviderOperationOutcome { + code: ModelProviderOperationOutcomeCode; /** - * Source-qualified user or plugin extension IDs to enable + * Human-readable detail for non-success outcomes. */ - ids: string[]; + message?: string; } /** - * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. + * Slash-prefixed command string to enqueue for FIFO processing. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiscoveredHook". + * via the `definition` "EnqueueCommandParams". */ /** @experimental */ -export interface DiscoveredHook { - /** - * Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. - */ - id: string; - hookType: HookType; - origin: HookOrigin; +export interface EnqueueCommandParams { /** - * Human-readable source label, such as a hook file path, settings source, or plugin name. + * 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. */ - source?: string; + command: string; /** - * Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + * Optional user-facing text for the queue row. The command string is shown when omitted. */ - projectPath?: string; + displayText?: string | null; +} + +/** @experimental */ +export interface UnsupportedEnqueueCommandResult { /** - * Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + * False because the target session does not support local command queueing (e.g. remote sessions). */ - enabled: boolean; + queued: false; /** - * Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + * Legacy null queue ID accepted for compatibility with older runtimes. */ - disableKey?: string; + queueId?: null; } /** - * MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. + * Hosting capabilities and session capacity advertised by an environment. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "DiscoveredMcpServer". + * via the `definition` "EnvironmentCapabilities". */ /** @experimental */ -export interface DiscoveredMcpServer { +export interface EnvironmentCapabilities { /** - * Server name (config key) + * Advertised Agent Host Protocol version. */ - name: string; - type?: DiscoveredMcpServerType; - source: McpServerSource; + ahpVersion?: string; /** - * Plugin name that provided this server, when source is plugin. + * Feature identifiers advertised by the environment. */ - sourcePlugin?: string; + features: string[]; /** - * Plugin version that provided this server, when source is plugin. + * Maximum session capacity, when advertised. */ - sourcePluginVersion?: string; - effectiveSource?: McpSourceRef; + maxSessions?: number; /** - * Whether the server is enabled (not in the disabled list) + * Current session count, when advertised. */ - enabled: boolean; + currentSessions?: number; } /** - * Canonical identity and location of the effective MCP server declaration. The declaration is uniquely addressed by this source id together with the discovered server name. + * Identify a user-managed Mission Control environment to delete. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpSourceRef". + * via the `definition` "EnvironmentsDeleteRequest". */ /** @experimental */ -export interface McpSourceRef { +export interface EnvironmentsDeleteRequest { /** - * Open source-kind identifier. Known values include user, workspace, invocation, plugin, builtin, and device-registry. - */ - kind: string; - /** - * Opaque stable identity for the configuration source. Clients must not parse this value. + * User-managed environment to delete. GitHub-managed environments cannot be deleted. */ - id: string; + 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 { /** - * Open semantic editability identifier. Known values are editable and read-only. + * Identifier assigned by Mission Control. */ - editability: string; - file?: McpSourceFile; - plugin?: McpSourcePlugin; + environmentId: string; } /** - * Concrete configuration file containing an MCP server declaration. + * Safe discovery information for the requested environment. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpSourceFile". + * via the `definition` "EnvironmentsGetResult". */ /** @experimental */ -export interface McpSourceFile { - /** - * Canonical file URI for the configuration document - */ - uri: string; - /** - * RFC 6901 JSON Pointer to the server declaration, when known - */ - jsonPointer?: string; +export interface EnvironmentsGetResult { + environment: GitHubEnvironment; } /** - * Plugin identity associated with an MCP server declaration. + * Safe discovery information. Host-side relay bootstrap credentials are never included. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpSourcePlugin". + * via the `definition` "GitHubEnvironment". */ /** @experimental */ -export interface McpSourcePlugin { +export interface GitHubEnvironment { /** - * Canonical plugin identity + * Identifier assigned by Mission Control. */ id: string; /** - * Human-readable plugin name, when available + * Human-readable environment name. */ - name?: string; + name: string; + kind: EnvironmentKind; /** - * Plugin version, when available + * Open-ended operational status vocabulary. */ - version?: string; + 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; } /** - * Slash-prefixed command string to enqueue for FIFO processing. + * Optional discovery filters supported by GitHub Mission Control. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EnqueueCommandParams". + * via the `definition` "EnvironmentsListRequest". */ /** @experimental */ -export interface 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. - */ - command: string; +export interface EnvironmentsListRequest { + kind?: EnvironmentKind; /** - * Optional user-facing text for the queue row. The command string is shown when omitted. + * Operational status, such as online, offline, degraded, waking, or draining. */ - displayText?: string | null; + 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 UnsupportedEnqueueCommandResult { - /** - * False because the target session does not support local command queueing (e.g. remote sessions). - */ - queued: false; +export interface EnvironmentsListResult { /** - * Legacy null queue ID accepted for compatibility with older runtimes. + * Safe discovery records without relay bootstrap credentials. */ - queueId?: null; + environments: GitHubEnvironment[]; } /** * Cursor, batch size, and optional long-poll/filter parameters for reading session events. @@ -9578,1794 +11861,2054 @@ export interface ExternalToolTextResultForLlmContentResource { resource: ExternalToolTextResultForLlmContentResourceDetails; } /** - * Parameters for cooperatively aborting a factory body. + * Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryAbortRequest". + * via the `definition` "FleetStartRequest". */ /** @experimental */ -export interface FactoryAbortRequest { +export interface FleetStartRequest { /** - * Target session identifier + * Optional user prompt to combine with fleet instructions */ - sessionId: string; + prompt?: string; /** - * Factory run identifier. + * Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request */ - runId: string; + attachments?: Attachment[]; /** - * Opaque token identifying the execution attempt to abort. + * If false, this request will not trigger a Premium Request Unit charge. User requests default to billable. + * + * @internal */ - executionToken: string; + billable?: boolean; + /** + * If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. + */ + wait?: boolean; } /** - * Acknowledgement that a factory request was accepted. + * Indicates whether fleet mode was successfully activated. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryAckResult". + * via the `definition` "FleetStartResult". */ /** @experimental */ -export interface FactoryAckResult {} +export interface FleetStartResult { + /** + * Whether fleet mode was successfully activated + */ + started: boolean; +} /** - * Options for one factory-scoped subagent call. + * Folder path to add to trusted folders. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryAgentOptions". + * via the `definition` "FolderTrustAddParams". */ /** @experimental */ -export interface FactoryAgentOptions { +export interface FolderTrustAddParams { /** - * Optional label distinguishing otherwise identical memoized agent calls. + * Folder path to mark as trusted */ - label?: string; + path: string; +} +/** + * Folder path to check for trust. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "FolderTrustCheckParams". + */ +/** @experimental */ +export interface FolderTrustCheckParams { /** - * Optional JSON Schema for structured agent output. + * Folder path to check */ - schema?: JsonValue; + path: string; +} +/** + * Folder trust check result. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "FolderTrustCheckResult". + */ +/** @experimental */ +export interface FolderTrustCheckResult { /** - * Optional model identifier for the subagent. + * Whether the folder is trusted */ - model?: string; + trusted: boolean; +} +/** + * The remote the checked-out branch tracks. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitCurrentBranchRemoteResult". + */ +/** @experimental */ +/** @internal */ +export interface GitCurrentBranchRemoteResult { /** - * Optional reasoning effort override for the subagent. + * Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. */ - reasoningEffort?: string; - contextTier?: ContextTier; + remote: string; +} +/** + * Working-tree path a git query applies to. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitCwdRequest". + */ +/** @experimental */ +/** @internal */ +export interface GitCwdRequest { /** - * Optional built-in or custom agent name whose definition configures the subagent. + * Absolute path to a directory inside the git working tree to query. */ - agent?: string; + cwd: string; } /** - * Parameters for one factory-scoped subagent call. + * A GitHub login the authenticated user may act as: their own account, or an organization they belong to. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryAgentRequest". + * via the `definition` "GitHubOwnerOption". */ /** @experimental */ -export interface FactoryAgentRequest { +/** @internal */ +export interface GitHubOwnerOption { /** - * Factory run identifier that owns the subagent. + * The owner's GitHub login. */ - factoryRunId: string; + login: string; /** - * Opaque token identifying the current factory execution attempt. + * Which kind of owner this is. The authenticated user's own account is always reported as `user`. */ - executionToken: string; + type: string; +} +/** + * The owner listing to abandon. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubOwnersCancelRequest". + */ +/** @experimental */ +/** @internal */ +export interface GitHubOwnersCancelRequest { /** - * Prompt to send to the subagent. + * Request id the listing was started with. */ - prompt: string; - opts: FactoryAgentOptions; + requestId: number; } /** - * Result of one factory-scoped subagent call. + * Whether the id named a running owner listing. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryAgentResult". + * via the `definition` "GitHubOwnersCancelResult". */ /** @experimental */ -export interface FactoryAgentResult { +/** @internal */ +export interface GitHubOwnersCancelResult { /** - * Agent result, omitted when the agent produced no result. + * True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. */ - result?: JsonValue; + canceled: boolean; } /** - * Prompt-safe durable identity and live status for a direct factory agent. + * Credential to list owners under, and the request id that makes the listing cancellable. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryAgentSummary". + * via the `definition` "GitHubOwnersListRequest". */ /** @experimental */ -export interface FactoryAgentSummary { +/** @internal */ +export interface GitHubOwnersListRequest { /** - * Stable direct-agent identifier. + * Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. */ - agentId: string; + requestId: number; /** - * Tool-call identifier that launched the agent. + * The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. */ - toolCallId: string; + authInfo: JsonValue; +} +/** + * Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubOwnersListResult". + */ +/** @experimental */ +/** @internal */ +export interface GitHubOwnersListResult { /** - * Owning factory run identifier. + * The owners, on success: the authenticated user first, then the organizations they belong to. */ - runId: string; + owners?: GitHubOwnerOption[]; /** - * Phase identifier active when the agent was launched, or null. + * Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. */ - phaseId: string | null; + message?: string; /** - * Friendly, non-unique name intended for display + * A line the caller should log. Present only alongside `message`, and only for failures worth recording. */ - label: string; + warning?: string; /** - * Friendly, non-unique name intended for display + * A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. */ - displayName?: string; + throwError?: string; +} +/** + * A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubOwnersRequestIdResult". + */ +/** @experimental */ +/** @internal */ +export interface GitHubOwnersRequestIdResult { /** - * Registered agent type. + * Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. */ - agentType: string; + requestId: number; +} +/** + * Working-tree path whose owning GitHub repository should be resolved. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubRepositoryAtPathRequest". + */ +/** @experimental */ +/** @internal */ +export interface GitHubRepositoryAtPathRequest { /** - * Current durable or live agent status. + * Absolute path to a directory inside the git working tree to resolve. */ - status: string; + path: string; +} +/** + * The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubRepositoryAtPathResult". + */ +/** @experimental */ +/** @internal */ +export interface GitHubRepositoryAtPathResult { /** - * Model requested when the agent was launched. + * Resolved repository identity, or null when the selected remote resolves to no GitHub host. + */ + repository?: GitHubRepositoryIdentity | null; +} +/** + * Owner, name, and host of a GitHub repository, as resolved from a git remote URL. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubRepositoryIdentity". + */ +/** @experimental */ +export interface GitHubRepositoryIdentity { + /** + * Repository owner login (user or organization). + */ + owner: string; + /** + * Repository name, without the owner prefix or the `.git` suffix. + */ + name: string; + /** + * Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + */ + host: string; +} +/** + * Client environment metadata describing the process that produced a telemetry event. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubTelemetryClientInfo". + */ +/** @experimental */ +export interface GitHubTelemetryClientInfo { + /** + * Copilot CLI version string. + */ + cli_version: string; + /** + * Operating system platform (e.g. darwin, linux, win32). + */ + os_platform: string; + /** + * Operating system version string. + */ + os_version: string; + /** + * Operating system architecture (e.g. arm64, x64). + */ + os_arch: string; + /** + * Node.js runtime version string. + */ + node_version: string; + /** + * Copilot subscription plan, when known. + */ + copilot_plan?: string; + /** + * Type of client. */ - requestedModel?: string; + client_type?: string; /** - * Concrete model resolved for the agent. + * Name of the client application. */ - resolvedModel?: string; + client_name?: string; /** - * Epoch milliseconds when the agent started. + * Whether the user is a GitHub/Microsoft staff member. */ - startedAt?: number; + is_staff?: boolean; /** - * Epoch milliseconds when the agent completed. + * Stable machine identifier for the device. */ - completedAt?: number; + dev_device_id?: string; /** - * Accumulated active agent time in milliseconds. + * Distinct CPU model names for the host, comma-separated. */ - activeMs: number; + cpu_model?: string; /** - * Prompt-safe live activity text. + * Number of logical CPU cores on the host. */ - activity?: string; + cpu_count?: number; } /** - * Parameters for cancelling a factory run. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryCancelRequest". + * via the `definition` "GitHubTelemetryEvent". */ /** @experimental */ -export interface FactoryCancelRequest { +export interface GitHubTelemetryEvent { /** - * Factory run identifier. + * Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). */ - runId: string; -} -/** - * Current factory phase identity. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryCurrentPhase". - */ -/** @experimental */ -export interface FactoryCurrentPhase { + kind: string; /** - * Current phase identifier. + * Timestamp when the event was created (ISO 8601 format). */ - id: string; + created_at?: string; /** - * Zero-based declared phase ordinal, or null for an undeclared phase. + * Reference to the model call that produced this event. */ - ordinal: number | null; -} -/** - * Declared or approved factory resource ceilings. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryDeclaredLimits". - */ -/** @experimental */ -export interface FactoryDeclaredLimits { + model_call_id?: string; /** - * Maximum concurrently active subagents. + * String-valued properties as a map from key to value. */ - maxConcurrentSubagents?: number; + properties: { + [k: string]: string | undefined; + }; /** - * Maximum total subagents spawned by the run. + * Numeric metrics as a map from key to value. */ - maxTotalSubagents?: number; + metrics: { + [k: string]: number | undefined; + }; /** - * Maximum accumulated active execution time in seconds. + * Experiment assignment context. */ - timeoutSeconds?: number; + exp_assignment_context?: string; /** - * Maximum AI credits consumed by subagents and descendants. + * Feature flags enabled for this session, as a map from flag to value. */ - maxAiCredits?: number; + features?: { + [k: string]: string | undefined; + }; + /** + * Session identifier the event belongs to. + */ + session_id?: string; + /** + * Copilot tracking ID for user-level attribution. + */ + copilot_tracking_id?: string; + client?: GitHubTelemetryClientInfo; } /** - * Parameters sent to the owning extension to execute a factory closure. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryExecuteRequest". + * via the `definition` "GitHubTelemetryNotification". */ /** @experimental */ -export interface FactoryExecuteRequest { - /** - * Target session identifier - */ - sessionId: string; - /** - * Registered factory name. - */ - name: string; - /** - * Factory run identifier. - */ - runId: string; +export interface GitHubTelemetryNotification { /** - * Opaque token identifying this factory execution attempt. + * 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. */ - executionToken: string; + sessionId?: string; /** - * Factory input value. + * Whether this is a restricted telemetry event (cli.restricted_telemetry). Hosts must route restricted events to first-party Microsoft stores only. */ - args: JsonValue; + restricted: boolean; + event: GitHubTelemetryEvent; } /** - * Result returned by an extension factory closure. + * Asks the SDK client to acquire a GitHub access token from an opaque callback registration. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryExecuteResult". + * via the `definition` "GitHubTokenAcquireRequest". */ /** @experimental */ -export interface FactoryExecuteResult { +export interface GitHubTokenAcquireRequest { /** - * Factory result value. + * Opaque identifier generated by the SDK for this callback registration. */ - result?: JsonValue; + registrationId: string; + /** + * Effective GitHub host for which the callback must return a token. + */ + host: string; + /** + * Session receiving the token. Absent only before a cloud session has been assigned its id. + */ + sessionId?: string; + reason: GitHubTokenAcquireReason; } /** - * Parameters for paging factory progress. + * A GitHub repository one of a working tree's remotes points at. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryGetRunProgressRequest". + * via the `definition` "GitRemoteRepository". */ /** @experimental */ -export interface FactoryGetRunProgressRequest { - /** - * Factory run identifier. - */ - runId: string; +/** @internal */ +export interface GitRemoteRepository { /** - * Optional phase identifier used to scope records and cursors. + * Account or organization owning the repository. */ - phaseId?: string; + owner: string; /** - * Exclusive forward cursor. + * Repository name, without the owner. */ - afterSeq?: number; + name: string; /** - * Exclusive backward cursor. + * GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. */ - beforeSeq?: number; + host: string; /** - * Maximum records to return. Defaults to 200 and is capped at 500. + * Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. */ - limit?: number; + remoteName: string; } /** - * Parameters for retrieving a factory run. + * Git working tree whose GitHub remotes should be listed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryGetRunRequest". + * via the `definition` "GitReposFromRemotesRequest". */ /** @experimental */ -export interface FactoryGetRunRequest { +/** @internal */ +export interface GitReposFromRemotesRequest { /** - * Factory run identifier. + * Absolute path to the root of the git working tree. */ - runId: string; + gitRoot: string; } /** - * Parameters for reading a factory journal entry. + * The GitHub repositories a working tree's remotes point at. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryJournalGetRequest". + * via the `definition` "GitReposFromRemotesResult". */ /** @experimental */ -export interface FactoryJournalGetRequest { - /** - * Factory run identifier. - */ - runId: string; - /** - * Opaque token identifying the current factory execution attempt. - */ - executionToken: string; +/** @internal */ +export interface GitReposFromRemotesResult { /** - * Namespaced journal key. + * One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. */ - key: string; + repositories: GitRemoteRepository[]; } /** - * Result of reading a factory journal entry. + * Selects the configuration directory whose machine-wide state to read. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryJournalGetResult". + * via the `definition` "GlobalStateLoadForConfigDirRequest". */ /** @experimental */ -export interface FactoryJournalGetResult { - /** - * Whether the journal contained the requested key. - */ - hit: boolean; +/** @internal */ +export interface GlobalStateLoadForConfigDirRequest { /** - * Cached JSON result. The hit field distinguishes a cached JSON null from a miss. + * Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. */ - resultJson?: JsonValue; + configDir?: string; } /** - * Parameters for storing a factory journal entry. + * The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryJournalPutRequest". + * via the `definition` "GlobalStateLoadResult". */ /** @experimental */ -export interface FactoryJournalPutRequest { - /** - * Factory run identifier. - */ - runId: string; - /** - * Opaque token identifying the current factory execution attempt. - */ - executionToken: string; +/** @internal */ +export interface GlobalStateLoadResult { /** - * Namespaced journal key. + * Plugins installed on this machine. */ - key: string; + installedPlugins?: InstalledPlugin[]; /** - * JSON result to memoize. + * Models the user selected recently, most recent first. */ - resultJson: JsonValue; -} -/** - * Parameters for paging factory runs. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryListRunsRequest". - */ -/** @experimental */ -export interface FactoryListRunsRequest { + recentModelIds?: string[]; /** - * Exclusive forward cursor. + * When the host first ran on this machine. */ - afterSeq?: number; + firstLaunchAt?: string; /** - * Exclusive backward cursor. + * Terminals the user has already been asked to set up, so the host does not ask twice. */ - beforeSeq?: number; + askedSetupTerminals?: string[]; /** - * Maximum terminal runs to return. Defaults to 200 and is capped at 500. + * Folders where the user declined the init prompt, so it stays hidden there. */ - limit?: number; -} -/** - * A page of factory runs in durable creation order. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryListRunsResult". - */ -/** @experimental */ -export interface FactoryListRunsResult { + suppressInitFolders?: string[]; /** - * Factory run summaries in durable creation order. + * Whether the sandbox onboarding has been shown. */ - runs: FactoryRunSummary[]; + sandboxOnboardingShown?: boolean; /** - * Oldest terminal-run cursor in this page, or null when the terminal window is empty. + * Whether the user declined to trust the sandbox credential proxy CA. */ - oldestSeq?: number | null; + sandboxCredentialProxyCaDeclined?: boolean; /** - * Newest terminal-run cursor in this page, or null when the terminal window is empty. + * Whether the app tip has been shown. */ - newestSeq?: number | null; + appTipShown?: boolean; /** - * Whether terminal runs newer than this page exist. + * Whether the one-off cleanup of stored reasoning summaries has run. */ - hasMoreNewer?: boolean; + reasoningSummariesCleanupDone?: boolean; + lastLoggedInUser?: LoggedInUser; /** - * Number of terminal runs older than this page. + * Every account the host has signed in to on this machine. */ - omittedOlder?: number; -} -/** - * Durable factory run summary with read-time live overlays. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunSummary". - */ -/** @experimental */ -export interface FactoryRunSummary { + loggedInUsers?: LoggedInUser[]; /** - * Factory run identifier. + * Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. */ - runId: string; + staff?: boolean; /** - * Registered factory name. + * Whether the user was recognized as GitHub staff. */ - factoryName: string; + staffGithub?: boolean; /** - * Human-readable factory description. + * Whether the user was recognized as Microsoft staff. */ - description: string; - status: FactoryRunStatus; + staffMicrosoft?: boolean; /** - * Monotonic durable run revision. + * When the staff-only model reset last ran. */ - revision: number; + staffModelResetAt?: string; /** - * Epoch milliseconds when the run was created. + * When the staff-only log level migration last ran. */ - createdAt: number; + staffLogLevelMigrationAt?: string; /** - * Epoch milliseconds when execution first started, or null before start. + * When the staff-only update channel migration last ran. */ - startedAt: number | null; + staffUpdateChannelMigrationAt?: string; /** - * Epoch milliseconds when the durable run was last updated. + * Folders the user has marked as trusted. */ - updatedAt: number; + trustedFolders?: string[]; /** - * Epoch milliseconds when the run completed, or null while nonterminal. + * Whether the user has answered the prompt suggesting they install the desktop app. */ - completedAt: number | null; + appInstallNudgeResponded?: boolean; /** - * Current phase identity, or null before any phase is entered. + * When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. */ - currentPhase: FactoryCurrentPhase | null; + autoFeedbackLastPromptedAt?: string; +} +/** + * Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "InstalledPlugin". + */ +/** @experimental */ +export interface InstalledPlugin { /** - * Number of phases declared by the factory. + * Plugin name */ - declaredPhaseCount: number; + name: string; /** - * Number of direct factory agents currently live. + * Marketplace the plugin came from (empty string for direct repo installs) */ - liveAgentCount: number; + marketplace: string; /** - * Total direct factory agents spawned across all attempts. + * Version installed (if available) */ - totalSpawnedAgentCount: number; - consumed: FactoryRunConsumed; - declaredLimits: FactoryDeclaredLimits; + version?: string; /** - * Approved effective resource ceilings, or null until approved. + * Installation timestamp */ - approved: FactoryDeclaredLimits | null; + installed_at: string; /** - * Epoch milliseconds when this live-overlay snapshot was observed. + * Whether the plugin is currently enabled */ - observedAt: number; + enabled: boolean; /** - * Epoch milliseconds when the current active segment started, or null while inactive. + * Path where the plugin is cached locally */ - activeSegmentStartedAt: number | null; + cache_path?: string; + source?: InstalledPluginSource; /** - * Terminal run outcome, or null while nonterminal. + * 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. */ - terminal: FactoryRunTerminal | null; + source_sha?: string; /** - * Whether the durable run state currently passes runtime resume eligibility checks. + * 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. */ - canResume: boolean; + installed_from?: string; } /** - * Durable factory resource consumption. + * Source descriptor for a direct GitHub plugin install, with `owner/repo`, optional ref or full commit SHA, and optional subpath. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunConsumed". + * via the `definition` "InstalledPluginSourceGitHub". */ /** @experimental */ -export interface FactoryRunConsumed { +export interface InstalledPluginSourceGitHub { /** - * Accumulated active execution time in milliseconds. + * Constant value. Always "github". */ - activeMs: number; + source: "github"; + /** + * GitHub repository in `owner/repo` form. + */ + repo: string; + /** + * Optional Git ref to resolve. + */ + ref?: string; /** - * Total subagents spawned by the run. + * Optional full 40-character hexadecimal commit SHA. */ - subagents: number; + sha?: string; /** - * AI usage consumed by the run in nano-AIU. + * Optional repository-relative path to the plugin. */ - nanoAiu: number; + path?: string; } /** - * Prompt-safe terminal factory outcome. + * Source descriptor for a direct URL plugin install, with URL, optional ref or full commit SHA, and optional subpath. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunTerminal". + * via the `definition` "InstalledPluginSourceUrl". */ /** @experimental */ -export interface FactoryRunTerminal { +export interface InstalledPluginSourceUrl { /** - * Human-readable terminal reason. + * Constant value. Always "url". */ - reason?: string; - failure?: FactoryRunFailure; + source: "url"; /** - * Human-readable terminal error. + * URL of the plugin source. */ - error?: string; + url: string; /** - * Prompt-safe preview of the completed result. + * Optional Git ref to resolve. */ - resultPreview?: string; + ref?: string; /** - * Pause initiator metadata, or null when the run did not pause. + * Optional full 40-character hexadecimal commit SHA. + */ + sha?: string; + /** + * Optional source-relative path to the plugin. */ - pauseInfo: FactoryPauseInfo | null; + path?: string; } /** - * One ordered factory progress line. + * Source descriptor for a direct local plugin install, with a local filesystem path. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryLogLine". + * via the `definition` "InstalledPluginSourceLocal". */ /** @experimental */ -export interface FactoryLogLine { +export interface InstalledPluginSourceLocal { /** - * Monotonic sequence number within the factory run. + * Constant value. Always "local". */ - seq: number; - kind: FactoryLogLineKind; + source: "local"; /** - * Progress text. + * Local filesystem path to the plugin. */ - text: string; + path: string; } /** - * Parameters for recording factory progress. + * An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryLogRequest". + * via the `definition` "LoggedInUser". */ /** @experimental */ -export interface FactoryLogRequest { +/** @internal */ +export interface LoggedInUser { /** - * Factory run identifier. + * Host the account belongs to, such as `github.com` or an Enterprise server. */ - runId: string; + host: string; /** - * Opaque token identifying the current factory execution attempt. + * Account login on that host. */ - executionToken: string; + login: string; /** - * Ordered progress lines to append. + * Account kind, when the host recorded one. Consumers must tolerate new strings. + */ + kind?: string; + /** + * Source account this account was derived from, when one was recorded. */ - lines: FactoryLogLine[]; + derivedFrom?: string; } /** - * Parameters for an owned durable pause checkpoint. + * A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryPauseCheckpointRequest". + * via the `definition` "GlobalStateWriteKeyRequest". */ /** @experimental */ -export interface FactoryPauseCheckpointRequest { +/** @internal */ +export interface GlobalStateWriteKeyRequest { /** - * Factory run identifier. + * Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. */ - runId: string; + configDir?: string; /** - * Opaque token identifying the execution attempt that reached the checkpoint. + * Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. */ - executionToken: string; + key: string; /** - * Stable author-defined checkpoint key. + * Value to store for the key. Omit it, or pass null, to remove the key instead. */ - key: string; -} - -/** @experimental */ -export interface FactoryPauseCheckpointResult { - action: FactoryPauseCheckpointAction; + value?: JsonValue; } /** - * Parameters for pausing a running factory. + * Pending external tool call request ID, with the tool result or an error describing why it failed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryPauseRequest". + * via the `definition` "HandlePendingToolCallRequest". */ /** @experimental */ -export interface FactoryPauseRequest { +export interface HandlePendingToolCallRequest { /** - * Factory run identifier. + * Request ID of the pending tool call */ - runId: string; + requestId: string; + result?: ExternalToolResult; + /** + * Error message if the tool call failed + */ + error?: string; } /** - * Durable lifecycle and timing for one factory phase. + * Indicates whether the external tool call result was handled successfully. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryPhaseObservation". + * via the `definition` "HandlePendingToolCallResult". */ /** @experimental */ -export interface FactoryPhaseObservation { - /** - * Phase identifier. - */ - id: string; - /** - * Zero-based declared phase ordinal, or null for an undeclared phase. - */ - ordinal: number | null; - /** - * Human-readable phase title. - */ - title: string; - /** - * Optional human-readable phase detail. - */ - detail?: string; - status: FactoryPhaseStatus; - /** - * Most recent run attempt that entered this phase, or `0` if the phase has never been entered. - */ - lastEnteredRunAttempt: number; - /** - * Number of times execution entered this phase. - */ - entryCount: number; - /** - * Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip timestamp (equal to `completedAt`). - */ - startedAt?: number; - /** - * Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip timestamp (equal to `startedAt`). - */ - completedAt?: number; - /** - * Completed active time accumulated by this phase in milliseconds. - */ - accumulatedActiveMs: number; - /** - * Current live active time for this phase in milliseconds. - */ - currentActiveMs: number; - /** - * Total direct agents associated with this phase. - */ - totalAgentCount: number; +export interface HandlePendingToolCallResult { /** - * Direct agents in this phase that are currently live. + * Whether the tool call result was handled successfully */ - liveAgentCount: number; + success: boolean; } /** - * One durable factory progress record. + * Indicates whether an in-progress manual compaction was aborted. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryProgressLine". + * via the `definition` "HistoryAbortManualCompactionResult". */ /** @experimental */ -export interface FactoryProgressLine { - /** - * Global monotonic sequence number within the run. - */ - seq: number; - /** - * Resume attempt that emitted this record. - */ - attempt: number; - /** - * Phase active when the record was emitted, or null before any phase. - */ - phaseId: string | null; - /** - * Epoch milliseconds when the record was persisted. - */ - recordedAt: number; - kind: FactoryLogLineKind; +export interface HistoryAbortManualCompactionResult { /** - * Prompt-safe progress text. + * 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. */ - text: string; + aborted: boolean; } /** - * A bidirectional page of factory progress. + * Indicates whether an in-progress background compaction was cancelled. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryProgressPage". + * via the `definition` "HistoryCancelBackgroundCompactionResult". */ /** @experimental */ -export interface FactoryProgressPage { - /** - * Progress records in sequence order. - */ - records: FactoryProgressLine[]; - /** - * Oldest sequence number in this page, or null when empty. - */ - oldestSeq: number | null; - /** - * Newest sequence number in this page, or null when empty. - */ - newestSeq: number | null; - /** - * Whether progress records older than this page exist. - */ - hasMoreOlder: boolean; - /** - * Whether progress records newer than this page exist. - */ - hasMoreNewer: boolean; +export interface HistoryCancelBackgroundCompactionResult { /** - * Run revision reflected by this page. + * 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. */ - revision: number; + cancelled: boolean; } /** - * Parameters for resuming a factory run from its persisted identity. + * Parameters for clearing the conversation and seeding the window that replaces it. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryResumeRequest". + * via the `definition` "HistoryClearContextRequest". */ /** @experimental */ -export interface FactoryResumeRequest { - /** - * Factory run identifier. - */ - runId: string; - limits?: FactoryRunLimits; +export interface HistoryClearContextRequest { /** - * Whether to notify the originating session when the factory completes. + * 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. */ - notifyOnComplete?: boolean; + prompt: string; +} +/** + * What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HistoryClearContextResult". + */ +/** @experimental */ +export interface HistoryClearContextResult { /** - * Whether to emit factory phase names to the session transcript. + * Number of non-system, non-developer messages that were removed from the conversation. Zero only when the window already held no conversation. */ - logPhaseNames?: boolean; + messagesCleared: number; } /** - * Wire-only per-invocation factory resource ceiling overrides. + * Post-compaction context window usage breakdown * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunLimits". + * via the `definition` "HistoryCompactContextWindow". */ /** @experimental */ -export interface FactoryRunLimits { +export interface HistoryCompactContextWindow { /** - * Maximum number of factory subagents that may run concurrently. + * Maximum token count for the model's context window */ - maxConcurrentSubagents?: number | null; + tokenLimit: number; /** - * Maximum total number of factory subagents that may be admitted. + * Current total tokens in the context window (system + conversation + tool definitions) */ - maxTotalSubagents?: number | null; + currentTokens: number; /** - * 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. + * Current number of messages in the conversation */ - timeoutSeconds?: number | null; + messagesLength: number; + /** + * Token count from system message(s) + */ + systemTokens?: number; /** - * Maximum AI credits consumed by factory subagents and their descendants. The post-paid ceiling is soft: parallel turns can settle beyond it before the run stops. + * Token count from non-system messages (user, assistant, tool) */ - maxAiCredits?: number | null; -} -/** - * Resolved persisted factory identity and resumed run envelope. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryResumeResult". - */ -/** @experimental */ -export interface FactoryResumeResult { + conversationTokens?: number; /** - * Persisted factory name resolved for the resumed run. + * Token count from tool definitions */ - factoryName: string; - run: FactoryRunResult; + toolDefinitionsTokens?: number; } /** - * Complete current or terminal factory run envelope. + * Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunResult". + * via the `definition` "HistoryCompactResult". */ /** @experimental */ -export interface FactoryRunResult { +export interface HistoryCompactResult { /** - * Factory run identifier. + * Whether compaction completed successfully */ - runId: string; + success: boolean; /** - * One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + * Number of tokens freed by compaction */ - attempt?: number; - status: FactoryRunStatus; + tokensRemoved: number; /** - * Completed factory result. + * Number of messages removed during compaction */ - result?: JsonValue; + messagesRemoved: number; /** - * Error message for an errored run. + * Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). */ - error?: string; - failure?: FactoryRunFailure; + summaryContent?: string; + contextWindow?: HistoryCompactContextWindow; +} +/** + * Rewind points and file-change-tracking availability for the session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HistoryListRewindPointsResult". + */ +/** @experimental */ +export interface HistoryListRewindPointsResult { /** - * Reason for a halted or cancelled run. + * Whether this session captured file changes from its first turn. */ - reason?: string; + fileChangeTrackingEnabled: boolean; + unavailableReason?: HistoryRewindUnavailableReason; /** - * Partial journal and progress snapshot for a halted, cancelled, or errored run. + * Root user turns in chronological order. Empty when `unavailableReason` is set. */ - snapshot?: JsonValue; - pauseInfo?: FactoryPauseInfo; + points: HistoryRewindPoint[]; } /** - * Full factory run observability detail. + * A root user turn that the session can rewind to. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunDetail". + * via the `definition` "HistoryRewindPoint". */ /** @experimental */ -export interface FactoryRunDetail { - /** - * Factory run identifier. - */ - runId: string; - /** - * Registered factory name. - */ - factoryName: string; - /** - * Human-readable factory description. - */ - description: string; - status: FactoryRunStatus; +export interface HistoryRewindPoint { /** - * Monotonic durable run revision. + * ID of the user.message event that begins the discarded suffix. */ - revision: number; + eventId: string; /** - * Epoch milliseconds when the run was created. + * User-visible message text for the turn. */ - createdAt: number; + userMessage: string; /** - * Epoch milliseconds when execution first started, or null before start. + * ISO timestamp of the user turn. */ - startedAt: number | null; + timestamp: string; /** - * Epoch milliseconds when the durable run was last updated. + * Whether at least one file in this turn or a later turn can be restored. */ - updatedAt: number; + canRestoreFiles: boolean; /** - * Epoch milliseconds when the run completed, or null while nonterminal. + * Number of unique files in this turn and all later turns that have captured changes. */ - completedAt: number | null; + fileCount: number; /** - * Current phase identity, or null before any phase is entered. + * Whether this turn itself captured any file changes. */ - currentPhase: FactoryCurrentPhase | null; + turnChangedFiles: boolean; /** - * Number of phases declared by the factory. + * Lines added by this turn's captured file changes. */ - declaredPhaseCount: number; + linesAdded: number; /** - * Number of direct factory agents currently live. + * Lines removed by this turn's captured file changes. */ - liveAgentCount: number; + linesRemoved: number; /** - * Total direct factory agents spawned across all attempts. + * Whether this turn was an automatically injected autopilot continuation. */ - totalSpawnedAgentCount: number; - consumed: FactoryRunConsumed; - declaredLimits: FactoryDeclaredLimits; + isAutopilotContinuation: boolean; +} +/** + * Event boundary to preview for conversation-and-files rewind. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HistoryPreviewRewindRequest". + */ +/** @experimental */ +export interface HistoryPreviewRewindRequest { /** - * Approved effective resource ceilings, or null until approved. + * ID of the user.message event that begins the discarded suffix. */ - approved: FactoryDeclaredLimits | null; + eventId: string; +} +/** + * Files and aggregate changes for a prospective rewind. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HistoryPreviewRewindResult". + */ +/** @experimental */ +export interface HistoryPreviewRewindResult { /** - * Epoch milliseconds when this live-overlay snapshot was observed. + * Whether file restore is available for this session. This is authoritative: switch on it and read `reason` only when it is false. */ - observedAt: number; + available: boolean; + reason?: HistoryRewindUnavailableReason; /** - * Epoch milliseconds when the current active segment started, or null while inactive. + * Number of unique files in the preview. */ - activeSegmentStartedAt: number | null; + fileCount: number; /** - * Terminal run outcome, or null while nonterminal. + * Files ordered by path. */ - terminal: FactoryRunTerminal | null; + files: HistoryRewindFilePreview[]; +} +/** + * A file that a conversation-and-files rewind would restore. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HistoryRewindFilePreview". + */ +/** @experimental */ +export interface HistoryRewindFilePreview { /** - * Whether the durable run state currently passes runtime resume eligibility checks. + * Absolute path of the captured file. */ - canResume: boolean; + path: string; + changeType: HistoryRewindChangeType; /** - * Lifecycle and timing observations for each factory phase. + * Lines added across the discarded turns. */ - phases: FactoryPhaseObservation[]; + linesAdded: number; /** - * Durable identities and live statuses for direct factory agents. + * Lines removed across the discarded turns. */ - agents: FactoryAgentSummary[]; - progress: FactoryProgressPage; + linesRemoved: number; } /** - * Parameters for invoking a registered factory. + * Boundary and mode for rewinding session history. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryRunRequest". + * via the `definition` "HistoryRewindRequest". */ /** @experimental */ -export interface FactoryRunRequest { - /** - * Registered factory name. - */ - name: string; +export interface HistoryRewindRequest { /** - * Factory input value. + * ID of the user.message event that begins the discarded suffix. */ - args: JsonValue; - options?: RunOptions; + eventId: string; + mode: HistoryRewindMode; } /** - * Options controlling factory invocation. + * Structured outcome of a rewind request. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "RunOptions". + * via the `definition` "HistoryRewindResult". */ /** @experimental */ -export interface RunOptions { - limits?: FactoryRunLimits; +export interface HistoryRewindResult { + outcome: HistoryRewindOutcome; /** - * Whether to notify the originating session when the factory completes. + * 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`. */ - notifyOnComplete?: boolean; + eventsRemoved?: number; /** - * Whether to emit factory phase names to the session transcript. + * 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. */ - logPhaseNames?: boolean; + restoredFiles: string[]; /** - * Run identifier whose journal and progress should seed this resumed run. + * 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. */ - resumeFromRunId?: string; + skippedFiles: HistorySkippedFileRestore[]; + /** + * 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`). + */ + error?: string; } /** - * Internal parameters for resuming a factory run from a tool. + * A captured file that rewind intentionally left unchanged. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryToolResumeRequest". + * via the `definition` "HistorySkippedFileRestore". */ /** @experimental */ -/** @internal */ -export interface FactoryToolResumeRequest { - /** - * Factory run identifier. - */ - runId: string; - limits?: FactoryRunLimits; +export interface HistorySkippedFileRestore { /** - * Opaque identifier of the originating tool call. + * Absolute path of the skipped file. */ - toolCallId?: string; + path: string; + reason: HistoryFileRestoreSkipReason; } /** - * Options for an internal tool-originated factory invocation. + * Markdown summary of the conversation context (empty when not available). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryToolRunOptions". + * via the `definition` "HistorySummarizeForHandoffResult". */ /** @experimental */ -/** @internal */ -export interface FactoryToolRunOptions { - limits?: FactoryRunLimits; +export interface HistorySummarizeForHandoffResult { /** - * Run identifier whose journal and progress should seed this resumed run. + * 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. */ - resumeFromRunId?: string; + summary: string; } /** - * Internal parameters for invoking a registered factory from a tool. + * Identifier of the event to truncate to; this event and all later events are removed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FactoryToolRunRequest". + * via the `definition` "HistoryTruncateRequest". */ /** @experimental */ -/** @internal */ -export interface FactoryToolRunRequest { - /** - * Registered factory name. - */ - name: string; - /** - * Factory input value. - */ - args: JsonValue; - options?: FactoryToolRunOptions; +export interface HistoryTruncateRequest { /** - * Opaque identifier of the originating tool call. + * Event ID to truncate to. This event and all events after it are removed from the session. */ - toolCallId?: string; + eventId: string; } /** - * Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. + * Number of events that were removed by the truncation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FleetStartRequest". + * via the `definition` "HistoryTruncateResult". */ /** @experimental */ -export interface FleetStartRequest { - /** - * Optional user prompt to combine with fleet instructions - */ - prompt?: string; +export interface HistoryTruncateResult { /** - * Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request + * Number of events that were removed */ - attachments?: Attachment[]; + eventsRemoved: number; /** - * If false, this request will not trigger a Premium Request Unit charge. User requests default to billable. - * - * @internal + * 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. */ - billable?: boolean; + checkpointCleanupFailed?: boolean; /** - * If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. + * Failure detail when checkpointCleanupFailed is true. */ - wait?: boolean; + checkpointCleanupError?: string; } /** - * Indicates whether fleet mode was successfully activated. + * Runtime-owned wire payload for a server-to-client hook callback invocation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FleetStartResult". + * via the `definition` "HookInvokeRequest". */ /** @experimental */ -export interface FleetStartResult { - /** - * Whether fleet mode was successfully activated - */ - started: boolean; +/** @internal */ +export interface HookInvokeRequest { + sessionId: string; + hookType: HookType; + input: JsonValue; } /** - * Folder path to add to trusted folders. + * Optional output returned by an SDK callback hook. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FolderTrustAddParams". + * via the `definition` "HookInvokeResponse". */ /** @experimental */ -export interface FolderTrustAddParams { - /** - * Folder path to mark as trusted - */ - path: string; +/** @internal */ +export interface HookInvokeResponse { + output?: JsonValue; } /** - * Folder path to check for trust. + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FolderTrustCheckParams". + * via the `definition` "HooksDiscoverRequest". */ /** @experimental */ -export interface FolderTrustCheckParams { +export interface HooksDiscoverRequest { /** - * Folder path to check + * Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. */ - path: string; + projectPaths?: string[]; + /** + * When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + */ + excludeHostHooks?: boolean; } /** - * Folder trust check result. + * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "FolderTrustCheckResult". + * via the `definition` "HooksDiscoverResult". */ /** @experimental */ -export interface FolderTrustCheckResult { +export interface HooksDiscoverResult { /** - * Whether the folder is trusted + * All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. */ - trusted: boolean; + hooks: DiscoveredHook[]; + /** + * Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + */ + warnings: string[]; + /** + * Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + */ + errors: string[]; } /** - * Client environment metadata describing the process that produced a telemetry event. + * Normalized listener settings delivered only to the supervised hosting participant. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "GitHubTelemetryClientInfo". + * via the `definition` "HostConfiguration". */ /** @experimental */ -export interface GitHubTelemetryClientInfo { +export interface HostConfiguration { + localServer?: HostLocalServerConfiguration; + githubEnvironment?: HostGitHubEnvironmentOptions; /** - * Copilot CLI version string. + * Whether session materialization is delegated to the owning application. */ - cli_version: string; + sessionFactory: boolean; /** - * Operating system platform (e.g. darwin, linux, win32). + * Whether app-owned durable sessions are resumed by the owning application. */ - os_platform: string; + resumeFactory: boolean; +} +/** + * Normalized local WebSocket listener settings. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostLocalServerConfiguration". + */ +/** @experimental */ +export interface HostLocalServerConfiguration { /** - * Operating system version string. + * Hostname or IP address to bind. + */ + hostname: string; + /** + * Port to bind, with zero requesting OS allocation. */ - os_version: string; + port: number; /** - * Operating system architecture (e.g. arm64, x64). + * Secret connection token, absent when authentication is disabled. */ - os_arch: string; + token?: string; /** - * Node.js runtime version string. + * Whether the listener requires token authentication. */ - node_version: string; + 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 { /** - * Copilot subscription plan, when known. + * Human-readable environment display name. */ - copilot_plan?: string; + name: string; /** - * Type of client. + * Stable application installation identity, reused across host restarts. */ - client_type?: string; + computeId: string; +} +/** + * Stops a connection-owned listener and joins its teardown. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostDisposeRequest". + */ +/** @experimental */ +export interface HostDisposeRequest { /** - * Name of the client application. + * Listener UUID. Unknown or successfully stopped IDs are harmless. */ - client_name?: string; + hostId: string; +} +/** + * Empty acknowledgement for a completed host lifecycle operation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostEmptyResult". + */ +/** @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 { /** - * Whether the user is a GitHub/Microsoft staff member. + * Current bearer token for the authenticated GitHub identity. */ - is_staff?: boolean; + token: string; /** - * Stable machine identifier for the device. + * Hostname of the authenticated GitHub service. */ - dev_device_id?: string; + githubHost: string; /** - * Distinct CPU model names for the host, comma-separated. + * GitHub API base URL for the authenticated service. */ - cpu_model?: string; + githubApiUrl: string; /** - * Number of logical CPU cores on the host. + * Mission Control API origin for environment registration and management. */ - cpu_count?: number; + missionControlUrl: string; } /** - * 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. + * Reports a supervised listener's hosting-task termination and cleanup outcome. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "GitHubTelemetryEvent". + * via the `definition` "HostExitedNotification". */ /** @experimental */ -export interface GitHubTelemetryEvent { +export interface HostExitedNotification { /** - * Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). + * Listener UUID. */ - kind: string; + hostId: string; + reason: HostExitReason; /** - * Timestamp when the event was created (ISO 8601 format). + * Process exit status when available; absent for in-process listener tasks. */ - created_at?: string; + exitCode?: number | null; /** - * Reference to the model call that produced this event. + * Explicit startup or teardown failure, when present. */ - model_call_id?: string; + error?: string | null; +} +/** + * Local WebSocket transport options. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostLocalServerOptions". + */ +/** @experimental */ +export interface HostLocalServerOptions { /** - * String-valued properties as a map from key to value. + * Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. */ - properties: { - [k: string]: string | undefined; - }; + hostname?: string; /** - * Numeric metrics as a map from key to value. + * Listener port. Omitted or zero requests an OS-allocated port. */ - metrics: { - [k: string]: number | undefined; - }; + port?: number; /** - * Experiment assignment context. + * Nonempty connection token. Generated randomly when required and omitted. */ - exp_assignment_context?: string; + token?: string; /** - * Feature flags enabled for this session, as a map from flag to value. + * Require token authentication (default true). Cannot be false with a token. */ - features?: { - [k: string]: string | undefined; - }; + requireConnectionToken?: boolean; +} +/** + * Publishes a resident session attached to the listener's owning connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostPublishSessionRequest". + */ +/** @experimental */ +export interface HostPublishSessionRequest { /** - * Session identifier the event belongs to. + * Listener UUID returned by host.start. */ - session_id?: string; + hostId: string; /** - * Copilot tracking ID for user-level attribution. + * Canonical runtime session ID attached to the listener's owning connection. */ - copilot_tracking_id?: string; - client?: GitHubTelemetryClientInfo; + sessionId: string; } /** - * 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. + * The existing runtime identity and its resource on the listener. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "GitHubTelemetryNotification". + * via the `definition` "HostPublishSessionResult". */ /** @experimental */ -export interface GitHubTelemetryNotification { +export interface HostPublishSessionResult { /** - * 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. + * Canonical runtime ID of the published session. */ - sessionId?: string; + sessionId: string; /** - * Whether this is a restricted telemetry event (cli.restricted_telemetry). Hosts must route restricted events to first-party Microsoft stores only. + * AHP resource URI for the session on this listener. */ - restricted: boolean; - event: GitHubTelemetryEvent; + sessionUri: string; } /** - * Asks the SDK client to acquire a GitHub access token from an opaque callback registration. + * Readiness reported by the supervised hosting participant on its own SDK connection. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "GitHubTokenAcquireRequest". + * via the `definition` "HostReadyRequest". */ /** @experimental */ -export interface GitHubTokenAcquireRequest { +export interface HostReadyRequest { /** - * Opaque identifier generated by the SDK for this callback registration. + * Actual bound WebSocket URL. */ - registrationId: string; + address?: string; /** - * Effective GitHub host for which the callback must return a token. + * Configured secret token, absent when authentication is disabled. */ - host: string; + token?: string; /** - * Session receiving the token. Absent only before a cloud session has been assigned its id. + * Registered environment ID, reported only once the relay transport is connected. */ - sessionId?: string; - reason: GitHubTokenAcquireReason; + environmentId?: string; } /** - * Pending external tool call request ID, with the tool result or an error describing why it failed. + * Listener-scoped registration, not a copy or durable adoption of a session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HandlePendingToolCallRequest". + * via the `definition` "HostRegisterSessionRequest". */ /** @experimental */ -export interface HandlePendingToolCallRequest { +export interface HostRegisterSessionRequest { /** - * Request ID of the pending tool call + * Canonical ID of the existing resident runtime session. */ - requestId: string; - result?: ExternalToolResult; + sessionId: string; /** - * Error message if the tool call failed + * Absolute working directory of the resident session. */ - error?: string; + workingDirectory: string; + /** + * Additional directories already granted to the resident session. + */ + additionalDirectories?: string[]; + /** + * Current display title of the resident session, when available. + */ + title?: string; + /** + * Session creation time in milliseconds since the Unix epoch, when available. + */ + createdAtUnixMs?: number; + /** + * Last session modification time in milliseconds since the Unix epoch, when available. + */ + modifiedAtUnixMs?: number; } /** - * Indicates whether the external tool call result was handled successfully. + * Application callback routed over its existing SDK connection. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HandlePendingToolCallResult". + * via the `definition` "HostSessionCreateCallback". */ /** @experimental */ -export interface HandlePendingToolCallResult { +export interface HostSessionCreateCallback { /** - * Whether the tool call result was handled successfully + * Listener UUID identifying the owning application's host. */ - success: boolean; + hostId: string; + /** + * Unique identity of the session participation being requested. + */ + handoffId: string; + /** + * Resume an app-owned durable session instead of creating a new session. + */ + resume?: boolean; + /** + * Host-selected SDK creation or resume settings, without executable callbacks or tools. + */ + config: { + [k: string]: unknown | undefined; + }; } /** - * Indicates whether an in-progress manual compaction was aborted. + * One application-owned session handoff, requested by the supervised hosting participant. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryAbortManualCompactionResult". + * via the `definition` "HostSessionCreateRequest". */ /** @experimental */ -export interface HistoryAbortManualCompactionResult { +export interface HostSessionCreateRequest { /** - * 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. + * Unique identity for this participation, independent of the session lifetime. */ - aborted: boolean; + handoffId: string; + /** + * Resume an app-owned durable session instead of creating a new session. + */ + resume?: boolean; + /** + * Host-selected SDK creation or resume settings, without executable callbacks or tools. + */ + config: { + [k: string]: unknown | undefined; + }; } /** - * Indicates whether an in-progress background compaction was cancelled. + * The resident session the application has materialized on its own connection. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryCancelBackgroundCompactionResult". + * via the `definition` "HostSessionCreateResult". */ /** @experimental */ -export interface HistoryCancelBackgroundCompactionResult { +export interface HostSessionCreateResult { /** - * 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. + * Runtime session UUID materialized on the application's SDK connection. */ - cancelled: boolean; + sessionId: string; } /** - * Parameters for clearing the conversation and seeding the window that replaces it. + * Releases the original application session object retained for one handoff. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryClearContextRequest". + * via the `definition` "HostSessionReleasedNotification". */ /** @experimental */ -export interface HistoryClearContextRequest { +export interface HostSessionReleasedNotification { /** - * 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. + * Listener UUID whose application session participation ended. */ - prompt: string; + hostId: string; + /** + * Identity of the handoff retaining the original application session object. + */ + handoffId: string; } /** - * What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. + * Ends one participation, not the application's session lifetime. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryClearContextResult". + * via the `definition` "HostSessionReleaseRequest". */ /** @experimental */ -export interface HistoryClearContextResult { +export interface HostSessionReleaseRequest { /** - * Number of non-system, non-developer messages that were removed from the conversation. Zero only when the window already held no conversation. + * Identity of the participation to release without destroying the session. */ - messagesCleared: number; + handoffId: string; } /** - * Post-compaction context window usage breakdown + * Starts a supervised AHP host with at least one explicitly selected transport. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryCompactContextWindow". + * via the `definition` "HostStartRequest". */ /** @experimental */ -export interface HistoryCompactContextWindow { +export interface HostStartRequest { /** - * Maximum token count for the model's context window - */ - tokenLimit: number; - /** - * Current total tokens in the context window (system + conversation + tool definitions) - */ - currentTokens: number; - /** - * Current number of messages in the conversation - */ - messagesLength: number; - /** - * Token count from system message(s) + * Caller-generated UUID identifying this connection-owned listener. */ - systemTokens?: number; + hostId: string; + localServer?: HostLocalServerOptions; + githubEnvironment?: HostGitHubEnvironmentOptions; /** - * Token count from non-system messages (user, assistant, tool) + * Ask the owning SDK application to materialize AHP sessions. */ - conversationTokens?: number; + sessionFactory?: boolean; /** - * Token count from tool definitions + * Ask the owning application to resume its durable AHP sessions. */ - toolDefinitionsTokens?: number; + resumeFactory?: boolean; } /** - * Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. + * Listener readiness, returned only after binding and the supervised participant's SDK handshake. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryCompactResult". + * via the `definition` "HostStartResult". */ /** @experimental */ -export interface HistoryCompactResult { - /** - * Whether compaction completed successfully - */ - success: boolean; +export interface HostStartResult { /** - * Number of tokens freed by compaction + * Caller-generated listener UUID. */ - tokensRemoved: number; + hostId: string; /** - * Number of messages removed during compaction + * Actual bound WebSocket URL, including the allocated port. */ - messagesRemoved: number; + url?: string; /** - * Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). + * Secret connection token, absent when authentication is disabled. */ - summaryContent?: string; - contextWindow?: HistoryCompactContextWindow; -} -/** - * Rewind points and file-change-tracking availability for the session. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryListRewindPointsResult". - */ -/** @experimental */ -export interface HistoryListRewindPointsResult { + token?: string; /** - * Whether this session captured file changes from its first turn. + * GitHub Mission Control environment ID, present when its relay transport is ready. */ - fileChangeTrackingEnabled: boolean; - unavailableReason?: HistoryRewindUnavailableReason; + environmentId?: string; /** - * Root user turns in chronological order. Empty when `unavailableReason` is set. + * Separate host process ID, when provided by a legacy runtime. Absent for in-process listeners. */ - points: HistoryRewindPoint[]; + pid?: number; } /** - * A root user turn that the session can rewind to. + * Catalogue identity retained from a bound candidate or plan at installation time. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryRewindPoint". + * via the `definition` "InstallationCatalogueIdentity". */ /** @experimental */ -export interface HistoryRewindPoint { - /** - * ID of the user.message event that begins the discarded suffix. - */ - eventId: string; +export interface InstallationCatalogueIdentity { /** - * User-visible message text for the turn. - */ - userMessage: string; - /** - * ISO timestamp of the user turn. + * Authority resource identifier when supplied by the catalogue. */ - timestamp: string; + resourceId?: string; /** - * Whether at least one file in this turn or a later turn can be restored. + * Catalogue item URL when supplied by the authority. */ - canRestoreFiles: boolean; + itemUrl?: string; /** - * Number of unique files in this turn and all later turns that have captured changes. + * Human display name retained from the catalogue candidate. */ - fileCount: number; + displayName: string; /** - * Whether this turn itself captured any file changes. + * Catalogue description retained at install planning time. */ - turnChangedFiles: boolean; + description?: string; /** - * Lines added by this turn's captured file changes. + * Catalogue publisher retained at install planning time. */ - linesAdded: number; + publisher?: string; /** - * Lines removed by this turn's captured file changes. + * Catalogue version retained at install planning time. */ - linesRemoved: number; + version?: string; /** - * Whether this turn was an automatically injected autopilot continuation. + * Catalogue authority/source string that supplied the candidate. */ - isAutopilotContinuation: boolean; + source: string; + trustAtInstall?: CatalogTrustSnapshot; } /** - * Event boundary to preview for conversation-and-files rewind. + * One connection-owned, expiring request for a trusted host's explicit user decision. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryPreviewRewindRequest". + * via the `definition` "InstallationConfirmationRequest". */ /** @experimental */ -export interface HistoryPreviewRewindRequest { +export interface InstallationConfirmationRequest { /** - * ID of the user.message event that begins the discarded suffix. + * 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. */ - eventId: string; + policySessionId?: string; + /** + * Opaque one-use challenge. Return unchanged; never log or persist. + */ + confirmationId: string; + /** + * Random identifier of this installation operation, not a plan handle. + */ + operationId: string; + /** + * Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. + */ + expiresAt: string; + /** + * Opaque commitment to the exact review and inputs. Return unchanged; never log. + */ + reviewFingerprint: string; + review: InstallationReview; } /** - * Files and aggregate changes for a prospective rewind. + * Normalised identity of the MCP server a plan targets, independent of how the card spelled it. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryPreviewRewindResult". + * via the `definition` "McpPlanResourceIdentity". */ /** @experimental */ -export interface HistoryPreviewRewindResult { +export interface McpPlanResourceIdentity { /** - * Whether file restore is available for this session. This is authoritative: switch on it and read `reason` only when it is false. + * Canonical, normalised name of the server, for example `io.github.owner/server`. */ - available: boolean; - reason?: HistoryRewindUnavailableReason; + canonicalName: string; /** - * Number of unique files in the preview. + * Local configuration key the server would be recorded under. */ - fileCount: number; + serverName: string; /** - * Files ordered by path. + * Version advertised by the card, when it declares one. */ - files: HistoryRewindFilePreview[]; + version?: string; + /** + * Registry identifier of the server, when it came from a registry. + */ + registryId?: string; } /** - * A file that a conversation-and-files rewind would restore. + * Provenance of the exact validated JSON MCP card content bound privately to a completed plan and its opaque handle. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryRewindFilePreview". + * via the `definition` "McpPlanProvenance". */ /** @experimental */ -export interface HistoryRewindFilePreview { +export interface McpPlanProvenance { /** - * Absolute path of the captured file. + * Authority associated with the validated card, without path, query, or credentials. Inert untrusted data. */ - path: string; - changeType: HistoryRewindChangeType; + authority: string; /** - * Lines added across the discarded turns. + * ISO 8601 timestamp at which the runtime completed strict parsing and schema validation of the card content. */ - linesAdded: number; + validatedAt: string; + cardDigest: CardDigest; + mediaType: McpServerCardMediaType; +} +/** + * Where a plan would be written. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPlanTarget". + */ +/** @experimental */ +export interface McpPlanTarget { + scope: McpPlanScope; /** - * Lines removed across the discarded turns. + * Configuration key the server would be recorded under within that scope. */ - linesRemoved: number; + configKey: string; } /** - * Boundary and mode for rewinding session history. + * Outcome of evaluating the planned server against registry and enterprise policy. Evaluation is read-only. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryRewindRequest". + * via the `definition` "McpPlanPolicyResult". */ /** @experimental */ -export interface HistoryRewindRequest { +export interface McpPlanPolicyResult { + decision: McpPlanPolicyDecision; + source: McpPlanPolicySource; /** - * ID of the user.message event that begins the discarded suffix. + * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ - eventId: string; - mode: HistoryRewindMode; + reason?: string; } /** - * Structured outcome of a rewind request. + * An eligible local-package transport choice. Package identity is required and a remote endpoint cannot be represented. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryRewindResult". + * via the `definition` "McpPlanTransportChoicePackage". */ /** @experimental */ -export interface HistoryRewindResult { - outcome: HistoryRewindOutcome; +export interface McpPlanTransportChoicePackage { /** - * 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`. + * Stable identifier for this choice within the plan, used to select it when the plan is applied. */ - eventsRemoved?: number; + choiceId: string; + transport: McpPlanPackageTransport; + installMethod: McpPlanPackageInstallMethod; /** - * 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. + * Packaging ecosystem, for example `oci` or `npm`. */ - restoredFiles: string[]; + packageType: string; /** - * 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. + * Package identifier. Inert untrusted data. */ - skippedFiles: HistorySkippedFileRestore[]; + packageIdentifier: string; /** - * 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`). + * Typed values this choice requires, excluding secrets. */ - error?: string; + requiredValues: McpPlanRequiredValue[]; + /** + * Secrets this choice requires, referenced by placeholder only. + */ + secretPlaceholders: McpPlanSecretPlaceholder[]; } /** - * A captured file that rewind intentionally left unchanged. + * One non-secret scalar value a transport choice needs before it can be applied. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistorySkippedFileRestore". + * via the `definition` "McpPlanRequiredValueScalar". */ /** @experimental */ -export interface HistorySkippedFileRestore { +export interface McpPlanRequiredValueScalar { + kind: McpPlanRequiredValueScalarKind; /** - * Absolute path of the skipped file. + * Key the value is supplied under. Inert untrusted data. */ - path: string; - reason: HistoryFileRestoreSkipReason; + key: string; + category: McpPlanValueCategory; + valueType: McpPlanScalarValueType; + /** + * Whether the value must be present for the plan to be applicable. + */ + required: boolean; + /** + * Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. + */ + defaultValue?: string; + /** + * Human-readable label from the card. Inert untrusted text. + */ + title?: string; + /** + * Human-readable explanation from the card. Inert untrusted text. + */ + description?: string; + /** + * Whether the value may be supplied more than once. + */ + isRepeated: boolean; } /** - * Markdown summary of the conversation context (empty when not available). + * One enumerated non-secret value a transport choice needs before it can be applied. The permitted values are structurally required. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistorySummarizeForHandoffResult". + * via the `definition` "McpPlanRequiredValueEnum". */ /** @experimental */ -export interface HistorySummarizeForHandoffResult { +export interface McpPlanRequiredValueEnum { + kind: McpPlanRequiredValueEnumKind; /** - * 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. + * Key the value is supplied under. Inert untrusted data. */ - summary: string; + key: string; + category: McpPlanValueCategory; + valueType: McpPlanEnumValueType; + /** + * Whether the value must be present for the plan to be applicable. + */ + required: boolean; + /** + * Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. + */ + defaultValue?: string; + /** + * Human-readable label from the card. Inert untrusted text. + */ + title?: string; + /** + * Human-readable explanation from the card. Inert untrusted text. + */ + description?: string; + /** + * Non-empty permitted value set. Inert untrusted data. + * + * @minItems 1 + */ + enumValues: [string, ...string[]]; + /** + * Whether the value may be supplied more than once. + */ + isRepeated: boolean; } /** - * Identifier of the event to truncate to; this event and all later events are removed. + * A secret a transport choice needs, referenced by placeholder. No secret value ever appears in a plan, and the placeholder resolves against the keychain only when a plan is applied. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryTruncateRequest". + * via the `definition` "McpPlanSecretPlaceholder". */ /** @experimental */ -export interface HistoryTruncateRequest { +export interface McpPlanSecretPlaceholder { /** - * Event ID to truncate to. This event and all events after it are removed from the session. + * Key the secret is supplied under. Inert untrusted data. */ - eventId: string; + key: string; + placeholder: McpPlanSecretReference; + /** + * Human-readable label from the card. Inert untrusted text. + */ + title?: string; } /** - * Number of events that were removed by the truncation. + * An eligible remote-endpoint transport choice. The endpoint is required and package identity cannot be represented. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HistoryTruncateResult". + * via the `definition` "McpPlanTransportChoiceRemote". */ /** @experimental */ -export interface HistoryTruncateResult { +export interface McpPlanTransportChoiceRemote { /** - * Number of events that were removed + * Stable identifier for this choice within the plan, used to select it when the plan is applied. */ - eventsRemoved: number; + choiceId: string; + transport: McpPlanRemoteTransport; + installMethod: McpPlanRemoteInstallMethod; /** - * 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. + * Endpoint URL. Inert untrusted data. */ - checkpointCleanupFailed?: boolean; + endpoint: string; /** - * Failure detail when checkpointCleanupFailed is true. + * Typed values this choice requires, excluding secrets. */ - checkpointCleanupError?: string; -} -/** - * Runtime-owned wire payload for a server-to-client hook callback invocation. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HookInvokeRequest". - */ -/** @experimental */ -/** @internal */ -export interface HookInvokeRequest { - sessionId: string; - hookType: HookType; - input: JsonValue; + requiredValues: McpPlanRequiredValue[]; + /** + * Secrets this choice requires, referenced by placeholder only. + */ + secretPlaceholders: McpPlanSecretPlaceholder[]; } /** - * Optional output returned by an SDK callback hook. + * The configuration-change alternative for the transportChoices entry at the same index. Only the selected alternative is applied; entries are not cumulative. The payload stays behind the runtime boundary. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HookInvokeResponse". + * via the `definition` "McpPlanConfigurationChange". */ /** @experimental */ -/** @internal */ -export interface HookInvokeResponse { - output?: JsonValue; +export interface McpPlanConfigurationChange { + operation: McpPlanConfigurationOperation; + scope: McpPlanScope; + /** + * Configuration key the change applies to. + */ + configKey: string; + /** + * Names of the configuration fields the change would set, without their values. + */ + changedFields: string[]; + /** + * Secret placeholders the written configuration would reference. The constrained placeholder type cannot carry a literal secret value. + */ + secretReferences: McpPlanSecretReference[]; } /** - * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * One Registry string-valued configuration entry for the selected transport. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HooksDiscoverRequest". + * via the `definition` "McpInstallationInput". */ /** @experimental */ -export interface HooksDiscoverRequest { +export interface McpInstallationInput { + category: McpPlanValueCategory; /** - * Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + * Exact key declared by the selected choice. */ - projectPaths?: string[]; + key: string; /** - * When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + * Explicit non-secret value. Secret placeholders use a separate input channel. */ - excludeHostHooks?: boolean; + value: string; } /** - * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + * Final remote configuration, not a template. The producer refuses external-value + * expansion before presenting this review. Receipt-owned secrets appear only as + * `${installation-secret:}` references whose `` matches a reviewed + * `${secret:}` placeholder; values are never included. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HooksDiscoverResult". + * via the `definition` "McpInstallationRemoteConfiguration". */ /** @experimental */ -export interface HooksDiscoverResult { +export interface McpInstallationRemoteConfiguration { + transport: McpPlanRemoteTransport; /** - * All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + * Exact resolved endpoint, without templates or secret placeholders. */ - hooks: DiscoveredHook[]; + url: string; /** - * Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + * Configured headers, excluding separately authorised OAuth tokens. Values may + * contain owned secret references, never secret values. */ - warnings: string[]; + headers: { + [k: string]: string | undefined; + }; /** - * Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + * Configured tool selection, not permission to invoke those tools. */ - errors: string[]; + tools: string[]; } /** - * Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. + * Source identity retained from Agent Finder and the pinned GitHub descriptor. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstalledPlugin". + * via the `definition` "SkillInstallationSource". */ /** @experimental */ -export interface InstalledPlugin { +export interface SkillInstallationSource { /** - * Plugin name + * Agent Finder resource identifier. */ - name: string; + resourceId: string; /** - * Marketplace the plugin came from (empty string for direct repo installs) + * Agent Finder materialisation revision identifier. */ - marketplace: string; + catalogRevisionId: string; /** - * Version installed (if available) + * GitHub repository database identifier. */ - version?: string; + repositoryId: string; /** - * Installation timestamp + * Repository full name, for example owner/name. */ - installed_at: string; + repository: string; /** - * Whether the plugin is currently enabled + * Pinned Git commit revision. */ - enabled: boolean; + revision: string; /** - * Path where the plugin is cached locally + * Root path within the pinned repository. */ - cache_path?: string; - source?: InstalledPluginSource; + root: string; /** - * 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. + * Digest of the canonical materialisation descriptor. */ - source_sha?: string; + descriptorDigest: string; /** - * Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. + * Digest of the descriptor's bundle manifest. */ - installed_from?: string; + bundleDigest: string; } /** - * Source descriptor for a direct GitHub plugin install, with `owner/repo`, optional ref or full commit SHA, and optional subpath. + * A user-facing personal Skill installation location without absolute host paths. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstalledPluginSourceGitHub". + * via the `definition` "SkillInstallationLocation". */ /** @experimental */ -export interface InstalledPluginSourceGitHub { +export interface SkillInstallationLocation { + scope: SkillInstallationScope; /** - * Constant value. Always "github". + * Path relative to the Copilot home. */ - source: "github"; + relativePath: string; /** - * GitHub repository in `owner/repo` form. + * Safe display label, for example ~/.copilot/skills/run-checks. */ - repo: string; + displayLabel: string; /** - * Optional Git ref to resolve. + * Diagnostics-only absolute host path. Hosts must not display it by default. */ - ref?: string; + diagnosticsAbsolutePath?: string; +} +/** + * One reviewed Skill file. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillInstallationFileReview". + */ +/** @experimental */ +export interface SkillInstallationFileReview { /** - * Optional full 40-character hexadecimal commit SHA. + * Relative file path within the Skill root. */ - sha?: string; + path: string; /** - * Optional repository-relative path to the plugin. + * Exact reviewed file size in bytes. */ - path?: string; + sizeBytes: number; + /** + * Declared media type for the file. + */ + mediaType: string; + /** + * Whether the file is installed with executable permissions. + */ + executable: boolean; + /** + * SHA-256 digest of the exact file bytes. + */ + digest: string; } /** - * Source descriptor for a direct URL plugin install, with URL, optional ref or full commit SHA, and optional subpath. + * Durable verified Skill ownership summary. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstalledPluginSourceUrl". + * via the `definition` "SkillInstallationSummary". */ /** @experimental */ -export interface InstalledPluginSourceUrl { +export interface SkillInstallationSummary { /** - * Constant value. Always "url". + * Exact durable installation receipt identity. */ - source: "url"; + installationId: string; /** - * URL of the plugin source. + * Operation that installed this Skill. */ - url: string; + operationId: string; /** - * Optional Git ref to resolve. + * Skill invocation name. */ - ref?: string; + name: string; + target: SkillInstallationLocation; /** - * Optional full 40-character hexadecimal commit SHA. + * Persisted enablement requested for this installation. */ - sha?: string; + configuredEnabled: boolean; + sessionState: SkillInstallationSessionState; + source: SkillInstallationSource; + catalogue: InstallationCatalogueIdentity; /** - * Optional source-relative path to the plugin. + * ISO 8601 wall-clock installation time. */ - path?: string; + installedAt: string; + ownershipState: SkillInstallationOwnershipState; } /** - * Source descriptor for a direct local plugin install, with a local filesystem path. + * A response is meaningful only on the connection and request that issued its challenge. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstalledPluginSourceLocal". + * via the `definition` "InstallationConfirmationResponse". */ /** @experimental */ -export interface InstalledPluginSourceLocal { +export interface InstallationConfirmationResponse { /** - * Constant value. Always "local". + * Exact challenge from the request. */ - source: "local"; + confirmationId: string; /** - * Local filesystem path to the plugin. + * Exact review commitment from the request. */ - path: string; + reviewFingerprint: string; + decision: InstallationDecision; } /** * Information about an installed plugin tracked in global state. @@ -11845,145 +14388,364 @@ export interface LocalSessionMetadataValue { isDetached?: boolean; context?: SessionContext; /** - * GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control. + * GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control. + */ + mcTaskId?: string; +} +/** + * Pre-resolved working-directory context for session startup. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionContext". + */ +/** @experimental */ +export interface SessionContext { + /** + * Most recent working directory for this session + */ + cwd: string; + /** + * Git repository root, if the cwd was inside a git repo + */ + gitRoot?: string; + /** + * Repository slug in `owner/name` form, when known + */ + repository?: string; + hostType?: SessionContextHostType; + /** + * Active git branch + */ + branch?: string; +} +/** + * Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "LogRequest". + */ +/** @experimental */ +export interface LogRequest { + /** + * Human-readable message + */ + message: string; + level?: SessionLogLevel; + /** + * Domain category for this log entry (e.g., "mcp", "subscription", "policy", "model"). Maps to `infoType`/`warningType`/`errorType` on the emitted event. Defaults to "notification". + */ + type?: string; + /** + * When true, the message is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Optional URL the user can open in their browser for more details + */ + url?: string; + /** + * Optional actionable tip displayed alongside the message. Only honored on `level: "info"`. + */ + tip?: string; +} +/** + * Identifier of the session event that was emitted for the log message. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "LogResult". + */ +/** @experimental */ +export interface LogResult { + /** + * The unique identifier of the emitted session event + */ + eventId: string; +} +/** + * Parameters for (re)loading the merged LSP configuration set. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "LspInitializeRequest". + */ +/** @experimental */ +export interface LspInitializeRequest { + /** + * Working directory used to load project-level LSP configs. Defaults to the session working directory when omitted. + */ + workingDirectory?: string; + /** + * Git root used as the boundary when traversing for project-level LSP configs (supports monorepos). + */ + gitRoot?: string; + /** + * Force re-initialization even when LSP configs were already loaded for the working directory. + */ + force?: boolean; +} +/** + * Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedMcpServerConfig". + */ +/** @experimental */ +export interface ManagedMcpServerConfig { + /** + * Human-readable catalog display name. + */ + displayName: string; + /** + * Hosted MCP streamable HTTP endpoint. + */ + url: string; + /** + * Tools to include. Defaults to all tools when omitted. + */ + tools?: string[]; + /** + * Timeout in milliseconds for tool discovery and tool calls. + */ + timeout?: number; + /** + * Maximum dynamic-header cache lifetime in milliseconds. + */ + 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. */ - mcTaskId?: string; + model?: string; + autoTier?: AutoTier; } /** - * Pre-resolved working-directory context for session startup. + * 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` "SessionContext". + * via the `definition` "ManagedSettingsMeta". */ /** @experimental */ -export interface SessionContext { +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 { /** - * Most recent working directory for this session + * 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. */ - cwd: string; + source: string; /** - * Git repository root, if the cwd was inside a git repo + * Validated managed-settings document this channel delivered. Absent when the channel delivered none. */ - gitRoot?: string; + 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 { /** - * Repository slug in `owner/name` form, when known + * Dot-separated path of the offending setting, such as `autoTier.overridable`. Empty for the document as a whole. */ - repository?: string; - hostType?: SessionContextHostType; + path: string; + severity: ManagedSettingsDiagnosticSeverity; /** - * Active git branch + * Human-readable description of the finding. */ - branch?: string; + message: string; } /** - * Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. + * Validated device-managed settings discovered before a session exists. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "LogRequest". + * via the `definition` "ManagedSettingsReadResult". */ /** @experimental */ -export interface LogRequest { +export interface ManagedSettingsReadResult { /** - * Human-readable message + * Validated, canonical managed-settings JSON. Omitted when no managed settings were discovered or when discovered settings failed validation. */ - message: string; - level?: SessionLogLevel; + settingsJson?: JsonValue; /** - * Domain category for this log entry (e.g., "mcp", "subscription", "policy", "model"). Maps to `infoType`/`warningType`/`errorType` on the emitted event. Defaults to "notification". + * Discovery or validation error text when managed settings could not be read safely. */ - type?: string; + errorMessage?: string; +} + +/** @experimental */ +export interface ManagedSettingsResolveRequest { /** - * When true, the message is transient and not persisted to the session event log on disk + * Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. */ - ephemeral?: boolean; + selectionId?: string; /** - * Optional URL the user can open in their browser for more details + * GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. */ - url?: string; + gitHubToken?: string; /** - * Optional actionable tip displayed alongside the message. Only honored on `level: "info"`. + * Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. */ - tip?: string; + clientName?: string; } /** - * Identifier of the session event that was emitted for the log message. + * Effective enterprise managed settings for an account, resolved without a session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "LogResult". + * via the `definition` "ManagedSettingsResolveResult". */ /** @experimental */ -export interface LogResult { +export interface ManagedSettingsResolveResult { /** - * The unique identifier of the emitted session event + * 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. */ - eventId: string; + 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[]; } /** - * Parameters for (re)loading the merged LSP configuration set. + * The authoring JSON schema for managed settings recognized by this runtime. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "LspInitializeRequest". + * via the `definition` "ManagedSettingsSchemaResult". */ /** @experimental */ -export interface LspInitializeRequest { - /** - * Working directory used to load project-level LSP configs. Defaults to the session working directory when omitted. - */ - workingDirectory?: string; +export interface ManagedSettingsSchemaResult { /** - * Git root used as the boundary when traversing for project-level LSP configs (supports monorepos). + * 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. */ - gitRoot?: string; + schema: JsonValue; /** - * Force re-initialization even when LSP configs were already loaded for the working directory. + * Version of the runtime that owns this schema. */ - force?: boolean; + runtimeVersion: string; } /** - * Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host. + * A candidate managed-settings document to validate without applying it. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ManagedMcpServerConfig". + * via the `definition` "ManagedSettingsValidateRequest". */ /** @experimental */ -export interface ManagedMcpServerConfig { - /** - * Human-readable catalog display name. - */ - displayName: string; - /** - * Hosted MCP streamable HTTP endpoint. - */ - url: string; - /** - * Tools to include. Defaults to all tools when omitted. - */ - tools?: string[]; +export interface ManagedSettingsValidateRequest { /** - * Timeout in milliseconds for tool discovery and tool calls. + * 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. */ - timeout?: number; + content: JsonValue; /** - * Maximum dynamic-header cache lifetime in milliseconds. + * 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. */ - headersRefreshTtlMs?: number; + layer?: string; } /** - * Validated device-managed settings discovered before a session exists. + * Result of validating a managed-settings document. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ManagedSettingsReadResult". + * via the `definition` "ManagedSettingsValidateResult". */ /** @experimental */ -export interface ManagedSettingsReadResult { +export interface ManagedSettingsValidateResult { /** - * Validated, canonical managed-settings JSON. Omitted when no managed settings were discovered or when discovered settings failed validation. + * 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. */ - settingsJson?: JsonValue; + valid: boolean; /** - * Discovery or validation error text when managed settings could not be read safely. + * Canonical form of the document the runtime would apply, with unrecognized keys removed. Absent when the document is invalid. */ - errorMessage?: string; + settings?: JsonValue; + /** + * Errors that reject the document and warnings about content the runtime ignores. + */ + diagnostics: ManagedSettingsDiagnostic[]; } /** * Result of registering a new marketplace. @@ -12138,6 +14900,42 @@ export interface McpAllowedServer { */ redactedNote?: string; } +/** + * Applies exactly one previously prepared operation on its original connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpApplyInstallRequest". + */ +/** @experimental */ +export interface McpApplyInstallRequest { + contract: CatalogClientContract; + /** + * Runtime-issued ID already returned by prepareInstall, never reused or rebound. + */ + operationId: string; + /** + * Same existing attached or privately borrowed session as preparation. + */ + policySessionId: string; +} +/** + * One-use application of the exact retained removal plan. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpApplyUninstallRequest". + */ +/** @experimental */ +export interface McpApplyUninstallRequest { + contract: CatalogClientContract; + /** + * Opaque original removal plan, consumed once. + */ + planHandle: string; + /** + * Same existing selected session as removal preparation. + */ + policySessionId: string; +} /** * MCP server, tool name, and arguments to invoke from an MCP App view. * @@ -12745,6 +15543,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. * @@ -12782,6 +15638,10 @@ export interface McpConfigureGitHubResult { */ /** @experimental */ export interface McpDisableRequest { + /** + * Required for an owned installation; omission preserves only manual-server behaviour. + */ + expectedInstallationId?: string; /** * Name of the MCP server to disable */ @@ -12825,6 +15685,10 @@ export interface McpDiscoverResult { */ /** @experimental */ export interface McpEnableRequest { + /** + * Exact receipt identity for explicit owned activation in this session. + */ + expectedInstallationId?: string; /** * Name of the MCP server to enable */ @@ -12979,333 +15843,204 @@ export interface McpHostState { /** * Map of server name to recorded pending-auth state. */ - needsAuthServers: { - [k: string]: McpServerNeedsAuthInfo | undefined; - }; -} -/** - * Recorded MCP server connection failure. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerFailureInfo". - */ -/** @experimental */ -export interface McpServerFailureInfo { - /** - * Failure message produced when the MCP server connection failed. - */ - message: string; - /** - * epoch-ms timestamp at which the failure was recorded. - */ - timestamp: number; -} -/** - * Recorded MCP server pending-auth state. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpServerNeedsAuthInfo". - */ -/** @experimental */ -export interface McpServerNeedsAuthInfo { - /** - * epoch-ms timestamp at which the server signalled it needs authentication. - */ - timestamp: number; -} -/** - * A normalised, inert description of what installing an MCP server would involve. Carries no raw card, no install specification, and no secret value. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpInstallPlan". - */ -/** @experimental */ -export interface McpInstallPlan { - /** - * Opaque, runtime-instance scoped, TTL-bound, single-use handle for this plan. Rejected when stale, replayed, or presented to a different runtime instance. Never logged. - */ - planHandle: string; - /** - * ISO 8601 timestamp after which the plan handle is stale and will be rejected. Abandoning a plan needs no call: an unused handle simply expires, so cancellation before commit is side-effect free. - */ - planHandleExpiresAt: string; - identity: McpPlanResourceIdentity; - provenance: McpPlanProvenance; - /** - * Every eligible transport, so a host can present an explicit choice. A completed plan always has at least one; when none is eligible, planning returns `CatalogUnavailableTransportError` instead. - * - * @minItems 1 - * @maxItems 50 - */ - transportChoices: [McpPlanTransportChoice, ...McpPlanTransportChoice[]]; - /** - * Identifier of the choice the runtime would pick by default. Omitted when there is no eligible transport, or when the runtime expresses no preference. - */ - recommendedTransportChoiceId?: string; - target: McpPlanTarget; - policy: McpPlanPolicyResult; - /** - * Alternative configuration changes, with exactly one entry for each transportChoices entry in the same order. Only the entry for the subsequently selected transport applies; these are not cumulative writes. Payloads remain behind the runtime boundary. - */ - configurationChanges: McpPlanConfigurationChange[]; - /** - * Whether applying this plan would require an MCP reload to take effect. Planning itself never reloads. - */ - reloadRequired: boolean; - /** - * True only when every eligible transport choice needs additional values or secrets. False means at least one choice needs no additional configuration, not that every choice is ready. A later apply operation must validate the selected choice's own inputs, secrets and policy after explicit confirmation. - */ - requiresInteractiveConfiguration: boolean; + needsAuthServers: { + [k: string]: McpServerNeedsAuthInfo | undefined; + }; } /** - * Normalised identity of the MCP server a plan targets, independent of how the card spelled it. + * Recorded MCP server connection failure. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanResourceIdentity". + * via the `definition` "McpServerFailureInfo". */ /** @experimental */ -export interface McpPlanResourceIdentity { - /** - * Canonical, normalised name of the server, for example `io.github.owner/server`. - */ - canonicalName: string; - /** - * Local configuration key the server would be recorded under. - */ - serverName: string; +export interface McpServerFailureInfo { /** - * Version advertised by the card, when it declares one. + * Failure message produced when the MCP server connection failed. */ - version?: string; + message: string; /** - * Registry identifier of the server, when it came from a registry. + * epoch-ms timestamp at which the failure was recorded. */ - registryId?: string; + timestamp: number; } /** - * Provenance of the exact validated JSON MCP card content bound privately to a completed plan and its opaque handle. + * Recorded MCP server pending-auth state. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanProvenance". + * via the `definition` "McpServerNeedsAuthInfo". */ /** @experimental */ -export interface McpPlanProvenance { - /** - * Authority associated with the validated card, without path, query, or credentials. Inert untrusted data. - */ - authority: string; +export interface McpServerNeedsAuthInfo { /** - * ISO 8601 timestamp at which the runtime completed strict parsing and schema validation of the card content. + * epoch-ms timestamp at which the server signalled it needs authentication. */ - validatedAt: string; - cardDigest: CardDigest; - mediaType: McpServerCardMediaType; + timestamp: number; } /** - * An eligible local-package transport choice. Package identity is required and a remote endpoint cannot be represented. + * Inert, runtime-owned admission. The operation ID is known before confirmation or effects. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanTransportChoicePackage". + * via the `definition` "McpPreparedInstall". */ /** @experimental */ -export interface McpPlanTransportChoicePackage { - /** - * Stable identifier for this choice within the plan, used to select it when the plan is applied. - */ - choiceId: string; - transport: McpPlanPackageTransport; - installMethod: McpPlanPackageInstallMethod; - /** - * Packaging ecosystem, for example `oci` or `npm`. - */ - packageType: string; - /** - * Package identifier. Inert untrusted data. - */ - packageIdentifier: string; +export interface McpPreparedInstall { /** - * Typed values this choice requires, excluding secrets. + * Original connection-owned operation, known before the first confirmation callback. */ - requiredValues: McpPlanRequiredValue[]; + operationId: string; /** - * Secrets this choice requires, referenced by placeholder only. + * Original plan expiry in Unix epoch milliseconds; preparation does not extend it. */ - secretPlaceholders: McpPlanSecretPlaceholder[]; + expiresAtEpochMs: number; } /** - * One non-secret scalar value a transport choice needs before it can be applied. + * Durable configuration ownership is distinct from session-specific usability. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanRequiredValueScalar". + * via the `definition` "McpInstallationSummary". */ /** @experimental */ -export interface McpPlanRequiredValueScalar { - kind: McpPlanRequiredValueScalarKind; - /** - * Key the value is supplied under. Inert untrusted data. - */ - key: string; - category: McpPlanValueCategory; - valueType: McpPlanScalarValueType; - /** - * Whether the value must be present for the plan to be applicable. - */ - required: boolean; +export interface McpInstallationSummary { /** - * Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. + * Exact durable installation receipt identity. */ - defaultValue?: string; + installationId: string; /** - * Human-readable label from the card. Inert untrusted text. + * Original installing operation, not a fresh management operation. */ - title?: string; + operationId: string; + identity: McpPlanResourceIdentity; /** - * Human-readable explanation from the card. Inert untrusted text. + * Exact alternative retained in the installing receipt. */ - description?: string; + choiceId: string; + state: McpInstallationState; + catalogue?: InstallationCatalogueIdentity; /** - * Whether the value may be supplied more than once. + * ISO 8601 wall-clock installation time when available. */ - isRepeated: boolean; + installedAt?: string; } /** - * One enumerated non-secret value a transport choice needs before it can be applied. The permitted values are structurally required. + * Exact inert removal plan. No configuration or credentials have changed. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanRequiredValueEnum". + * via the `definition` "McpUninstallPlan". */ /** @experimental */ -export interface McpPlanRequiredValueEnum { - kind: McpPlanRequiredValueEnumKind; - /** - * Key the value is supplied under. Inert untrusted data. - */ - key: string; - category: McpPlanValueCategory; - valueType: McpPlanEnumValueType; +export interface McpUninstallPlan { /** - * Whether the value must be present for the plan to be applicable. + * One-use original connection and authority-bound plan handle. */ - required: boolean; + planHandle: string; /** - * Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. + * The original operation, inspectable and cancellable on this same connection. */ - defaultValue?: string; + operationId: string; /** - * Human-readable label from the card. Inert untrusted text. + * Original wall-clock expiry in milliseconds. Applying never renews it. */ - title?: string; + expiresAtEpochMs: number; + installation: McpInstallationSummary; /** - * Human-readable explanation from the card. Inert untrusted text. + * Whether removal restores a protected earlier configuration. */ - description?: string; + restoresPreviousConfiguration: boolean; /** - * Non-empty permitted value set. Inert untrusted data. - * - * @minItems 1 + * Exact configured input slots owned by this installation, never shared OAuth tokens. */ - enumValues: [string, ...string[]]; + ownedSecretCount: number; /** - * Whether the value may be supplied more than once. + * Shared authentication is deliberately retained; revocation is a separate action. */ - isRepeated: boolean; + preservesSharedAuthentication: boolean; } /** - * A secret a transport choice needs, referenced by placeholder. No secret value ever appears in a plan, and the placeholder resolves against the keychain only when a plan is applied. + * Existing-operation control. A new session selector is deliberately not accepted. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanSecretPlaceholder". + * via the `definition` "McpInstallationOperationRequest". */ /** @experimental */ -export interface McpPlanSecretPlaceholder { - /** - * Key the secret is supplied under. Inert untrusted data. - */ - key: string; - placeholder: McpPlanSecretReference; +export interface McpInstallationOperationRequest { + contract: CatalogClientContract; /** - * Human-readable label from the card. Inert untrusted text. + * Exact runtime-issued operation ID on the original connection. */ - title?: string; + operationId: string; } /** - * An eligible remote-endpoint transport choice. The endpoint is required and package identity cannot be represented. + * A request-local value for one exact reviewed placeholder. Never logged or persisted in a plan. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanTransportChoiceRemote". + * via the `definition` "McpInstallationSecret". */ /** @experimental */ -export interface McpPlanTransportChoiceRemote { - /** - * Stable identifier for this choice within the plan, used to select it when the plan is applied. - */ - choiceId: string; - transport: McpPlanRemoteTransport; - installMethod: McpPlanRemoteInstallMethod; - /** - * Endpoint URL. Inert untrusted data. - */ - endpoint: string; - /** - * Typed values this choice requires, excluding secrets. - */ - requiredValues: McpPlanRequiredValue[]; +export interface McpInstallationSecret { + placeholder: McpPlanSecretReference; /** - * Secrets this choice requires, referenced by placeholder only. + * Fresh explicit secret value. It is omitted from confirmation reviews and telemetry. */ - secretPlaceholders: McpPlanSecretPlaceholder[]; + value: string; } /** - * Where a plan would be written. + * New-work inventory or recovery request under an explicitly selected existing session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanTarget". + * via the `definition` "McpInstallationsRequest". */ /** @experimental */ -export interface McpPlanTarget { - scope: McpPlanScope; +export interface McpInstallationsRequest { + contract: CatalogClientContract; /** - * Configuration key the server would be recorded under within that scope. + * Existing selected local session on this connection. */ - configKey: string; + policySessionId: string; } /** - * Outcome of evaluating the planned server against registry and enterprise policy. Evaluation is read-only. + * A normalised, inert description of what installing an MCP server would involve. Carries no raw card, no install specification, and no secret value. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanPolicyResult". + * via the `definition` "McpInstallPlan". */ /** @experimental */ -export interface McpPlanPolicyResult { - decision: McpPlanPolicyDecision; - source: McpPlanPolicySource; +export interface McpInstallPlan { /** - * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + * Opaque, runtime-instance scoped, TTL-bound, single-use handle for this plan. Rejected when stale, replayed, or presented to a different runtime instance. Never logged. */ - reason?: string; -} -/** - * The configuration-change alternative for the transportChoices entry at the same index. Only the selected alternative is applied; entries are not cumulative. The payload stays behind the runtime boundary. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpPlanConfigurationChange". - */ -/** @experimental */ -export interface McpPlanConfigurationChange { - operation: McpPlanConfigurationOperation; - scope: McpPlanScope; + planHandle: string; /** - * Configuration key the change applies to. + * ISO 8601 timestamp after which the plan handle is stale and will be rejected. Abandoning a plan needs no call: an unused handle simply expires, so cancellation before commit is side-effect free. */ - configKey: string; + planHandleExpiresAt: string; + identity: McpPlanResourceIdentity; + provenance: McpPlanProvenance; /** - * Names of the configuration fields the change would set, without their values. + * Every eligible transport, so a host can present an explicit choice. A completed plan always has at least one; when none is eligible, planning returns `CatalogUnavailableTransportError` instead. + * + * @minItems 1 + * @maxItems 50 */ - changedFields: string[]; + transportChoices: [McpPlanTransportChoice, ...McpPlanTransportChoice[]]; /** - * Secret placeholders the written configuration would reference. The constrained placeholder type cannot carry a literal secret value. + * Identifier of the choice the runtime would pick by default. Omitted when there is no eligible transport, or when the runtime expresses no preference. */ - secretReferences: McpPlanSecretReference[]; + recommendedTransportChoiceId?: string; + target: McpPlanTarget; + policy: McpPlanPolicyResult; + /** + * Alternative configuration changes, with exactly one entry for each transportChoices entry in the same order. Only the entry for the subsequently selected transport applies; these are not cumulative writes. Payloads remain behind the runtime boundary. + */ + configurationChanges: McpPlanConfigurationChange[]; + /** + * Whether applying this plan would require an MCP reload to take effect. Planning itself never reloads. + */ + reloadRequired: boolean; + /** + * True only when every eligible transport choice needs additional values or secrets. False means at least one choice needs no additional configuration, not that every choice is ready. A later apply operation must validate the selected choice's own inputs, secrets and policy after explicit confirmation. + */ + requiresInteractiveConfiguration: boolean; } /** * Server name to check running status for. @@ -13360,56 +16095,103 @@ export interface McpListToolsResult { tools: McpTools[]; } /** - * MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. + * MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpTools". + */ +/** @experimental */ +export interface McpTools { + /** + * Tool name. + */ + name: string; + /** + * Tool description, when provided. + */ + description?: string; + ui?: McpToolUi; +} +/** + * Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpToolUi". + */ +/** @experimental */ +export interface McpToolUi { + /** + * URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. + */ + resourceUri?: string; + /** + * Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + */ + visibility?: McpToolUiVisibility[]; +} +/** + * Identifies the MCP server whose persisted OAuth credentials were updated. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpOauthAuthenticationStateChangedRequest". + */ +/** @experimental */ +export interface McpOauthAuthenticationStateChangedRequest { + /** + * Name of the MCP server whose OAuth credentials were updated. Omit only when the host cannot identify the server. + */ + serverName?: string; + /** + * Whether the target session must mint a session-scoped access token instead of reusing a shared access token persisted by another session. + */ + refreshSessionToken?: boolean; +} +/** + * Targets only the original prepared/applying owned login on this exact session requester. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpTools". + * via the `definition` "McpOauthCancelLoginRequest". */ /** @experimental */ -export interface McpTools { +export interface McpOauthCancelLoginRequest { /** - * Tool name. + * The same authoritative installation identity supplied during preparation. */ - name: string; + expectedInstallationId: string; /** - * Tool description, when provided. + * Runtime-issued login handle known before the effectful login request begins. */ - description?: string; - ui?: McpToolUi; + loginId: string; } /** - * Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. + * Honest terminal cancellation result; persistence or recovery failures remain RPC errors. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpToolUi". + * via the `definition` "McpOauthCancelLoginResult". */ /** @experimental */ -export interface McpToolUi { - /** - * URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. - */ - resourceUri?: string; +export interface McpOauthCancelLoginResult { /** - * Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + * True after cancellation settles, false when the original login already connected successfully. */ - visibility?: McpToolUiVisibility[]; + cancelled: boolean; } /** - * Identifies the MCP server whose persisted OAuth credentials were updated. + * Host-delivered callback for a runtime-managed MCP OAuth login. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpOauthAuthenticationStateChangedRequest". + * via the `definition` "McpOauthCompleteRequest". */ /** @experimental */ -export interface McpOauthAuthenticationStateChangedRequest { +export interface McpOauthCompleteRequest { /** - * Name of the MCP server whose OAuth credentials were updated. Omit only when the host cannot identify the server. + * Opaque identifier returned by session.mcp.oauth.login for the pending external callback. */ - serverName?: string; + authorizationId: string; /** - * Whether the target session must mint a session-scoped access token instead of reusing a shared access token persisted by another session. + * 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. */ - refreshSessionToken?: boolean; + callbackUrl: string; } /** * Pending MCP OAuth request ID and host-provided token or cancellation response. @@ -13439,7 +16221,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". @@ -13475,6 +16257,19 @@ 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. + */ + loginId?: string; + /** + * Exact owned receipt identity. Owned login never uses an implicit helper session. + */ + expectedInstallationId?: string; } /** * OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. @@ -13485,9 +16280,64 @@ export interface McpOauthLoginRequest { /** @experimental */ export interface 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. + * Runtime-issued owned flow identity; never a server name or installation operation ID. + */ + 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. 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpOauthPrepareLoginRequest". + */ +/** @experimental */ +export interface McpOauthPrepareLoginRequest { + /** + * Name recorded by the authoritative owned installation receipt. + */ + serverName: string; + /** + * Exact installation identity from owned inventory, never a server-name alias. + */ + expectedInstallationId: string; + /** + * Request a new authorisation rather than accepting a usable cached grant. + */ + forceReauth?: boolean; + /** + * Display name used by the incumbent OAuth client-registration flow. + */ + clientName?: string; + /** + * Text shown on the loopback callback page after successful authorisation. + */ + callbackSuccessMessage?: string; +} +/** + * An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpOauthPrepareLoginResult". + */ +/** @experimental */ +export interface McpOauthPrepareLoginResult { + /** + * Retain with the original requester and use for one login or cancellation. + */ + loginId: string; + /** + * Original expiry, not extended by consumption, retries or cancellation. + */ + expiresAt: string; } /** * Remote MCP server name for a passive OAuth status probe. @@ -13497,6 +16347,10 @@ export interface McpOauthLoginResult { */ /** @experimental */ export interface McpOauthProbeRequest { + /** + * Exact owned receipt identity; probing never activates a dormant installation. + */ + expectedInstallationId?: string; /** * Name of the configured remote MCP server to probe. */ @@ -13554,6 +16408,10 @@ export interface McpPlanInstallRequest { contract: CatalogClientContract; source: McpPlanInstallSource; scope?: McpPlanScope; + /** + * The same existing attached session that owns the original catalogue candidate. + */ + policySessionId?: string; } /** * Plan from a candidate returned by a previous catalog search. @@ -13610,9 +16468,290 @@ export interface McpServerCardEmbedded { kind: McpServerCardEmbeddedKind; mediaType: McpServerCardMediaType; /** - * The card document verbatim, treated as inert untrusted bytes. The runtime parses and validates it; the host is not expected to interpret it. Never logged. + * The card document verbatim, treated as inert untrusted bytes. The runtime parses and validates it; the host is not expected to interpret it. Never logged. + */ + data: string; +} +/** + * Read-only preparation of one owned removal under fresh selected-session authority. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPlanUninstallRequest". + */ +/** @experimental */ +export interface McpPlanUninstallRequest { + contract: CatalogClientContract; + /** + * Exact receipt to inspect, not a server-name guess. + */ + installationId: string; + /** + * Existing selected session on the original connection. + */ + policySessionId: string; +} +/** + * Side-effect-free preparation of one original bound remote MCP choice. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPrepareInstallRequest". + */ +/** @experimental */ +export interface McpPrepareInstallRequest { + contract: CatalogClientContract; + /** + * Original single-use bound plan, never a client-authored configuration. + */ + planHandle: string; + /** + * Exact selected alternative from that plan. + */ + choiceId: string; + /** + * An existing local session attached to this connection, not permission to attach one. + */ + policySessionId: string; + /** + * Declared non-secret values. Non-empty only when the caller requires + * `mcp-configured-remote-installation`; omitted values use the card default. + */ + inputs: McpInstallationInput[]; + /** + * One entry per declared secret placeholder of the selected choice. Non-empty + * only when the caller requires `mcp-configured-remote-installation`. + */ + secrets: McpInstallationSecret[]; + 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 */ - data: string; + 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. @@ -13994,6 +17133,10 @@ export interface McpRestartServerRequest { */ serverName: string; config?: McpSerializableServerConfig; + /** + * Exact receipt identity for an explicit owned restart; configuration overrides are refused. + */ + expectedInstallationId?: string; } /** * Outcome of an MCP sampling execution: success result, failure error, or cancellation. @@ -14024,6 +17167,10 @@ export interface McpServer { name: string; status: McpServerStatus; source?: McpServerSource; + /** + * Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. + */ + url?: string; /** * Plugin name that provided this server, when source is plugin. */ @@ -14041,6 +17188,20 @@ export interface McpServer { */ error?: string; serverMetadata?: McpServerMetadata; + owned?: McpServerOwnership; +} +/** + * Owned installation that a listed MCP server's live configuration came from. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpServerOwnership". + */ +/** @experimental */ +export interface McpServerOwnership { + /** + * Stable installation identifier from the owned installation receipt. + */ + installationId: string; } /** * In-process MCP server configuration used by embedded SDK clients. @@ -14171,6 +17332,10 @@ export interface McpStartServerRequest { */ serverName: string; config?: McpSerializableServerConfig; + /** + * Exact receipt identity for explicit owned activation in this session. + */ + expectedInstallationId?: string; } /** * MCP server startup filtering result. @@ -14181,410 +17346,674 @@ export interface McpStartServerRequest { /** @experimental */ export interface McpStartServersResult { /** - * Servers filtered out before startup + * Servers filtered out before startup + */ + filteredServers: McpFilteredServer[]; + /** + * Non-default servers allowed by policy + */ + allowedServers?: McpAllowedServer[]; + /** + * Servers whose connection attempt failed. + */ + failedServers?: McpFailedServer[]; +} +/** + * Server name for an individual MCP server stop. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpStopServerRequest". + */ +/** @experimental */ +export interface McpStopServerRequest { + /** + * Exact owned receipt identity. Stop also forgets this session's durable activation. + */ + expectedInstallationId?: string; + /** + * Name of the MCP server to stop + */ + serverName: string; +} +/** + * Metadata controlling an MCP task's lifetime. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpTaskMetadata". + */ +/** @experimental */ +export interface McpTaskMetadata { + /** + * Task time-to-live. + */ + ttl?: number; +} +/** + * Server name identifying the external client to remove. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpUnregisterExternalClientRequest". + */ +/** @experimental */ +/** @internal */ +export interface McpUnregisterExternalClientRequest { + /** + * Server name of the external client to unregister + */ + serverName: string; +} +/** + * Memory configuration for this session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MemoryConfiguration". + */ +/** @experimental */ +export interface MemoryConfiguration { + /** + * Whether memory is enabled for the session. + */ + enabled: boolean; +} +/** + * Per-source attribution breakdown for the session's current context window, or null if uninitialized. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataContextAttributionResult". + */ +/** @experimental */ +export interface MetadataContextAttributionResult { + /** + * Per-source context-window attribution, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + */ + contextAttribution?: SessionContextAttribution | null; +} +/** + * Parameters for the heaviest-messages query. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataContextHeaviestMessagesRequest". + */ +/** @experimental */ +export interface MetadataContextHeaviestMessagesRequest { + /** + * Maximum number of messages to return, most-expensive first. Omit for the server default. + */ + limit?: number; +} +/** + * The heaviest individual messages in the session's context window, most-expensive first. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataContextHeaviestMessagesResult". + */ +/** @experimental */ +export interface MetadataContextHeaviestMessagesResult { + /** + * Total token count of the current context window, so callers can compute each message's share without a second call. + */ + totalTokens: number; + /** + * Heaviest messages, most-expensive first. + */ + messages: ContextHeaviestMessage[]; +} +/** + * Model identifier and token limits used to compute the context-info breakdown. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataContextInfoRequest". + */ +/** @experimental */ +export interface MetadataContextInfoRequest { + /** + * Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. + */ + promptTokenLimit: number; + /** + * Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. + */ + outputTokenLimit: number; + /** + * Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. + */ + selectedModel?: string; +} +/** + * Token breakdown for the session's current context window, or null if uninitialized. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataContextInfoResult". + */ +/** @experimental */ +export interface MetadataContextInfoResult { + /** + * Token breakdown for the current context window, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + */ + contextInfo?: SessionContextInfo | null; +} +/** + * Indicates whether the local session is currently processing a turn or background continuation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataIsProcessingResult". + */ +/** @experimental */ +export interface MetadataIsProcessingResult { + /** + * 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. + */ + processing: boolean; +} +/** + * Model identifier to use when re-tokenizing the session's existing messages. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataRecomputeContextTokensRequest". + */ +/** @experimental */ +export interface MetadataRecomputeContextTokensRequest { + /** + * Model identifier used for tokenization. The runtime token-counts both chat-context and system-context messages against this model. + */ + modelId: string; +} +/** + * 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataRecomputeContextTokensResult". + */ +/** @experimental */ +export interface MetadataRecomputeContextTokensResult { + /** + * Sum of tokens across chat-context and system-context messages currently held by the session. + */ + totalTokens: number; + /** + * Tokens contributed by user/assistant/tool messages (excludes system/developer prompts). + */ + messagesTokenCount: number; + /** + * Tokens contributed by system/developer prompt snapshots. + */ + systemTokenCount: number; +} +/** + * Updated working-directory/git context to record on the session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MetadataRecordContextChangeRequest". + */ +/** @experimental */ +export interface MetadataRecordContextChangeRequest { + context: SessionWorkingDirectoryContext; +} +/** + * Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionWorkingDirectoryContext". + */ +/** @experimental */ +export interface SessionWorkingDirectoryContext { + /** + * Current working directory path + */ + cwd: string; + /** + * Root directory of the git repository, resolved via git rev-parse + */ + gitRoot?: string; + /** + * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) + */ + repository?: string; + hostType?: SessionWorkingDirectoryContextHostType; + /** + * Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com") + */ + repositoryHost?: string; + /** + * Current git branch name */ - filteredServers: McpFilteredServer[]; + branch?: string; /** - * Non-default servers allowed by policy + * Head commit of the current git branch */ - allowedServers?: McpAllowedServer[]; + headCommit?: string; /** - * Servers whose connection attempt failed. + * Merge-base commit SHA (fork point from the remote default branch) */ - failedServers?: McpFailedServer[]; + baseCommit?: string; } /** - * Server name for an individual MCP server stop. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpStopServerRequest". + * via the `definition` "MetadataRecordContextChangeResult". */ /** @experimental */ -export interface McpStopServerRequest { - /** - * Name of the MCP server to stop - */ - serverName: string; -} +export interface MetadataRecordContextChangeResult {} /** - * Metadata controlling an MCP task's lifetime. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpTaskMetadata". + * via the `definition` "MetadataSetWorkingDirectoryRequest". */ /** @experimental */ -export interface McpTaskMetadata { +export interface MetadataSetWorkingDirectoryRequest { /** - * Task time-to-live. + * 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. */ - ttl?: number; + workingDirectory: string; } /** - * Server name identifying the external client to remove. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpUnregisterExternalClientRequest". + * via the `definition` "MetadataSetWorkingDirectoryResult". */ /** @experimental */ -/** @internal */ -export interface McpUnregisterExternalClientRequest { +export interface MetadataSetWorkingDirectoryResult { /** - * Server name of the external client to unregister + * Working directory after the update */ - serverName: string; + workingDirectory: string; } /** - * Memory configuration for this session. + * Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MemoryConfiguration". + * via the `definition` "MetadataSnapshotRemoteMetadata". */ /** @experimental */ -export interface MemoryConfiguration { +export interface MetadataSnapshotRemoteMetadata { /** - * Whether memory is enabled for the session. + * The original resource identifier (task ID or PR node ID), preserved across event-replay reconstructions. Falls back to `sessionId` when absent. */ - enabled: boolean; -} -/** - * Per-source attribution breakdown for the session's current context window, or null if uninitialized. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataContextAttributionResult". - */ -/** @experimental */ -export interface MetadataContextAttributionResult { + resourceId?: string; + repository: MetadataSnapshotRemoteMetadataRepository; /** - * Per-source context-window attribution, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + * The pull request number the remote session is associated with, if any. */ - contextAttribution?: SessionContextAttribution | null; + pullRequestNumber?: number; + taskType?: MetadataSnapshotRemoteMetadataTaskType; } /** - * Parameters for the heaviest-messages query. + * The repository the remote session targets. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataContextHeaviestMessagesRequest". + * via the `definition` "MetadataSnapshotRemoteMetadataRepository". */ /** @experimental */ -export interface MetadataContextHeaviestMessagesRequest { +export interface MetadataSnapshotRemoteMetadataRepository { /** - * Maximum number of messages to return, most-expensive first. Omit for the server default. + * The GitHub owner (user or organization) of the target repository. */ - limit?: number; -} -/** - * The heaviest individual messages in the session's context window, most-expensive first. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataContextHeaviestMessagesResult". - */ -/** @experimental */ -export interface MetadataContextHeaviestMessagesResult { + owner: string; /** - * Total token count of the current context window, so callers can compute each message's share without a second call. + * The GitHub repository name (without owner). */ - totalTokens: number; + name: string; /** - * Heaviest messages, most-expensive first. + * The branch the remote session is operating on. */ - messages: ContextHeaviestMessage[]; + branch: string; } /** - * Model identifier and token limits used to compute the context-info breakdown. + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataContextInfoRequest". + * via the `definition` "MetadataUpdateClientMetadataRequest". */ /** @experimental */ -export interface MetadataContextInfoRequest { +export interface MetadataUpdateClientMetadataRequest { /** - * Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + * Remove every existing client metadata entry before applying remove and set. Defaults to false. */ - promptTokenLimit: number; + clear?: boolean; /** - * Maximum output tokens allowed by the target model. Pass 0 if unknown. + * 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. + * + * @maxItems 128 */ - outputTokenLimit: number; + remove?: string[]; /** - * Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. + * 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. */ - selectedModel?: string; + set?: { + [k: string]: string | undefined; + }; } /** - * Token breakdown for the session's current context window, or null if uninitialized. + * Copilot model metadata, including identifier, display name, capabilities, policy, billing, reasoning efforts, and picker categories. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataContextInfoResult". + * via the `definition` "Model". */ /** @experimental */ -export interface MetadataContextInfoResult { +export interface Model { /** - * Token breakdown for the current context window, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + * Model identifier (e.g., "claude-sonnet-4.5") */ - contextInfo?: SessionContextInfo | null; + id: string; + /** + * Display name + */ + name: string; + capabilities: ModelCapabilities; + /** + * Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + */ + metadata?: { + [k: string]: JsonValue | undefined; + }; + /** + * Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. + */ + vendor?: string; + policy?: ModelPolicy; + billing?: ModelBilling; + /** + * Supported reasoning effort levels (only present if model supports reasoning effort) + */ + supportedReasoningEfforts?: string[]; + /** + * Default reasoning effort level (only present if model supports reasoning effort) + */ + defaultReasoningEffort?: string; + /** + * Context-window tiers this model offers, when the provider advertises them independently of tiered token pricing. Copilot models carry their tiers in `billing.tokenPrices`; a provider that has no pricing to publish (an agent host reached over AHP, for example) declares them here instead, so the model picker can still offer the tier toggle. + */ + supportedContextTiers?: string[]; + modelPickerCategory?: ModelPickerCategory; + modelPickerPriceCategory?: ModelPickerPriceCategory; + warningText?: ModelWarningText; + /** + * Informational notices the service published for this model, such as an upcoming change or a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model. + */ + infoMessages?: ModelMessage[]; + /** + * Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. + */ + warningMessages?: ModelMessage[]; + provider?: ModelProviderRef; } /** - * Indicates whether the local session is currently processing a turn or background continuation. + * Policy state (if applicable) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataIsProcessingResult". + * via the `definition` "ModelPolicy". */ /** @experimental */ -export interface MetadataIsProcessingResult { +export interface ModelPolicy { + state: ModelPolicyState; /** - * 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. + * Usage terms or conditions for this model */ - processing: boolean; + terms?: string; } /** - * Model identifier to use when re-tokenizing the session's existing messages. + * Billing information * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataRecomputeContextTokensRequest". + * via the `definition` "ModelBilling". */ /** @experimental */ -export interface MetadataRecomputeContextTokensRequest { +export interface ModelBilling { /** - * Model identifier used for tokenization. The runtime token-counts both chat-context and system-context messages against this model. + * Billing cost multiplier relative to the base rate */ - modelId: string; + multiplier?: number; + tokenPrices?: ModelBillingTokenPrices; + /** + * Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. + */ + discountPercent?: number; + promo?: ModelBillingPromo; } /** - * 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. + * Token-level pricing information for this model * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataRecomputeContextTokensResult". + * via the `definition` "ModelBillingTokenPrices". */ /** @experimental */ -export interface MetadataRecomputeContextTokensResult { +export interface ModelBillingTokenPrices { /** - * Sum of tokens across chat-context and system-context messages currently held by the session. + * AI Credits cost per billing batch of input tokens */ - totalTokens: number; + inputPrice?: number; /** - * Tokens contributed by user/assistant/tool messages (excludes system/developer prompts). + * AI Credits cost per billing batch of output tokens */ - messagesTokenCount: number; + outputPrice?: number; /** - * Tokens contributed by system/developer prompt snapshots. + * @deprecated + * Use cacheReadPrice instead. AI Credits cost per billing batch of cached tokens */ - systemTokenCount: number; -} -/** - * Updated working-directory/git context to record on the session. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataRecordContextChangeRequest". - */ -/** @experimental */ -export interface MetadataRecordContextChangeRequest { - context: SessionWorkingDirectoryContext; + cachePrice?: number; + /** + * AI Credits cost per billing batch of cached (read) tokens + */ + cacheReadPrice?: number; + /** + * AI Credits cost per billing batch of cache-write (cache creation) tokens. + */ + cacheWritePrice?: number; + /** + * AI Credits cost per billing batch of 1-hour cache-write (cache creation) tokens. + */ + cacheWrite1hPrice?: number; + /** + * Number of tokens per standard billing batch + */ + batchSize?: number; + /** + * @deprecated + * Use maxPromptTokens instead. Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. + */ + contextMax?: number; + /** + * Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. + */ + maxPromptTokens?: number; + longContext?: ModelBillingTokenPricesLongContext; } /** - * Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + * Long context tier pricing (available for models with extended context windows) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionWorkingDirectoryContext". + * via the `definition` "ModelBillingTokenPricesLongContext". */ /** @experimental */ -export interface SessionWorkingDirectoryContext { +export interface ModelBillingTokenPricesLongContext { + /** + * AI Credits cost per billing batch of input tokens + */ + inputPrice?: number; /** - * Current working directory path + * AI Credits cost per billing batch of output tokens */ - cwd: string; + outputPrice?: number; /** - * Root directory of the git repository, resolved via git rev-parse + * @deprecated + * Use cacheReadPrice instead. AI Credits cost per billing batch of cached tokens */ - gitRoot?: string; + cachePrice?: number; /** - * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) + * AI Credits cost per billing batch of cached (read) tokens */ - repository?: string; - hostType?: SessionWorkingDirectoryContextHostType; + cacheReadPrice?: number; /** - * Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com") + * AI Credits cost per billing batch of cache-write (cache creation) tokens. */ - repositoryHost?: string; + cacheWritePrice?: number; /** - * Current git branch name + * AI Credits cost per billing batch of 1-hour cache-write (cache creation) tokens. */ - branch?: string; + cacheWrite1hPrice?: number; /** - * Head commit of the current git branch + * @deprecated + * Use maxPromptTokens instead. Prompt token budget for the long context tier. The total context window is this value plus the model's max_output_tokens. */ - headCommit?: string; + contextMax?: number; /** - * Merge-base commit SHA (fork point from the remote default branch) + * Prompt token budget for the long context tier. The total context window is this value plus the model's max_output_tokens. */ - baseCommit?: string; + maxPromptTokens?: number; } /** - * 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. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataRecordContextChangeResult". - */ -/** @experimental */ -export interface MetadataRecordContextChangeResult {} -/** - * 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. + * Active server-driven promotion for a model, including its discount and optional expiry. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataSetWorkingDirectoryRequest". + * via the `definition` "ModelBillingPromo". */ /** @experimental */ -export interface MetadataSetWorkingDirectoryRequest { +export interface ModelBillingPromo { /** - * 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. + * Stable identifier for the promotion campaign. */ - workingDirectory: string; + id?: string; + /** + * Percentage discount (0-100) applied while the promotion is active. May be fractional. + */ + discountPercent?: number; + /** + * UTC ISO 8601 timestamp marking when the promotion ends. Optional: an open-ended promotion omits this field. When present, the API only surfaces a promo whose expiry parses and is in the future, so consumers should treat a past value as expired. + */ + endsAt?: string; + /** + * Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. + */ + message?: string; + /** + * Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + */ + showBanner?: boolean; } /** - * 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. + * Service-published warning text that hosts should display when presenting a model. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataSetWorkingDirectoryResult". + * via the `definition` "ModelWarningText". */ /** @experimental */ -export interface MetadataSetWorkingDirectoryResult { +export interface ModelWarningText { /** - * Working directory after the update + * Data-retention warning for the model. The text may contain Markdown links and should be rendered as Markdown when supported. */ - workingDirectory: string; + dataRetention?: string; } /** - * Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. + * A service-published message about a model, carrying a stable machine-readable code alongside human-readable text. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataSnapshotRemoteMetadata". + * via the `definition` "ModelMessage". */ /** @experimental */ -export interface MetadataSnapshotRemoteMetadata { +export interface ModelMessage { /** - * The original resource identifier (task ID or PR node ID), preserved across event-replay reconstructions. Falls back to `sessionId` when absent. + * Stable machine-readable identifier for the message, such as `client_version_deprecated`. Hosts can key custom presentation off this; unrecognized codes should fall back to displaying `message`. */ - resourceId?: string; - repository: MetadataSnapshotRemoteMetadataRepository; + code: string; /** - * The pull request number the remote session is associated with, if any. + * Human-readable message text intended for display to the user. */ - pullRequestNumber?: number; - taskType?: MetadataSnapshotRemoteMetadataTaskType; + message: string; } /** - * The repository the remote session targets. + * A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataSnapshotRemoteMetadataRepository". + * via the `definition` "ModelProviderRef". */ /** @experimental */ -export interface MetadataSnapshotRemoteMetadataRepository { - /** - * The GitHub owner (user or organization) of the target repository. - */ - owner: string; +export interface ModelProviderRef { /** - * The GitHub repository name (without owner). + * Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. */ - name: string; + id: string; /** - * The branch the remote session is operating on. + * Human-readable provider label, owned by the runtime so every consumer renders identical text. */ - branch: string; + label: string; + kind: ModelProviderKind; } /** - * 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. + * Managed, repository, and CLI model overrides to overlay onto the session at startup. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MetadataUpdateClientMetadataRequest". + * via the `definition` "ModelApplyStartupOverlayRequest". */ /** @experimental */ -export interface MetadataUpdateClientMetadataRequest { +export interface ModelApplyStartupOverlayRequest { /** - * Remove every existing client metadata entry before applying remove and set. Defaults to false. + * Model required by device-managed policy, when configured. */ - clear?: boolean; + deviceManagedModel?: string; /** - * 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. - * - * @maxItems 128 + * Model required by server-managed policy, when configured. */ - remove?: string[]; + serverManagedModel?: string; /** - * 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. + * 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. */ - set?: { - [k: string]: string | undefined; - }; -} -/** - * Copilot model metadata, including identifier, display name, capabilities, policy, billing, reasoning efforts, and picker categories. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "Model". - */ -/** @experimental */ -export interface Model { + policyHelperModel?: string; + autoTier?: AutoTier; /** - * Model identifier (e.g., "claude-sonnet-4.5") + * Model selected by repository settings, when configured. */ - id: string; + repoModel?: string; /** - * Display name + * Reasoning effort selected by repository settings, when configured. */ - name: string; - capabilities: ModelCapabilities; + repoReasoningEffort?: string; /** - * Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + * Context tier selected by repository settings, when configured. */ - metadata?: { - [k: string]: JsonValue | undefined; - }; - policy?: ModelPolicy; - billing?: ModelBilling; + repoContextTier?: string; /** - * Supported reasoning effort levels (only present if model supports reasoning effort) + * 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. */ - supportedReasoningEfforts?: string[]; + repoAutoTier?: string; /** - * Default reasoning effort level (only present if model supports reasoning effort) + * Model explicitly selected by the CLI, when provided. */ - defaultReasoningEffort?: string; + cliModel?: string; /** - * Context-window tiers this model offers, when the provider advertises them independently of tiered token pricing. Copilot models carry their tiers in `billing.tokenPrices`; a provider that has no pricing to publish (an agent host reached over AHP, for example) declares them here instead, so the model picker can still offer the tier toggle. + * Whether the overlay is being applied while resuming a deferred session. */ - supportedContextTiers?: string[]; - modelPickerCategory?: ModelPickerCategory; - modelPickerPriceCategory?: ModelPickerPriceCategory; - warningText?: ModelWarningText; + deferredResume?: boolean; /** - * Informational notices the service published for this model, such as an upcoming change or a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model. + * 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. */ - infoMessages?: ModelMessage[]; + managedReasoningEffort?: string; /** - * Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. + * 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. */ - warningMessages?: ModelMessage[]; + managedContextTier?: string; } /** - * Model capabilities and limits + * Optional capability overrides (vision, tool_calls, reasoning, etc.). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilities". + * via the `definition` "ModelCapabilitiesOverride". */ /** @experimental */ -export interface ModelCapabilities { - supports?: ModelCapabilitiesSupports; - limits?: ModelCapabilitiesLimits; +export interface ModelCapabilitiesOverride { + supports?: ModelCapabilitiesOverrideSupports; + limits?: ModelCapabilitiesOverrideLimits; } /** * Feature flags indicating what the model supports * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesSupports". + * via the `definition` "ModelCapabilitiesOverrideSupports". */ /** @experimental */ -export interface ModelCapabilitiesSupports { +export interface ModelCapabilitiesOverrideSupports { /** * Whether this model supports vision/image input */ @@ -14603,10 +18032,10 @@ export interface ModelCapabilitiesSupports { * Token limits for prompts, outputs, and context window * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesLimits". + * via the `definition` "ModelCapabilitiesOverrideLimits". */ /** @experimental */ -export interface ModelCapabilitiesLimits { +export interface ModelCapabilitiesOverrideLimits { /** * Maximum number of prompt/input tokens */ @@ -14619,380 +18048,504 @@ export interface ModelCapabilitiesLimits { * Maximum total context window size in tokens */ max_context_window_tokens?: number; - vision?: ModelCapabilitiesLimitsVision; + vision?: ModelCapabilitiesOverrideLimitsVision; } /** * Vision-specific limits * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesLimitsVision". + * via the `definition` "ModelCapabilitiesOverrideLimitsVision". */ /** @experimental */ -export interface ModelCapabilitiesLimitsVision { +export interface ModelCapabilitiesOverrideLimitsVision { /** * MIME types the model accepts */ - supported_media_types: string[]; + supported_media_types?: string[]; /** * Maximum number of images per prompt */ - max_prompt_images: number; + max_prompt_images?: number; /** * Maximum image size in bytes */ - max_prompt_image_size: number; + max_prompt_image_size?: number; } /** - * Policy state (if applicable) + * List of Copilot models available to the resolved user, including capabilities and billing metadata. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelPolicy". + * via the `definition` "ModelList". */ /** @experimental */ -export interface ModelPolicy { - state: ModelPolicyState; +export interface ModelList { /** - * Usage terms or conditions for this model + * List of available models with full metadata */ - terms?: string; + models: Model[]; } -/** - * Billing information - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelBilling". - */ + /** @experimental */ -export interface ModelBilling { +export interface ModelPickerPersistenceRequest { + settingsContext: ModelPickerSettingsContext; /** - * Billing cost multiplier relative to the base rate + * Whether reasoning effort was explicitly selected and should be persisted. */ - multiplier?: number; - tokenPrices?: ModelBillingTokenPrices; + reasoningEffortExplicit?: boolean; /** - * Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. + * Whether context tier was explicitly selected and should be persisted. */ - discountPercent?: number; - promo?: ModelBillingPromo; + contextTierExplicit?: boolean; } /** - * Token-level pricing information for this model + * Filesystem and environment context used to resolve model-picker settings. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelBillingTokenPrices". + * via the `definition` "ModelPickerSettingsContext". */ /** @experimental */ -export interface ModelBillingTokenPrices { - /** - * AI Credits cost per billing batch of input tokens - */ - inputPrice?: number; +export interface ModelPickerSettingsContext { /** - * AI Credits cost per billing batch of output tokens + * Optional Copilot configuration directory containing persisted settings. */ - outputPrice?: number; + configDir?: string; /** - * @deprecated - * Use cacheReadPrice instead. AI Credits cost per billing batch of cached tokens + * User home directory used when resolving persisted settings. */ - cachePrice?: number; + homeDirectory: string; /** - * AI Credits cost per billing batch of cached (read) tokens + * Environment variables consulted while resolving model-picker settings. */ - cacheReadPrice?: number; + 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 { /** - * AI Credits cost per billing batch of cache-write (cache creation) tokens. + * Available provider adapters ordered by adapterId. */ - cacheWritePrice?: number; + 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 { /** - * AI Credits cost per billing batch of 1-hour cache-write (cache creation) tokens. + * Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. */ - cacheWrite1hPrice?: number; + adapterId: string; /** - * Number of tokens per standard billing batch + * Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. */ - batchSize?: number; + providerKind: string; /** - * @deprecated - * Use maxPromptTokens instead. Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. + * Human-readable provider name. */ - contextMax?: number; + displayName: string; + provenance: ModelProviderAttribution; + automaticDiscovery: ModelProviderAutomaticDiscoveryPolicy; /** - * Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. + * Operations supported by this provider adapter. */ - maxPromptTokens?: number; - longContext?: ModelBillingTokenPricesLongContext; + operations: ModelProviderAdapterOperationDescriptor[]; } /** - * Long context tier pricing (available for models with extended context windows) + * Contributor attribution, independent of routing identity and authorization. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelBillingTokenPricesLongContext". + * via the `definition` "ModelProviderAttribution". */ /** @experimental */ -export interface ModelBillingTokenPricesLongContext { +export interface ModelProviderAttribution { + source: ModelProviderProvenanceSource; /** - * AI Credits cost per billing batch of input tokens + * Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. */ - inputPrice?: number; + ownerId?: string; /** - * AI Credits cost per billing batch of output tokens + * Human-readable contributor name, not the adapter display name. */ - outputPrice?: number; + 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; /** - * @deprecated - * Use cacheReadPrice instead. AI Credits cost per billing batch of cached tokens + * 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. */ - cachePrice?: number; + requiresInput: boolean; /** - * AI Credits cost per billing batch of cached (read) tokens + * True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. */ - cacheReadPrice?: number; + 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 { /** - * AI Credits cost per billing batch of cache-write (cache creation) tokens. + * Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. */ - cacheWritePrice?: number; + name: string; /** - * AI Credits cost per billing batch of 1-hour cache-write (cache creation) tokens. + * 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. */ - cacheWrite1hPrice?: number; + 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; /** - * @deprecated - * Use maxPromptTokens instead. Prompt token budget for the long context tier. The total context window is this value plus the model's max_output_tokens. + * Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. */ - contextMax?: number; + selectionId: string; + providerDisposition: ModelProviderConfigurationDisposition; + modelDisposition: ModelProviderConfigurationDisposition; /** - * Prompt token budget for the long context tier. The total context window is this value plus the model's max_output_tokens. + * Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. */ - maxPromptTokens?: number; + warnings: ModelProviderWarning[]; } /** - * Active server-driven promotion for a model, including its discount and optional expiry. + * 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` "ModelBillingPromo". + * via the `definition` "NamedProviderConfig". */ /** @experimental */ -export interface ModelBillingPromo { +export interface NamedProviderConfig { /** - * Stable identifier for the promotion campaign. + * Unique provider name used to qualify model selection IDs. */ - id?: string; + name: string; + type?: ProviderConfigType; + wireApi?: ProviderConfigWireApi; + transport?: ProviderConfigTransport; /** - * Percentage discount (0-100) applied while the promotion is active. May be fractional. + * Base URL for provider API requests. */ - discountPercent?: number; + baseUrl: string; /** - * UTC ISO 8601 timestamp marking when the promotion ends. Optional: an open-ended promotion omits this field. When present, the API only surfaces a promo whose expiry parses and is in the future, so consumers should treat a past value as expired. + * Static API key used to authenticate provider requests. */ - endsAt?: string; + apiKey?: string; /** - * Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. + * Static bearer token used to authenticate provider requests. */ - message?: string; + bearerToken?: string; + azure?: ProviderConfigAzure; /** - * Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + * Additional HTTP headers included with provider requests. */ - showBanner?: boolean; + headers?: { + [k: string]: string | undefined; + }; + modelProvider?: ProviderConfigModelProvider; + /** + * Whether the host supplies bearer tokens dynamically. + */ + hasBearerTokenProvider?: boolean; } /** - * Service-published warning text that hosts should display when presenting a model. + * Azure-specific provider options. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelWarningText". + * via the `definition` "ProviderConfigAzure". */ /** @experimental */ -export interface ModelWarningText { +export interface ProviderConfigAzure { /** - * Data-retention warning for the model. The text may contain Markdown links and should be rendered as Markdown when supported. + * API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. */ - dataRetention?: string; + apiVersion?: string; } /** - * A service-published message about a model, carrying a stable machine-readable code alongside human-readable text. + * A BYOK model definition referencing a named provider. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelMessage". + * via the `definition` "ProviderModelConfig". */ /** @experimental */ -export interface ModelMessage { +export interface ProviderModelConfig { /** - * Stable machine-readable identifier for the message, such as `client_version_deprecated`. Hosts can key custom presentation off this; unrecognized codes should fall back to displaying `message`. + * 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`. */ - code: string; + id: string; /** - * Human-readable message text intended for display to the user. + * Name of the configured provider that serves this model. */ - message: string; + 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; + }; } -/** - * Managed, repository, and CLI model overrides to overlay onto the session at startup. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelApplyStartupOverlayRequest". - */ + /** @experimental */ -export interface ModelApplyStartupOverlayRequest { +export interface ProtocolSystemMessageAppendConfig { + mode?: ProtocolAppendMode; /** - * Model required by device-managed policy, when configured. + * Text appended to the standard system prompt. */ - deviceManagedModel?: string; + content?: string; +} + +/** @experimental */ +export interface ProtocolSystemMessageReplaceConfig { + mode: ProtocolReplaceMode; /** - * Model required by server-managed policy, when configured. + * Complete replacement system-message text. */ - serverManagedModel?: string; + content: string; /** - * 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. + * Optional structured blocks corresponding to the replacement content. */ - policyHelperModel?: string; - autoTier?: AutoTier; + contentBlocks?: SystemMessageBlock[]; +} + +/** @experimental */ +export interface SystemMessageBlock { /** - * Model selected by repository settings, when configured. + * Text content for this system-message block. */ - repoModel?: string; + content: string; /** - * Reasoning effort selected by repository settings, when configured. + * Whether the block is static and may be cached independently of dynamic prompt content. */ - repoReasoningEffort?: string; + isStatic?: boolean; /** - * Context tier selected by repository settings, when configured. + * Whether providers with explicit prompt caching should place a cache breakpoint after this block. */ - repoContextTier?: string; + cacheBreakpoint?: boolean; +} + +/** @experimental */ +export interface ProtocolSystemMessageCustomizeConfig { + mode: ProtocolCustomizeMode; /** - * 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. + * Named standard-prompt section overrides. */ - repoAutoTier?: string; + sections?: { + [k: string]: ProtocolSectionOverride | undefined; + }; /** - * Model explicitly selected by the CLI, when provided. + * Text appended after the customized sections. */ - cliModel?: string; + content?: string; +} + +/** @experimental */ +export interface ProtocolStaticSectionOverride { + action: ProtocolStaticSectionAction; /** - * Whether the overlay is being applied while resuming a deferred session. + * Optional content used by replace, append, and prepend operations. */ - deferredResume?: boolean; + content?: string; } /** - * Optional capability overrides (vision, tool_calls, reasoning, etc.). + * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesOverride". + * via the `definition` "ModelProviderDescriptor". */ /** @experimental */ -export interface ModelCapabilitiesOverride { - supports?: ModelCapabilitiesOverrideSupports; - limits?: ModelCapabilitiesOverrideLimits; +export interface ModelProviderDescriptor { + kind: ModelProviderKind; + /** + * Opaque, stable provider id, stamped onto every model this provider returns. + */ + id: string; + /** + * Human-readable menu label, owned by the runtime so every consumer renders identical text. + */ + label: string; + /** + * Stable ordering key for presenting providers in a deterministic sequence. + */ + ordering: number; } /** - * Feature flags indicating what the model supports + * Provider discovery parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesOverrideSupports". + * via the `definition` "ModelProviderDiscoverRequest". */ /** @experimental */ -export interface ModelCapabilitiesOverrideSupports { +export interface ModelProviderDiscoverRequest { /** - * Whether this model supports vision/image input + * Opaque adapter identity returned by `session.providers.getCatalog`. */ - vision?: boolean; + adapterId: string; /** - * Whether this model supports canonical tool calling + * 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. */ - toolCalls?: boolean; + 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; /** - * Whether this model supports reasoning effort configuration + * Discovered provider instances. Empty when passive default discovery finds no reachable provider. */ - reasoningEffort?: boolean; - adaptive_thinking?: AdaptiveThinkingSupport; + instances: ModelProviderInstance[]; } /** - * Token limits for prompts, outputs, and context window + * A normalized model-provider instance discovered by the runtime. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesOverrideLimits". + * via the `definition` "ModelProviderInstance". */ /** @experimental */ -export interface ModelCapabilitiesOverrideLimits { - /** - * Maximum number of prompt/input tokens - */ - max_prompt_tokens?: number; +export interface ModelProviderInstance { + reference: ModelProviderInstanceReference; /** - * Maximum number of output/completion tokens + * Human-readable instance name. */ - max_output_tokens?: number; + displayName: string; + provenance: ModelProviderProvenance; /** - * Maximum total context window size in tokens + * Inference API endpoint when the provider exposes one separately from its management endpoint. */ - max_context_window_tokens?: number; - vision?: ModelCapabilitiesOverrideLimitsVision; + inferenceEndpoint?: string; + inferenceType?: ProviderEndpointType; + inferenceWireApi?: ProviderEndpointWireApi; + inferenceTransport?: ProviderEndpointTransport; } /** - * Vision-specific limits + * Serializable reference to a discovered provider instance. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesOverrideLimitsVision". + * via the `definition` "ModelProviderInstanceReference". */ /** @experimental */ -export interface ModelCapabilitiesOverrideLimitsVision { +export interface ModelProviderInstanceReference { /** - * MIME types the model accepts + * Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. */ - supported_media_types?: string[]; + adapterId: string; /** - * Maximum number of images per prompt + * Descriptive provider family. Must match the selected adapter; not a routing key. */ - max_prompt_images?: number; + providerKind: string; /** - * Maximum image size in bytes + * Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. */ - max_prompt_image_size?: number; + 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; } /** - * List of Copilot models available to the resolved user, including capabilities and billing metadata. + * Provider status request parameters. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelList". + * via the `definition` "ModelProviderGetStatusRequest". */ /** @experimental */ -export interface ModelList { - /** - * List of available models with full metadata - */ - models: Model[]; +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 ModelPickerPersistenceRequest { - settingsContext: ModelPickerSettingsContext; - /** - * Whether reasoning effort was explicitly selected and should be persisted. - */ - reasoningEffortExplicit?: boolean; - /** - * Whether context tier was explicitly selected and should be persisted. - */ - contextTierExplicit?: boolean; +export interface ModelProviderModelsListRequest { + instance: ModelProviderInstanceReference; } /** - * Filesystem and environment context used to resolve model-picker settings. + * 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` "ModelPickerSettingsContext". + * via the `definition` "ModelProviderPrepareConfigurationRequest". */ /** @experimental */ -export interface ModelPickerSettingsContext { - /** - * Optional Copilot configuration directory containing persisted settings. - */ - configDir?: string; +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; /** - * User home directory used when resolving persisted settings. + * Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. */ - homeDirectory: string; + status: string; /** - * Environment variables consulted while resolving model-picker settings. + * Provider-reported version. */ - environment: {}; + 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. @@ -15320,58 +18873,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. * @@ -15676,19 +19177,19 @@ export interface PermissionDecisionApproveForSessionApprovalExtensionManagement operation?: string; } /** - * Session-scoped factory approval, optionally narrowed by approval key. + * Session-scoped workflow approval, optionally narrowed by approval key. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionDecisionApproveForSessionApprovalFactory". + * via the `definition` "PermissionDecisionApproveForSessionApprovalWorkflow". */ /** @experimental */ -export interface PermissionDecisionApproveForSessionApprovalFactory { +export interface PermissionDecisionApproveForSessionApprovalWorkflow { /** - * Approval covering factory operations. + * Approval covering workflow operations. */ - kind: "factory"; + kind: "workflow"; /** - * Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. + * Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. */ approvalKey?: string; } @@ -15732,6 +19233,25 @@ export interface PermissionDecisionApproveForSessionApprovalExtensionEnvAccess { */ environmentVariables: [string, ...string[]]; } +/** + * Approve file-tool read access to specific directories for this logical session, including continuation or resume. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "PermissionDecisionApproveReadOnlyForSession". + */ +/** @experimental */ +export interface PermissionDecisionApproveReadOnlyForSession { + /** + * Approve read-only file-tool directory access for this session + */ + kind: "approve-read-only-for-session"; + /** + * Canonical directories covered by the read-only grant + * + * @minItems 1 + */ + directories: [string, ...string[]]; +} /** * Permission-decision request variant to approve and persist a permission for a project location, with approval details and location key. * @@ -15879,19 +19399,19 @@ export interface PermissionDecisionApproveForLocationApprovalExtensionManagement operation?: string; } /** - * Location-scoped factory approval, optionally narrowed by approval key. + * Location-scoped workflow approval, optionally narrowed by approval key. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionDecisionApproveForLocationApprovalFactory". + * via the `definition` "PermissionDecisionApproveForLocationApprovalWorkflow". */ /** @experimental */ -export interface PermissionDecisionApproveForLocationApprovalFactory { +export interface PermissionDecisionApproveForLocationApprovalWorkflow { /** - * Approval covering factory operations. + * Approval covering workflow operations. */ - kind: "factory"; + kind: "workflow"; /** - * Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. + * Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. */ approvalKey?: string; } @@ -16308,19 +19828,19 @@ export interface PermissionsLocationsAddToolApprovalDetailsExtensionManagement { operation?: string; } /** - * Location-persisted factory approval, optionally narrowed by approval key. + * Location-persisted workflow approval, optionally narrowed by approval key. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionsLocationsAddToolApprovalDetailsFactory". + * via the `definition` "PermissionsLocationsAddToolApprovalDetailsWorkflow". */ /** @experimental */ -export interface PermissionsLocationsAddToolApprovalDetailsFactory { +export interface PermissionsLocationsAddToolApprovalDetailsWorkflow { /** - * Approval covering factory operations. + * Approval covering workflow operations. */ - kind: "factory"; + kind: "workflow"; /** - * Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. + * Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. */ approvalKey?: string; } @@ -17465,159 +20985,48 @@ export interface ProtocolExternalToolDefinition { */ title?: string; /** - * JSON Schema describing the tool's input arguments. - */ - parameters?: { - [k: string]: JsonValue | undefined; - }; - /** - * Whether this definition replaces a built-in tool with the same name. - */ - overridesBuiltInTool?: boolean; - /** - * Whether execution bypasses the normal tool permission prompt. - */ - skipPermission?: boolean; - defer?: ProtocolExternalToolDefer; - /** - * Whether the tool executes commands in a terminal. - */ - isTerminal?: boolean; - /** - * Optional caller-defined metadata associated with the tool. - */ - metadata?: { - [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. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderAddRequest". - */ -/** @experimental */ -export interface ProviderAddRequest { - /** - * 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 '/'. - */ - providers?: NamedProviderConfig[]; - /** - * 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. - */ - 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`). + * JSON Schema describing the tool's input arguments. */ - name?: string; + parameters?: { + [k: string]: JsonValue | undefined; + }; /** - * Maximum prompt/input tokens for the model. + * Whether this definition replaces a built-in tool with the same name. */ - maxPromptTokens?: number; + overridesBuiltInTool?: boolean; /** - * Maximum context window tokens for the model. + * Whether execution bypasses the normal tool permission prompt. */ - maxContextWindowTokens?: number; + skipPermission?: boolean; + defer?: ProtocolExternalToolDefer; /** - * Maximum output tokens for the model. + * Whether the tool executes commands in a terminal. */ - maxOutputTokens?: number; - capabilities?: ModelCapabilitiesOverride; - systemMessage?: ProtocolSystemMessageConfig; + isTerminal?: boolean; /** - * Provider-published model metadata, preserved verbatim as the public Model.metadata object. + * Optional caller-defined metadata associated with the tool. */ metadata?: { [k: string]: JsonValue | undefined; }; } +/** + * BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderAddRequest". + */ +/** @experimental */ +export interface ProviderAddRequest { + /** + * 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 '/'. + */ + providers?: NamedProviderConfig[]; + /** + * 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. + */ + models?: ProviderModelConfig[]; +} /** * The selectable model entries synthesized for the models added by this call. * @@ -17664,6 +21073,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. */ @@ -17811,6 +21221,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 * @@ -19194,6 +22638,7 @@ export interface SandboxConfig { */ managedLspRoutingLocked?: boolean; auth?: SandboxConfigAuth; + credentials?: SandboxCredentialsConfig; /** * 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). */ @@ -19329,14 +22774,42 @@ export interface SandboxConfigUserPolicyExperimentalSeatbelt { /** @experimental */ export interface SandboxConfigAuth { /** - * Whether to inject git credentials as an `http..extraheader` so authenticated HTTPS git works inside the sandbox without the shell-based credential helper the sandbox blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's own helper before the sandbox is applied. Default: false (opt-in). + * Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http..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). */ git?: boolean; /** - * Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the OS keyring the sandbox blocks. Default: false (opt-in). + * 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). */ gh?: boolean; } +/** + * Whole-value environment credential masking for sandboxed children. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxCredentialsConfig". + */ +/** @experimental */ +export interface SandboxCredentialsConfig { + /** + * Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. + */ + envVars: { + [k: string]: SandboxMaskedEnvVar | undefined; + }; +} +/** + * Destinations authorized to receive one masked environment credential. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxMaskedEnvVar". + */ +/** @experimental */ +export interface 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. + */ + injectHosts: string[]; +} /** * Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. * @@ -19390,7 +22863,34 @@ export interface SandboxEnforcementStatus { reason?: string; } /** - * Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`. + * Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxGrantPathForRequestRequest". + */ +/** @experimental */ +export interface SandboxGrantPathForRequestRequest { + /** + * Identifier of the exact pending sandbox escalation permission request whose sandboxPathGrant to accept. + */ + requestId: string; + decisionContext?: PermissionDecisionContext; +} +/** + * Result of accepting a sandbox path grant. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxGrantPathForRequestResult". + */ +/** @experimental */ +export interface SandboxGrantPathForRequestResult { + /** + * Whether this call resolved the pending request and added the path to the session's sandbox policy. + */ + 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`, 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". @@ -19428,6 +22928,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. * @@ -20074,6 +23615,23 @@ export interface SessionBulkDeleteResult { [k: string]: number | undefined; }; } +/** + * The IDE a host is connected to, as reported to the session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionConnectedIdeInfo". + */ +/** @experimental */ +export interface SessionConnectedIdeInfo { + /** + * Display name of the connected IDE, for example `VS Code`. + */ + ideName: string; + /** + * Absolute path of the workspace folder the IDE has open. + */ + workspaceFolder: string; +} /** * The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted. * @@ -20125,6 +23683,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. @@ -20257,6 +23819,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. * @@ -20346,6 +23939,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. @@ -20556,6 +24153,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. * @@ -20841,6 +24463,10 @@ export interface SessionManagedPermissions { * Permission rules that allow matching operations unless another managed source, deny, or ask rule restricts them. */ allow?: string[]; + /** + * Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected because every network request must be enforceable at host-level egress. Multiple managed sources intersect their lists; an empty list denies all hosts. + */ + limitTo?: string[]; } /** * Managed settings an SDK host may inject at session startup. Only permissions are accepted in this initial contract. @@ -20852,6 +24478,19 @@ export interface SessionManagedPermissions { export interface SessionManagedSettings { permissions?: SessionManagedPermissions; } +/** + * Records which IDE the host is connected to, or clears it. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionMcpSetConnectedIdeInfoParams". + */ +/** @experimental */ +export interface SessionMcpSetConnectedIdeInfoParams { + /** + * The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + */ + ide?: SessionConnectedIdeInfo | null; +} /** * Point-in-time snapshot of slow-changing session identifier and state fields * @@ -20928,6 +24567,10 @@ export interface SessionModelList { * 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`). */ list: JsonValue[]; + /** + * 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. + */ + providers?: ModelProviderDescriptor[]; /** * 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. */ @@ -21100,9 +24743,9 @@ export interface SessionOpenOptions { shellProcessFlags?: string[]; sandboxConfig?: SandboxConfig; /** - * Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. + * 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. * - * @internal + * @experimental */ sandboxConfigSource?: SandboxConfigSource; /** @@ -21713,13 +25356,97 @@ export interface SessionsCloseRequest { sessionId: string; } /** - * Closes a session: emits shutdown, flushes pending events to disk, releases the in-use lock, disposes the active session. Idempotent: succeeds even if the session is not currently active. + * Closes a session: emits shutdown, flushes pending events to disk, releases the in-use lock, disposes the active session. Idempotent: succeeds even if the session is not currently active. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsCloseResult". + */ +/** @experimental */ +export interface SessionsCloseResult {} +/** + * Identity, state location and starting context for a workspace record. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsCreateWorkspaceRequest". + */ +/** @experimental */ +export interface SessionsCreateWorkspaceRequest { + /** + * Session ID the workspace record belongs to + */ + sessionId: string; + /** + * Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + */ + sessionStatePath: string; + /** + * `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + */ + convention: string; + context?: SessionWorkingDirectoryContextWithClient; + /** + * User-supplied display name for the workspace + */ + name?: string; +} +/** + * A working-directory context together with the client that produced it. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionWorkingDirectoryContextWithClient". + */ +/** @experimental */ +export interface SessionWorkingDirectoryContextWithClient { + /** + * Current working directory path + */ + cwd: string; + /** + * Root directory of the git repository + */ + gitRoot?: string; + /** + * Repository identifier derived from the git remote URL + */ + repository?: string; + /** + * Hosting platform type of the repository + */ + hostType?: string; + /** + * Current git branch name + */ + branch?: string; + /** + * Raw host string from the git remote URL + */ + repositoryHost?: string; + /** + * Head commit of the current git branch + */ + headCommit?: string; + /** + * Merge-base commit SHA + */ + baseCommit?: string; + /** + * Name of the client that created the session + */ + clientName?: string; +} +/** + * The workspace record that was written. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionsCloseResult". + * via the `definition` "SessionsCreateWorkspaceResult". */ /** @experimental */ -export interface SessionsCloseResult {} +export interface SessionsCreateWorkspaceResult { + /** + * The created workspace record, as JSON + */ + workspaceJson: string; +} /** * Session ID to delete from disk. * @@ -22347,6 +26074,36 @@ export interface SessionsLoadDeferredRepoHooksRequest { */ sessionId: string; } +/** + * Where the session's state lives, as a root directory and the session ID under it. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsLoadWorkspaceRequest". + */ +/** @experimental */ +export interface SessionsLoadWorkspaceRequest { + /** + * Root directory every session's state directory sits under + */ + sessionsHome: string; + /** + * Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + */ + sessionId: string; +} +/** + * The workspace record on disk, omitted when the session has none. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsLoadWorkspaceResult". + */ +/** @experimental */ +export interface SessionsLoadWorkspaceResult { + /** + * The workspace record, as JSON. Omitted when the record does not exist. + */ + workspaceJson?: string; +} /** * Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). * @@ -22538,6 +26295,35 @@ export interface SessionsTransferRemoteControlRequest { */ expectedFromSessionId?: string; } +/** + * Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsUpdateWorkspaceFieldsRequest". + */ +/** @experimental */ +export interface SessionsUpdateWorkspaceFieldsRequest { + /** + * Root directory every session's state directory sits under + */ + sessionsHome: string; + /** + * Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + */ + sessionId: string; + /** + * Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + */ + fieldsJson: string; +} +/** + * The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsUpdateWorkspaceFieldsResult". + */ +/** @experimental */ +export interface SessionsUpdateWorkspaceFieldsResult {} /** * Telemetry engagement ID for the session, when available. * @@ -22631,9 +26417,9 @@ export interface SessionUpdateOptionsParams { shellProcessFlags?: string[]; sandboxConfig?: SandboxConfig; /** - * Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. + * 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. * - * @internal + * @experimental */ sandboxConfigSource?: SandboxConfigSource; /** @@ -22808,7 +26594,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". @@ -22829,7 +26615,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". @@ -22837,7 +26623,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; } @@ -22945,6 +26731,42 @@ export interface Skill { */ argumentHint?: string; } +/** + * Applies exactly one retained verified Skill installation plan. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillApplyInstallRequest". + */ +/** @experimental */ +export interface SkillApplyInstallRequest { + contract: CatalogClientContract; + /** + * Opaque original plan, consumed once. + */ + planHandle: string; + /** + * Same existing selected session as planning. + */ + policySessionId: string; +} +/** + * One-use application of the exact retained Skill removal plan. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillApplyUninstallRequest". + */ +/** @experimental */ +export interface SkillApplyUninstallRequest { + contract: CatalogClientContract; + /** + * Opaque original removal plan, consumed once. + */ + planHandle: string; + /** + * Same existing selected session as removal preparation. + */ + policySessionId: string; +} /** * Canonical directory where skills can be discovered or created, with scope, preference, and optional project path. * @@ -22980,6 +26802,79 @@ export interface SkillDiscoveryPathList { */ paths: SkillDiscoveryPath[]; } +/** + * A computed Skill install plan. Nothing has been applied. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillInstallPlan". + */ +/** @experimental */ +export interface SkillInstallPlan { + /** + * One-use plan handle, bound to the original candidate authority. + */ + planHandle: string; + /** + * Original operation identifier returned before confirmation. + */ + operationId: string; + /** + * Original wall-clock expiry as an ISO 8601 timestamp. + */ + expiresAt: string; + review: SkillInstallationReview; +} +/** + * A computed Skill uninstall plan. Nothing has been removed. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillUninstallPlan". + */ +/** @experimental */ +export interface SkillUninstallPlan { + /** + * One-use uninstall plan handle. + */ + planHandle: string; + /** + * Original removal operation identifier returned before confirmation. + */ + operationId: string; + /** + * Original wall-clock expiry as an ISO 8601 timestamp. + */ + expiresAt: string; + installation: SkillInstallationSummary; + review: SkillInstallationReview; +} +/** + * Existing-operation control. A new session selector is deliberately not accepted. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillInstallationOperationRequest". + */ +/** @experimental */ +export interface SkillInstallationOperationRequest { + contract: CatalogClientContract; + /** + * Exact runtime-issued operation ID on the original connection. + */ + operationId: string; +} +/** + * Inventory request under an explicitly selected existing session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillInstallationsRequest". + */ +/** @experimental */ +export interface SkillInstallationsRequest { + contract: CatalogClientContract; + /** + * Existing selected local session on this connection. + */ + policySessionId: string; +} /** * Skills available to the session, with their enabled state. * @@ -22994,7 +26889,43 @@ export interface SkillList { skills: Skill[]; } /** - * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * Side-effect-free planning of one verified Agent Finder Skill candidate. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillPlanInstallRequest". + */ +/** @experimental */ +export interface SkillPlanInstallRequest { + contract: CatalogClientContract; + /** + * Fresh single-use AI skill candidate handle returned by a bound catalogue search. + */ + candidateHandle: string; + /** + * Existing local session attached to this connection. + */ + policySessionId: string; +} +/** + * Read-only preparation of one owned Skill removal under fresh selected-session authority. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillPlanUninstallRequest". + */ +/** @experimental */ +export interface SkillPlanUninstallRequest { + contract: CatalogClientContract; + /** + * Exact receipt to inspect. + */ + installationId: string; + /** + * Existing selected local session on this connection. + */ + policySessionId: string; +} +/** + * Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is fetched separately and lazily. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "SkillProviderDescriptor". @@ -23057,7 +26988,7 @@ export interface SkillProviderReadRequest { name: string; } /** - * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter is optional: fields it omits come from the catalog descriptor, fields it declares must match the descriptor, and `allowed-tools` is read only from frontmatter. Related files and assets are not supported. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "SkillProviderReadResult". @@ -23066,9 +26997,9 @@ export interface SkillProviderReadRequest { /** @internal */ export interface SkillProviderReadResult { /** - * Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + * SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. */ - markdown: string; + markdown: string | null; } /** * Skill names to mark as disabled in global configuration, replacing any previous list. @@ -23151,6 +27082,28 @@ export interface SkillsEnableRequest { */ name: string; } +/** + * Persisted enablement update for one owned Skill installation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillSetEnabledRequest". + */ +/** @experimental */ +export interface SkillSetEnabledRequest { + contract: CatalogClientContract; + /** + * Exact receipt identity to update. + */ + installationId: string; + /** + * Persisted enablement value. + */ + enabled: boolean; + /** + * Existing selected local session to reconcile after persistence. + */ + policySessionId: string; +} /** * Optional project paths to enumerate. * @@ -23445,6 +27398,7 @@ export interface SlashCommandSetModelResult { * Model selected by the command. */ model: string; + autoTier?: AutoTier; /** * Settings scope modified by the command. */ @@ -25429,7 +29383,7 @@ export interface UserSettingMetadata { isDefault: boolean; } /** - * Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + * Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "UserSettingsGetResult". @@ -25456,19 +29410,6 @@ export interface UserSettingsSetRequest { */ settings: JsonValue; } -/** - * Outcome of writing user settings. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "UserSettingsSetResult". - */ -/** @experimental */ -export interface UserSettingsSetResult { - /** - * Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. - */ - shadowedKeys: string[]; -} /** * Current sharing status and shareable GitHub URL for a session. * @@ -26986,102 +30927,214 @@ export interface WorkspacesSaveLargePasteResult { * via the `definition` "WorkspacesStatFileRequest". */ /** @experimental */ -export interface WorkspacesStatFileRequest { +export interface WorkspacesStatFileRequest { + /** + * Slash-separated relative path within the workspace files directory + */ + path: string; +} +/** + * Filesystem metadata for a path in the session workspace files directory. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspacesStatFileResult". + */ +/** @experimental */ +export interface WorkspacesStatFileResult { + /** + * Whether the path identifies a regular file + */ + isFile: boolean; + /** + * Whether the path identifies a directory + */ + isDirectory: boolean; + /** + * Size in bytes + */ + size: number; + /** + * Last modification time in Unix epoch milliseconds + */ + mtimeMs: number; + /** + * Creation time in Unix epoch milliseconds + */ + birthtimeMs: number; +} +/** + * Rollback point for local workspace summaries. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspacesTruncateSummariesRequest". + */ +/** @experimental */ +export interface WorkspacesTruncateSummariesRequest { + /** + * Number of newest summaries to keep. + */ + keepCount: number; +} +/** + * Workspace metadata fields to update. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspacesUpdateMetadataRequest". + */ +/** @experimental */ +export interface WorkspacesUpdateMetadataRequest { + /** + * Opaque workspace context supplied by the session host. + */ + context?: JsonValue; + /** + * Optional workspace display name override. + */ + name?: string; +} +/** + * Autopilot objective file content to persist. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspacesWriteAutopilotObjectiveRequest". + */ +/** @experimental */ +export interface WorkspacesWriteAutopilotObjectiveRequest { + /** + * Autopilot objective file content. + */ + content: string; +} +/** + * Result of writing the autopilot objective file. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WorkspacesWriteAutopilotObjectiveResult". + */ +/** @experimental */ +export interface WorkspacesWriteAutopilotObjectiveResult { + /** + * Filesystem operation performed. + */ + operation: string; +} +/** + * The resident session the application has materialized on its own connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostCreateSessionResult". + */ +/** @experimental */ +export interface HostCreateSessionResult { /** - * Slash-separated relative path within the workspace files directory + * Runtime session UUID materialized on the application's SDK connection. */ - path: string; + sessionId: string; } /** - * Filesystem metadata for a path in the session workspace files directory. + * One application-owned session handoff, requested by the supervised hosting participant. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspacesStatFileResult". + * via the `definition` "HostCreateSessionRequest". */ /** @experimental */ -export interface WorkspacesStatFileResult { - /** - * Whether the path identifies a regular file - */ - isFile: boolean; - /** - * Whether the path identifies a directory - */ - isDirectory: boolean; +export interface HostCreateSessionRequest { /** - * Size in bytes + * Unique identity for this participation, independent of the session lifetime. */ - size: number; + handoffId: string; /** - * Last modification time in Unix epoch milliseconds + * Resume an app-owned durable session instead of creating a new session. */ - mtimeMs: number; + resume?: boolean; /** - * Creation time in Unix epoch milliseconds + * Host-selected SDK creation or resume settings, without executable callbacks or tools. */ - birthtimeMs: number; + config: { + [k: string]: unknown | undefined; + }; } /** - * Rollback point for local workspace summaries. + * Empty acknowledgement for a completed host lifecycle operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspacesTruncateSummariesRequest". + * via the `definition` "HostReleaseSessionResult". */ /** @experimental */ -export interface WorkspacesTruncateSummariesRequest { +export interface HostReleaseSessionResult {} +/** + * Ends one participation, not the application's session lifetime. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostReleaseSessionRequest". + */ +/** @experimental */ +export interface HostReleaseSessionRequest { /** - * Number of newest summaries to keep. + * Identity of the participation to release without destroying the session. */ - keepCount: number; + handoffId: string; } /** - * Workspace metadata fields to update. + * Empty acknowledgement for a completed host lifecycle operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspacesUpdateMetadataRequest". + * via the `definition` "HostDisposeResult". */ /** @experimental */ -export interface WorkspacesUpdateMetadataRequest { +export interface HostDisposeResult {} +/** + * Private credentials delivered only to a runtime-owned Mission Control hosting participant. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostGetEnvironmentCredentialsResult". + */ +/** @experimental */ +export interface HostGetEnvironmentCredentialsResult { /** - * Opaque workspace context supplied by the session host. + * Current bearer token for the authenticated GitHub identity. */ - context?: JsonValue; + token: string; /** - * Optional workspace display name override. + * Hostname of the authenticated GitHub service. */ - name?: string; + githubHost: string; + /** + * GitHub API base URL for the authenticated service. + */ + githubApiUrl: string; + /** + * Mission Control API origin for environment registration and management. + */ + missionControlUrl: string; } /** - * Autopilot objective file content to persist. + * Normalized listener settings delivered only to the supervised hosting participant. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspacesWriteAutopilotObjectiveRequest". + * via the `definition` "HostGetConfigurationResult". */ /** @experimental */ -export interface WorkspacesWriteAutopilotObjectiveRequest { +export interface HostGetConfigurationResult { + localServer?: HostLocalServerConfiguration; + githubEnvironment?: HostGitHubEnvironmentOptions; /** - * Autopilot objective file content. + * Whether session materialization is delegated to the owning application. */ - content: string; + sessionFactory: boolean; + /** + * Whether app-owned durable sessions are resumed by the owning application. + */ + resumeFactory: boolean; } /** - * Result of writing the autopilot objective file. + * Empty acknowledgement for a completed host lifecycle operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "WorkspacesWriteAutopilotObjectiveResult". + * via the `definition` "HostReadyResult". */ /** @experimental */ -export interface WorkspacesWriteAutopilotObjectiveResult { - /** - * Filesystem operation performed. - */ - operation: string; -} - -/** @experimental */ -export interface SessionFactoryPauseAtCheckpointResult { - action: FactoryPauseCheckpointAction; -} +export interface HostReadyResult {} /** @experimental */ export interface SessionWorkflowPauseAtCheckpointResult { @@ -27107,6 +31160,82 @@ export interface SessionAgentListRequest { */ includePrompt?: boolean; } +/** + * An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionMcpOauthPrepareLoginResult". + */ +/** @experimental */ +export interface SessionMcpOauthPrepareLoginResult { + /** + * Retain with the original requester and use for one login or cancellation. + */ + loginId: string; + /** + * Original expiry, not extended by consumption, retries or cancellation. + */ + expiresAt: string; +} +/** + * Effect-free preparation bound to the existing local session, requester and installation, with frozen options. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionMcpOauthPrepareLoginRequest". + */ +/** @experimental */ +export interface SessionMcpOauthPrepareLoginRequest { + /** + * Name recorded by the authoritative owned installation receipt. + */ + serverName: string; + /** + * Exact installation identity from owned inventory, never a server-name alias. + */ + expectedInstallationId: string; + /** + * Request a new authorisation rather than accepting a usable cached grant. + */ + forceReauth?: boolean; + /** + * Display name used by the incumbent OAuth client-registration flow. + */ + clientName?: string; + /** + * Text shown on the loopback callback page after successful authorisation. + */ + callbackSuccessMessage?: string; +} +/** + * Honest terminal cancellation result; persistence or recovery failures remain RPC errors. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionMcpOauthCancelLoginResult". + */ +/** @experimental */ +export interface SessionMcpOauthCancelLoginResult { + /** + * True after cancellation settles, false when the original login already connected successfully. + */ + cancelled: boolean; +} +/** + * Targets only the original prepared/applying owned login on this exact session requester. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionMcpOauthCancelLoginRequest". + */ +/** @experimental */ +export interface SessionMcpOauthCancelLoginRequest { + /** + * The same authoritative installation identity supplied during preparation. + */ + expectedInstallationId: string; + /** + * Runtime-issued login handle known before the effectful login request begins. + */ + loginId: string; +} /** * Standard MCP CallToolResult * @@ -27227,10 +31356,223 @@ export interface SessionFsSqliteExistsRequest { */ sessionId: string; } +/** + * The existing runtime identity and its resource on the listener. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostRegisterSessionResult". + */ +/** @experimental */ +export interface HostRegisterSessionResult { + /** + * Canonical runtime ID of the published session. + */ + sessionId: string; + /** + * AHP resource URI for the session on this listener. + */ + sessionUri: string; +} +/** + * The resident session the application has materialized on its own connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostMaterializeSessionResult". + */ +/** @experimental */ +export interface HostMaterializeSessionResult { + /** + * Runtime session UUID materialized on the application's SDK connection. + */ + sessionId: string; +} +/** + * Application callback routed over its existing SDK connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostMaterializeSessionRequest". + */ +/** @experimental */ +export interface HostMaterializeSessionRequest { + /** + * Listener UUID identifying the owning application's host. + */ + hostId: string; + /** + * Unique identity of the session participation being requested. + */ + handoffId: string; + /** + * Resume an app-owned durable session instead of creating a new session. + */ + resume?: boolean; + /** + * Host-selected SDK creation or resume settings, without executable callbacks or tools. + */ + config: { + [k: string]: unknown | undefined; + }; +} +/** + * Releases the original application session object retained for one handoff. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostSessionReleasedRequest". + */ +/** @experimental */ +export interface HostSessionReleasedRequest { + /** + * Listener UUID whose application session participation ended. + */ + hostId: string; + /** + * Identity of the handoff retaining the original application session object. + */ + handoffId: string; +} +/** + * Empty acknowledgement for a completed host lifecycle operation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostShutdownResult". + */ +/** @experimental */ +export interface HostShutdownResult {} +/** + * Reports a supervised listener's hosting-task termination and cleanup outcome. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostExitedRequest". + */ +/** @experimental */ +export interface HostExitedRequest { + /** + * Listener UUID. + */ + hostId: string; + reason: HostExitReason; + /** + * Process exit status when available; absent for in-process listener tasks. + */ + exitCode?: number | null; + /** + * Explicit startup or teardown failure, when present. + */ + error?: string | null; +} +/** + * A response is meaningful only on the connection and request that issued its challenge. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "InstallationsConfirmResult". + */ +/** @experimental */ +export interface InstallationsConfirmResult { + /** + * Exact challenge from the request. + */ + confirmationId: string; + /** + * Exact review commitment from the request. + */ + reviewFingerprint: string; + decision: InstallationDecision; +} +/** + * One connection-owned, expiring request for a trusted host's explicit user decision. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "InstallationsConfirmRequest". + */ +/** @experimental */ +export interface InstallationsConfirmRequest { + /** + * 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. + */ + policySessionId?: string; + /** + * Opaque one-use challenge. Return unchanged; never log or persist. + */ + confirmationId: string; + /** + * Random identifier of this installation operation, not a plan handle. + */ + operationId: string; + /** + * Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. + */ + expiresAt: string; + /** + * Opaque commitment to the exact review and inputs. Return unchanged; never log. + */ + reviewFingerprint: string; + review: InstallationReview; +} /** 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: { + /** + * Publishes an attached resident session for this listener's lifetime without copying it. + * + * @param params Publishes a resident session attached to the listener's owning connection. + * + * @returns The existing runtime identity and its resource on the listener. + */ + publishSession: async (params: HostPublishSessionRequest): Promise => + connection.sendRequest("host.publishSession", params), + /** + * Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. + * + * @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. + */ + start: async (params: HostStartRequest): Promise => + connection.sendRequest("host.start", params), + /** + * Stops a listener owned by this SDK connection and joins its cleanup without deleting sessions. + * + * @param params Stops a connection-owned listener and joins its teardown. + * + * @returns Empty acknowledgement for a completed host lifecycle operation. + */ + dispose: async (params: HostDisposeRequest): Promise => + connection.sendRequest("host.dispose", params), + }, /** * Checks server responsiveness and returns protocol information. * @@ -27282,6 +31624,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: { @@ -27421,6 +31809,81 @@ export function createServerRpc(connection: MessageConnection) { */ planInstall: async (params: McpPlanInstallRequest): Promise => connection.sendRequest("mcp.planInstall", params), + /** + * 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. + * + * @param params Side-effect-free preparation of one original bound remote MCP choice. + * + * @returns Management result with contract receipt, or a typed request/negotiation refusal. + */ + prepareInstall: async (params: McpPrepareInstallRequest): Promise => + connection.sendRequest("mcp.prepareInstall", params), + /** + * 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. + * + * @param params Applies exactly one previously prepared operation on its original connection. + * + * @returns An installation result together with the exact honoured contract, or a negotiation refusal. + */ + applyInstall: async (params: McpApplyInstallRequest): Promise => + connection.sendRequest("mcp.applyInstall", params), + /** + * 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. + * + * @param params Read-only preparation of one owned removal under fresh selected-session authority. + * + * @returns Management result with contract receipt, or a typed request/negotiation refusal. + */ + planUninstall: async (params: McpPlanUninstallRequest): Promise => + connection.sendRequest("mcp.planUninstall", params), + /** + * 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. + * + * @param params One-use application of the exact retained removal plan. + * + * @returns An installation result together with the exact honoured contract, or a negotiation refusal. + */ + applyUninstall: async (params: McpApplyUninstallRequest): Promise => + connection.sendRequest("mcp.applyUninstall", params), + /** @experimental */ + installations: { + /** + * 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. + * + * @param params New-work inventory or recovery request under an explicitly selected existing session. + * + * @returns Management result with contract receipt, or a typed request/negotiation refusal. + */ + list: async (params: McpInstallationsRequest): Promise => + connection.sendRequest("mcp.installations.list", params), + /** + * 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. + * + * @param params New-work inventory or recovery request under an explicitly selected existing session. + * + * @returns Management result with contract receipt, or a typed request/negotiation refusal. + */ + recover: async (params: McpInstallationsRequest): Promise => + connection.sendRequest("mcp.installations.recover", params), + /** + * 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. + * + * @param params Existing-operation control. A new session selector is deliberately not accepted. + * + * @returns Management result with contract receipt, or a typed request/negotiation refusal. + */ + status: async (params: McpInstallationOperationRequest): Promise => + connection.sendRequest("mcp.installations.status", params), + /** + * 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. + * + * @param params Existing-operation control. A new session selector is deliberately not accepted. + * + * @returns Management result with contract receipt, or a typed request/negotiation refusal. + */ + cancel: async (params: McpInstallationOperationRequest): Promise => + connection.sendRequest("mcp.installations.cancel", params), + }, }, /** @experimental */ extensions: { @@ -27446,6 +31909,128 @@ export function createServerRpc(connection: MessageConnection) { disable: async (params: DiscoveredExtensionsDisableRequest): Promise => connection.sendRequest("extensions.disable", params), }, + /** @experimental */ + skills: { + /** + * 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. + * + * @param params Side-effect-free planning of one verified Agent Finder Skill candidate. + * + * @returns Skill installation management result with the honoured contract, or a typed refusal. + */ + planInstall: async (params: SkillPlanInstallRequest): Promise => + connection.sendRequest("skills.planInstall", params), + /** + * Consumes one verified Skill installation plan, requests explicit human consent through installations.confirm on the original connection, then revalidates and installs the Skill disabled. + * + * @param params Applies exactly one retained verified Skill installation plan. + * + * @returns Skill installation result with the honoured contract, or a typed request/negotiation refusal. + */ + applyInstall: async (params: SkillApplyInstallRequest): Promise => + connection.sendRequest("skills.applyInstall", params), + /** @experimental */ + installations: { + /** + * Lists owned verified Agent Finder Skill installations for the selected existing session. Listing is never gated by the Skill-install feature flag. + * + * @param params Inventory request under an explicitly selected existing session. + * + * @returns Skill installation management result with the honoured contract, or a typed refusal. + */ + list: async (params: SkillInstallationsRequest): Promise => + connection.sendRequest("skills.installations.list", params), + /** + * 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. + * + * @param params Inventory request under an explicitly selected existing session. + * + * @returns Skill installation management result with the honoured contract, or a typed refusal. + */ + recover: async (params: SkillInstallationsRequest): Promise => + connection.sendRequest("skills.installations.recover", params), + /** + * Inspects a known Skill installation operation on its original runtime connection. Status is never gated by the Skill-install feature flag. + * + * @param params Existing-operation control. A new session selector is deliberately not accepted. + * + * @returns Skill installation management result with the honoured contract, or a typed refusal. + */ + status: async (params: SkillInstallationOperationRequest): Promise => + connection.sendRequest("skills.installations.status", params), + /** + * Requests cancellation of a known Skill installation operation before commit. Already-started durable work requires recovery instead of silent replay. + * + * @param params Existing-operation control. A new session selector is deliberately not accepted. + * + * @returns Skill installation management result with the honoured contract, or a typed refusal. + */ + cancel: async (params: SkillInstallationOperationRequest): Promise => + connection.sendRequest("skills.installations.cancel", params), + /** + * 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. + * + * @param params Persisted enablement update for one owned Skill installation. + * + * @returns Skill installation management result with the honoured contract, or a typed refusal. + */ + setEnabled: async (params: SkillSetEnabledRequest): Promise => + connection.sendRequest("skills.installations.setEnabled", params), + }, + /** + * 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. + * + * @param params Read-only preparation of one owned Skill removal under fresh selected-session authority. + * + * @returns Skill installation management result with the honoured contract, or a typed refusal. + */ + planUninstall: async (params: SkillPlanUninstallRequest): Promise => + connection.sendRequest("skills.planUninstall", params), + /** + * Consumes an owned Skill removal plan, requests explicit human consent through installations.confirm, refuses drift, and removes the exact owned files through quarantine. + * + * @param params One-use application of the exact retained Skill removal plan. + * + * @returns Skill installation result with the honoured contract, or a typed request/negotiation refusal. + */ + applyUninstall: async (params: SkillApplyUninstallRequest): Promise => + connection.sendRequest("skills.applyUninstall", params), + /** @experimental */ + config: { + /** + * Replaces the global list of disabled skills. + * + * @param params Skill names to mark as disabled in global configuration, replacing any previous list. + */ + setDisabledSkills: async (params: SkillsConfigSetDisabledSkillsRequest): Promise => + connection.sendRequest("skills.config.setDisabledSkills", params), + /** + * Atomically adds or removes one skill from the disabled list. + * + * @param params Adds or removes a single skill from the global disabled list, leaving every other entry untouched. + */ + setSkillDisabled: async (params: SkillsConfigSetSkillDisabledRequest): Promise => + connection.sendRequest("skills.config.setSkillDisabled", params), + }, + /** + * Discovers skills across global and project sources. + * + * @param params Optional project paths and additional skill directories to include in discovery. + * + * @returns Skills discovered across global and project sources. + */ + discover: async (params: SkillsDiscoverRequest): Promise => + connection.sendRequest("skills.discover", params), + /** + * 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. + * + * @param params Optional project paths to enumerate. + * + * @returns Canonical locations where skills can be created so the runtime will recognize them. + */ + getDiscoveryPaths: async (params: SkillsGetDiscoveryPathsRequest): Promise => + connection.sendRequest("skills.getDiscoveryPaths", params), + }, /** * 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. * @@ -27587,44 +32172,6 @@ export function createServerRpc(connection: MessageConnection) { }, }, /** @experimental */ - skills: { - /** @experimental */ - config: { - /** - * Replaces the global list of disabled skills. - * - * @param params Skill names to mark as disabled in global configuration, replacing any previous list. - */ - setDisabledSkills: async (params: SkillsConfigSetDisabledSkillsRequest): Promise => - connection.sendRequest("skills.config.setDisabledSkills", params), - /** - * Atomically adds or removes one skill from the disabled list. - * - * @param params Adds or removes a single skill from the global disabled list, leaving every other entry untouched. - */ - setSkillDisabled: async (params: SkillsConfigSetSkillDisabledRequest): Promise => - connection.sendRequest("skills.config.setSkillDisabled", params), - }, - /** - * Discovers skills across global and project sources. - * - * @param params Optional project paths and additional skill directories to include in discovery. - * - * @returns Skills discovered across global and project sources. - */ - discover: async (params: SkillsDiscoverRequest): Promise => - connection.sendRequest("skills.discover", params), - /** - * 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. - * - * @param params Optional project paths to enumerate. - * - * @returns Canonical locations where skills can be created so the runtime will recognize them. - */ - getDiscoveryPaths: async (params: SkillsGetDiscoveryPathsRequest): Promise => - connection.sendRequest("skills.getDiscoveryPaths", params), - }, - /** @experimental */ agents: { /** * Discovers custom agents across user, project, plugin, and remote sources. @@ -27681,42 +32228,69 @@ export function createServerRpc(connection: MessageConnection) { /** @experimental */ settings: { /** - * Drops this runtime process's in-memory user settings cache so the next settings read observes disk. - */ - reload: async (): Promise => - connection.sendRequest("user.settings.reload", {}), - /** - * Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. + * Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. * - * @returns Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + * @returns Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. */ get: async (): Promise => connection.sendRequest("user.settings.get", {}), /** - * Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. + * Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. * * @param params Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. - * - * @returns Outcome of writing user settings. */ - set: async (params: UserSettingsSetRequest): Promise => + set: async (params: UserSettingsSetRequest): Promise => connection.sendRequest("user.settings.set", params), }, }, /** @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: { @@ -28002,6 +32576,41 @@ export function createServerRpc(connection: MessageConnection) { spawn: async (params: AgentRegistrySpawnRequest): Promise => connection.sendRequest("agentRegistry.spawn", params), }, + /** @experimental */ + connectors: { + /** + * Returns feature availability. + * + * @returns Feature availability. + */ + getCapabilities: async (): Promise => + connection.sendRequest("connectors.getCapabilities", {}), + /** + * Returns eligible accounts. + * + * @returns Eligible accounts. + */ + getAccounts: async (): Promise => + connection.sendRequest("connectors.getAccounts", {}), + /** + * Lists entries for the selected account. + * + * @param params Selected account. + * + * @returns Entries for the selected account. + */ + list: async (params: ConnectorDiscoveryAccountRequest): Promise => + connection.sendRequest("connectors.list", params), + /** + * Refreshes entries for the selected account. + * + * @param params Selected account. + * + * @returns Entries for the selected account. + */ + refresh: async (params: ConnectorDiscoveryAccountRequest): Promise => + connection.sendRequest("connectors.refresh", params), + }, }; } @@ -28012,6 +32621,50 @@ export function createServerRpc(connection: MessageConnection) { */ export function createInternalServerRpc(connection: MessageConnection) { return { + /** @experimental */ + host: { + /** + * Requests app-owned materialization over the owning SDK participant. + * + * @param params One application-owned session handoff, requested by the supervised hosting participant. + * + * @returns The resident session the application has materialized on its own connection. + */ + createSession: async (params: HostCreateSessionRequest): Promise => + connection.sendRequest("host.createSession", params), + /** + * Releases app ownership retention after AHP detaches. + * + * @param params Ends one participation, not the application's session lifetime. + * + * @returns Empty acknowledgement for a completed host lifecycle operation. + */ + 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. + * + * @returns Normalized listener settings delivered only to the supervised hosting participant. + */ + getConfiguration: async (): Promise => + connection.sendRequest("host.getConfiguration", {}), + /** + * Reports a supervised hosting participant's bound AHP endpoint after its SDK handshake. + * + * @param params Readiness reported by the supervised hosting participant on its own SDK connection. + * + * @returns Empty acknowledgement for a completed host lifecycle operation. + */ + ready: async (params: HostReadyRequest): Promise => + connection.sendRequest("host.ready", params), + }, /** * 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. * @@ -28024,6 +32677,148 @@ export function createInternalServerRpc(connection: MessageConnection) { connect: async (params: ConnectRequest): Promise => connection.sendRequest("connect", params), /** @experimental */ + agents: { + /** + * Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + * + * @returns The agents this runtime ships, named so a consumer can tell them apart from authored ones. + */ + getBuiltins: async (): Promise => + connection.sendRequest("agents.getBuiltins", {}), + /** + * Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + * + * @param params The feature flags to evaluate shipped agents against. + * + * @returns The shipped agents available under the requested flags. + */ + getAvailableBuiltins: async (params: AgentsGetAvailableBuiltinsRequest): Promise => + connection.sendRequest("agents.getAvailableBuiltins", params), + /** + * Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + * + * @param params The shipped agent whose definition to load. + * + * @returns One shipped agent's definition. + */ + getBuiltinDefinition: async (params: AgentsGetBuiltinDefinitionRequest): Promise => + connection.sendRequest("agents.getBuiltinDefinition", params), + /** + * Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + * + * @param params The shipped agent whose listing entry to load. + * + * @returns One shipped agent, projected for a listing. + */ + getBuiltinListingDefinition: async (params: AgentsGetBuiltinListingDefinitionRequest): Promise => + connection.sendRequest("agents.getBuiltinListingDefinition", params), + /** + * Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + * + * @param params The models a custom agent asks for, and the models actually available. + * + * @returns The model to switch to, and the warning to show when the agent's preference could not be met. + */ + customAgentInitialModelDecision: async (params: AgentsCustomAgentInitialModelDecisionParams): Promise => + connection.sendRequest("agents.customAgentInitialModelDecision", params), + }, + /** @experimental */ + globalState: { + /** + * Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. + * + * @returns The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + */ + load: async (): Promise => + connection.sendRequest("globalState.load", {}), + /** + * Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. + * + * @param params Selects the configuration directory whose machine-wide state to read. + * + * @returns The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + */ + loadForConfigDir: async (params: GlobalStateLoadForConfigDirRequest): Promise => + connection.sendRequest("globalState.loadForConfigDir", params), + /** + * Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. + * + * @param params A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. + */ + writeKey: async (params: GlobalStateWriteKeyRequest): Promise => + connection.sendRequest("globalState.writeKey", params), + }, + /** @experimental */ + gitHubRepository: { + /** + * Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. + * + * @param params Working-tree path whose owning GitHub repository should be resolved. + * + * @returns The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. + */ + atPath: async (params: GitHubRepositoryAtPathRequest): Promise => + connection.sendRequest("gitHubRepository.atPath", params), + }, + /** @experimental */ + gitHubOwners: { + /** + * Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + * + * @returns A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + */ + nextRequestId: async (): Promise => + connection.sendRequest("gitHubOwners.nextRequestId", {}), + /** + * Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. + * + * @param params Credential to list owners under, and the request id that makes the listing cancellable. + * + * @returns Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. + */ + list: async (params: GitHubOwnersListRequest): Promise => + connection.sendRequest("gitHubOwners.list", params), + /** + * Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. + * + * @param params The owner listing to abandon. + * + * @returns Whether the id named a running owner listing. + */ + cancel: async (params: GitHubOwnersCancelRequest): Promise => + connection.sendRequest("gitHubOwners.cancel", params), + }, + /** @experimental */ + git: { + /** + * Reads the remote that the branch checked out in a working tree tracks, as `branch..remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + * + * @param params Working-tree path a git query applies to. + * + * @returns The remote the checked-out branch tracks. + */ + currentBranchRemote: async (params: GitCwdRequest): Promise => + connection.sendRequest("git.currentBranchRemote", params), + /** + * Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. + * + * @param params Working-tree path a git query applies to. + * + * @returns Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + */ + workingDirectoryContext: async (params: GitCwdRequest): Promise => + connection.sendRequest("git.workingDirectoryContext", params), + /** + * Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + * + * @param params Git working tree whose GitHub remotes should be listed. + * + * @returns The GitHub repositories a working tree's remotes point at. + */ + reposFromRemotes: async (params: GitReposFromRemotesRequest): Promise => + connection.sendRequest("git.reposFromRemotes", params), + }, + /** @experimental */ sessions: { /** * Reads lightweight persisted metadata for one local session without opening it. @@ -28068,6 +32863,33 @@ export function createInternalServerRpc(connection: MessageConnection) { */ delete: async (params: SessionsDeleteRequest): Promise => connection.sendRequest("sessions.delete", params), + /** + * Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + * + * @param params Identity, state location and starting context for a workspace record. + * + * @returns The workspace record that was written. + */ + createWorkspace: async (params: SessionsCreateWorkspaceRequest): Promise => + connection.sendRequest("sessions.createWorkspace", params), + /** + * Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + * + * @param params Where the session's state lives, as a root directory and the session ID under it. + * + * @returns The workspace record on disk, omitted when the session has none. + */ + loadWorkspace: async (params: SessionsLoadWorkspaceRequest): Promise => + connection.sendRequest("sessions.loadWorkspace", params), + /** + * Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + * + * @param params Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. + * + * @returns The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. + */ + updateWorkspaceFields: async (params: SessionsUpdateWorkspaceFieldsRequest): Promise => + connection.sendRequest("sessions.updateWorkspaceFields", params), /** * 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. * @@ -28091,6 +32913,55 @@ export function createInternalServerRpc(connection: MessageConnection) { /** 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. * @@ -28108,7 +32979,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. * @@ -28119,7 +32990,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: { /** @@ -28137,7 +33008,16 @@ 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. + * + * @param params Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. + * + * @returns Result of accepting a sandbox path grant. + */ + grantPathForRequest: async (params: SandboxGrantPathForRequestRequest): Promise => + connection.sendRequest("session.sandbox.grantPathForRequest", { ...params, sessionId }), }, /** * Aborts the current agent turn. @@ -28149,7 +33029,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. * @@ -28160,7 +33040,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. * @@ -28178,7 +33058,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: { /** @@ -28196,7 +33076,65 @@ 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: { + /** + * Enumerate a typed accounts collection: the signed-in accounts, or the providers offered for interactive login. + * + * @param params Enumerate request carrying the typed collection query. + * + * @returns The enumerated collection, keyed by the same selector as the query. + */ + enumerate: async (params: AccountsEnumerateRequest): Promise => + connection.sendRequest("session.accounts.enumerate", { ...params, sessionId }), + /** + * Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors. + * + * @param params Read request carrying the typed datum query. + * + * @returns The read result, keyed by the same selector as the query. + */ + get: async (params: AccountsGetRequest): Promise => + 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. + * + * @param params Mutation request carrying the typed write command. + * + * @returns Result of a non-interactive accounts mutation. + */ + set: async (params: AccountsSetRequest): Promise => + connection.sendRequest("session.accounts.set", { ...params, sessionId }), + /** @experimental */ + login: { + /** + * Begin an interactive login flow for a provider kind (dispatch is kind-only) and return its opaque flow id and first step. + * + * @param params Begin an interactive login flow for a provider kind. Dispatch is kind-only. + * + * @returns A started login flow: its opaque id and first step. + */ + begin: async (params: AuthLoginBeginRequest): Promise => + 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. + * + * @param params Advance an in-flight login flow, optionally fulfilling an input-required step. + * + * @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", { ...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", { ...params, sessionId }), + }, }, /** @experimental */ debug: { @@ -28208,7 +33146,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: { @@ -28234,14 +33172,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: { /** @@ -28252,121 +33190,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 }), - }, - }, - /** @experimental */ - factory: { - /** - * Runs a registered factory by name at the top level. - * - * @param params Parameters for invoking a registered factory. - * - * @returns Complete current or terminal factory run envelope. - */ - run: async (params: FactoryRunRequest): Promise => - connection.sendRequest("session.factory.run", { sessionId, ...params }), - /** - * Resumes a factory run using its persisted name, arguments, journal, and accounting. - * - * @param params Parameters for resuming a factory run from its persisted identity. - * - * @returns Resolved persisted factory identity and resumed run envelope. - */ - resume: async (params: FactoryResumeRequest): Promise => - connection.sendRequest("session.factory.resume", { sessionId, ...params }), - /** - * Gets the current or settled envelope for a factory run. - * - * @param params Parameters for retrieving a factory run. - * - * @returns Complete current or terminal factory run envelope. - */ - getRun: async (params: FactoryGetRunRequest): Promise => - connection.sendRequest("session.factory.getRun", { sessionId, ...params }), - /** - * Lists durable factory runs for this session in creation order. - * - * @param params Parameters for paging factory runs. - * - * @returns A page of factory runs in durable creation order. - */ - listRuns: async (params: FactoryListRunsRequest): Promise => - connection.sendRequest("session.factory.listRuns", { sessionId, ...params }), - /** - * Gets durable and live observability detail for one factory run. - * - * @param params Parameters for retrieving a factory run. - * - * @returns Full factory run observability detail. - */ - getRunDetail: async (params: FactoryGetRunRequest): Promise => - connection.sendRequest("session.factory.getRunDetail", { sessionId, ...params }), - /** - * Pages durable progress for one factory run. - * - * @param params Parameters for paging factory progress. - * - * @returns A bidirectional page of factory progress. - */ - getRunProgress: async (params: FactoryGetRunProgressRequest): Promise => - connection.sendRequest("session.factory.getRunProgress", { sessionId, ...params }), - /** - * Requests cancellation of a factory run and returns its run envelope. - * - * @param params Parameters for cancelling a factory run. - * - * @returns Complete current or terminal factory run envelope. - */ - cancel: async (params: FactoryCancelRequest): Promise => - connection.sendRequest("session.factory.cancel", { sessionId, ...params }), - /** - * Pauses a running factory and returns its settled run envelope. - * - * @param params Parameters for pausing a running factory. - * - * @returns Complete current or terminal factory run envelope. - */ - pause: async (params: FactoryPauseRequest): Promise => - connection.sendRequest("session.factory.pause", { sessionId, ...params }), - /** - * Records a batch of ordered factory progress lines. - * - * @param params Parameters for recording factory progress. - * - * @returns Acknowledgement that a factory request was accepted. - */ - log: async (params: FactoryLogRequest): Promise => - connection.sendRequest("session.factory.log", { sessionId, ...params }), - /** - * Runs one factory-scoped subagent and returns its result. - * - * @param params Parameters for one factory-scoped subagent call. - * - * @returns Result of one factory-scoped subagent call. - */ - agent: async (params: FactoryAgentRequest): Promise => - connection.sendRequest("session.factory.agent", { sessionId, ...params }), - /** @experimental */ - journal: { - /** - * Reads a memoized factory journal entry. - * - * @param params Parameters for reading a factory journal entry. - * - * @returns Result of reading a factory journal entry. - */ - get: async (params: FactoryJournalGetRequest): Promise => - connection.sendRequest("session.factory.journal.get", { sessionId, ...params }), - /** - * Stores a memoized factory journal entry. - * - * @param params Parameters for storing a factory journal entry. - * - * @returns Acknowledgement that a factory request was accepted. - */ - put: async (params: FactoryJournalPutRequest): Promise => - connection.sendRequest("session.factory.journal.put", { sessionId, ...params }), + connection.sendRequest("session.canvas.action.invoke", { ...params, sessionId }), }, }, /** @experimental */ @@ -28379,7 +33203,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. * @@ -28388,7 +33212,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. * @@ -28397,7 +33221,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. * @@ -28406,7 +33230,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. * @@ -28415,7 +33239,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. * @@ -28424,7 +33248,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. * @@ -28433,7 +33257,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. * @@ -28442,7 +33266,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. * @@ -28451,7 +33275,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. * @@ -28460,7 +33284,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: { /** @@ -28471,7 +33295,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. * @@ -28480,7 +33304,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 */ @@ -28500,7 +33324,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`. * @@ -28509,7 +33333,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. * @@ -28518,7 +33342,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. * @@ -28527,7 +33351,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. * @@ -28536,7 +33360,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: { @@ -28555,7 +33379,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: { @@ -28572,7 +33396,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. * @@ -28581,7 +33405,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: { @@ -28598,7 +33422,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. */ @@ -28636,7 +33460,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. * @@ -28645,7 +33469,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. * @@ -28661,14 +33485,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. * @@ -28677,28 +33501,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. * @@ -28714,7 +33538,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. * @@ -28723,7 +33547,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. * @@ -28732,7 +33556,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. * @@ -28748,7 +33572,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. * @@ -28771,7 +33595,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`. * @@ -28780,7 +33604,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: { @@ -28809,7 +33633,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: { @@ -28821,19 +33645,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 */ @@ -28846,7 +33672,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: { @@ -28858,14 +33684,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. * @@ -28881,7 +33707,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. */ @@ -28905,7 +33731,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. * @@ -28921,7 +33747,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. * @@ -28930,7 +33756,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. * @@ -28953,7 +33779,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. * @@ -28969,7 +33795,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. * @@ -28985,7 +33811,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. * @@ -28994,7 +33820,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. * @@ -29003,7 +33829,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: { @@ -29027,14 +33853,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. * @@ -29051,12 +33877,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. * @@ -29065,21 +33898,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. */ @@ -29100,7 +33933,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. * @@ -29109,7 +33942,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). * @@ -29118,7 +33951,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. * @@ -29132,21 +33965,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. * @@ -29155,7 +33988,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: { /** @@ -29166,23 +33999,39 @@ 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 }), /** - * Starts OAuth authentication for a remote MCP server. + * 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. * - * @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 Effect-free preparation bound to the existing local session, requester and installation, with frozen options. + * + * @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", { ...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 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. * @@ -29191,7 +34040,16 @@ 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. + * + * @param params Targets only the original prepared/applying owned login on this exact session requester. + * + * @returns Honest terminal cancellation result; persistence or recovery failures remain RPC errors. + */ + cancelLogin: async (params: SessionMcpOauthCancelLoginRequest): Promise => + connection.sendRequest("session.mcp.oauth.cancelLogin", { ...params, sessionId }), /** * Responds to a pending MCP OAuth authorization request by its request id. * @@ -29200,7 +34058,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: { @@ -29212,7 +34070,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: { @@ -29224,7 +34082,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"`. * @@ -29233,7 +34091,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`. * @@ -29242,14 +34100,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. * @@ -29265,7 +34123,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: { @@ -29277,7 +34135,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`. * @@ -29286,7 +34144,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`. * @@ -29295,7 +34153,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 */ @@ -29308,7 +34187,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. * @@ -29317,7 +34196,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: { @@ -29328,6 +34207,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. * @@ -29343,7 +34229,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. * @@ -29352,7 +34238,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. * @@ -29361,7 +34247,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. * @@ -29370,7 +34256,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. * @@ -29379,7 +34265,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. * @@ -29388,7 +34274,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. * @@ -29397,14 +34283,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 }), @@ -29426,14 +34312,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. * @@ -29442,21 +34328,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: { /** @@ -29474,7 +34360,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. * @@ -29483,7 +34369,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. * @@ -29492,7 +34378,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. * @@ -29501,7 +34387,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. @@ -29509,7 +34395,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: { @@ -29521,7 +34407,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. * @@ -29530,7 +34416,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. * @@ -29539,7 +34425,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: { @@ -29551,7 +34446,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: { @@ -29561,7 +34456,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: { @@ -29578,14 +34473,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. */ @@ -29597,7 +34492,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: { @@ -29609,7 +34504,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. * @@ -29618,7 +34513,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. * @@ -29627,7 +34522,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. * @@ -29636,7 +34531,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. * @@ -29659,7 +34554,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. * @@ -29668,7 +34563,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: { @@ -29680,7 +34575,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. * @@ -29689,7 +34584,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. * @@ -29698,7 +34593,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. * @@ -29707,7 +34602,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. * @@ -29716,7 +34611,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. * @@ -29725,7 +34620,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: { @@ -29742,7 +34637,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: { @@ -29754,7 +34649,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. * @@ -29763,7 +34658,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. * @@ -29772,7 +34667,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. * @@ -29781,7 +34676,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. * @@ -29790,7 +34685,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. * @@ -29799,7 +34694,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. * @@ -29808,7 +34703,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. * @@ -29817,7 +34712,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. * @@ -29833,7 +34728,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: { @@ -29845,7 +34740,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. * @@ -29854,7 +34749,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. * @@ -29870,7 +34765,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. * @@ -29879,7 +34774,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. * @@ -29895,7 +34790,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. * @@ -29904,7 +34799,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. * @@ -29913,7 +34808,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. * @@ -29922,7 +34817,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: { /** @@ -29940,7 +34835,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. * @@ -29949,7 +34844,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. * @@ -29958,7 +34853,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. * @@ -29967,7 +34862,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: { @@ -29979,7 +34874,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. * @@ -29988,7 +34883,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. * @@ -29997,7 +34892,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: { @@ -30009,7 +34904,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. * @@ -30018,7 +34913,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: { @@ -30030,7 +34925,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 }), }, }, /** @@ -30043,7 +34938,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: { /** @@ -30068,7 +34963,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. * @@ -30091,7 +34986,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. * @@ -30107,7 +35002,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. * @@ -30116,7 +35011,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. * @@ -30125,7 +35020,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. * @@ -30134,7 +35029,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: { @@ -30146,19 +35041,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. * @@ -30167,7 +35062,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. * @@ -30176,7 +35071,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. * @@ -30185,7 +35080,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: { @@ -30197,7 +35092,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. * @@ -30206,7 +35101,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. * @@ -30222,7 +35117,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. * @@ -30231,7 +35126,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. * @@ -30261,7 +35156,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: { @@ -30280,7 +35175,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. * @@ -30289,7 +35184,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. * @@ -30298,7 +35193,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. * @@ -30307,7 +35202,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. * @@ -30316,7 +35211,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. * @@ -30325,7 +35220,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. * @@ -30334,14 +35229,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. * @@ -30350,7 +35245,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). * @@ -30374,7 +35269,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. * @@ -30390,7 +35285,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. * @@ -30399,7 +35294,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: { @@ -30421,7 +35316,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: { @@ -30433,7 +35328,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. */ @@ -30447,7 +35342,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: { @@ -30466,7 +35361,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: { @@ -30485,7 +35380,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 }), }, }; } @@ -30505,7 +35400,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: { /** @@ -30513,7 +35408,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. @@ -30527,7 +35422,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. @@ -30537,14 +35432,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. * @@ -30560,7 +35455,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. * @@ -30579,45 +35474,17 @@ 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 */ - factory: { - /** - * Internal tool-originated factory invocation. - * - * @param params Internal parameters for invoking a registered factory from a tool. - * - * @returns Complete current or terminal factory run envelope. - */ - runFromTool: async (params: FactoryToolRunRequest): Promise => - connection.sendRequest("session.factory.runFromTool", { sessionId, ...params }), - /** - * Internal tool-originated factory resume. - * - * @param params Internal parameters for resuming a factory run from a tool. - * - * @returns Resolved persisted factory identity and resumed run envelope. - */ - resumeFromTool: async (params: FactoryToolResumeRequest): Promise => - connection.sendRequest("session.factory.resumeFromTool", { sessionId, ...params }), - /** - * Atomically pauses an owned factory attempt at a durable checkpoint. - * - * @param params Parameters for an owned durable pause checkpoint. - */ - pauseAtCheckpoint: async (params: FactoryPauseCheckpointRequest): Promise => - connection.sendRequest("session.factory.pauseAtCheckpoint", { sessionId, ...params }), - }, - /** @experimental */ workflow: { /** * Internal tool-originated dynamic workflow invocation. @@ -30627,7 +35494,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. * @@ -30636,14 +35503,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: { @@ -30655,10 +35522,17 @@ 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: { + /** + * Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + * + * @param params Records which IDE the host is connected to, or clears it. + */ + setConnectedIdeInfo: async (params: SessionMcpSetConnectedIdeInfoParams): Promise => + connection.sendRequest("session.mcp.setConnectedIdeInfo", { ...params, sessionId }), /** * Reloads MCP server connections for the session with an explicit host-provided configuration. * @@ -30667,7 +35541,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. * @@ -30676,21 +35550,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: { @@ -30702,7 +35576,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. * @@ -30721,7 +35595,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: { @@ -30740,7 +35644,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: { @@ -30766,7 +35670,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. * @@ -30775,14 +35679,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. * @@ -30791,7 +35695,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. * @@ -30827,7 +35731,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. * @@ -30836,7 +35740,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. * @@ -30845,7 +35749,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. * @@ -30854,7 +35758,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. * @@ -30863,7 +35767,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 }), }, }; } @@ -30881,27 +35785,6 @@ export interface ProviderTokenHandler { getToken(params: ProviderTokenAcquireRequest): Promise; } -/** Handler for `factory` client session API methods. */ -/** @experimental */ -export interface FactoryHandler { - /** - * Asks the owning extension connection to execute a registered factory closure. - * - * @param params Parameters sent to the owning extension to execute a factory closure. - * - * @returns Result returned by an extension factory closure. - */ - execute(params: FactoryExecuteRequest): Promise; - /** - * Asks the owning extension connection to abort a running factory cooperatively. - * - * @param params Parameters for cooperatively aborting a factory body. - * - * @returns Acknowledgement that a factory request was accepted. - */ - abort(params: FactoryAbortRequest): Promise; -} - /** Handler for `workflow` client session API methods. */ /** @experimental */ export interface WorkflowHandler { @@ -30947,6 +35830,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. * @@ -30955,6 +35846,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. * @@ -31075,7 +35974,6 @@ export interface CanvasHandler { /** All client session API handler groups. */ export interface ClientSessionApiHandlers { providerToken?: ProviderTokenHandler; - factory?: FactoryHandler; workflow?: WorkflowHandler; tasks?: TasksHandler; sessionFs?: SessionFsHandler; @@ -31097,16 +35995,6 @@ export function registerClientSessionApiHandlers( if (!handler) throw new Error(`No providerToken handler registered for session: ${params.sessionId}`); return handler.getToken(params); }); - connection.onRequest("factory.execute", async (params: FactoryExecuteRequest) => { - const handler = getHandlers(params.sessionId).factory; - if (!handler) throw new Error(`No factory handler registered for session: ${params.sessionId}`); - return handler.execute(params); - }); - connection.onRequest("factory.abort", async (params: FactoryAbortRequest) => { - const handler = getHandlers(params.sessionId).factory; - if (!handler) throw new Error(`No factory handler registered for session: ${params.sessionId}`); - return handler.abort(params); - }); connection.onRequest("workflow.execute", async (params: WorkflowExecuteRequest) => { const handler = getHandlers(params.sessionId).workflow; if (!handler) throw new Error(`No workflow handler registered for session: ${params.sessionId}`); @@ -31127,11 +36015,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}`); @@ -31204,6 +36102,17 @@ export function registerClientSessionApiHandlers( }); } +/** Handler for `host` client global API methods. */ +/** @experimental */ +export interface HostHandler { + /** + * Reports termination of a connection-owned host listener. + * + * @param params Reports a supervised listener's hosting-task termination and cleanup outcome. + */ + exited(params: HostExitedRequest): Promise; +} + /** Handler for `extensionLaunchProvider` client global API methods. */ /** @experimental */ export interface ExtensionLaunchProviderHandler { @@ -31262,12 +36171,27 @@ export interface GitHubTokenHandler { getToken(params: GitHubTokenAcquireRequest): Promise; } +/** Handler for `installations` client global API methods. */ +/** @experimental */ +export interface InstallationsHandler { + /** + * Requests a fresh explicit human decision for one sealed installation operation on its original connection. Present the complete typed review, return the original challenge and fingerprint, and never infer approval. The expiresAt deadline, connection closure or standard JSON-RPC $/cancelRequest retires the request; late replies grant no authority. + * + * @param params One connection-owned, expiring request for a trusted host's explicit user decision. + * + * @returns A response is meaningful only on the connection and request that issued its challenge. + */ + confirm(params: InstallationsConfirmRequest): Promise; +} + /** All client global API handler groups. */ export interface ClientGlobalApiHandlers { + host?: HostHandler; extensionLaunchProvider?: ExtensionLaunchProviderHandler; llmInference?: LlmInferenceHandler; gitHubTelemetry?: GitHubTelemetryHandler; gitHubToken?: GitHubTokenHandler; + installations?: InstallationsHandler; } /** @@ -31281,6 +36205,11 @@ export function registerClientGlobalApiHandlers( connection: MessageConnection, handlers: ClientGlobalApiHandlers, ): void { + connection.onNotification("host.exited", async (params: HostExitedRequest) => { + const handler = handlers.host; + if (!handler) return; + await handler.exited(params); + }); connection.onRequest("extensionLaunchProvider.resolve", async (params: ExtensionLaunchProviderResolveRequest) => { const handler = handlers.extensionLaunchProvider; if (!handler) throw new Error("No extensionLaunchProvider client-global handler registered"); @@ -31306,4 +36235,9 @@ export function registerClientGlobalApiHandlers( if (!handler) throw new Error("No gitHubToken client-global handler registered"); return handler.getToken(params); }); + connection.onRequest("installations.confirm", async (params: InstallationsConfirmRequest) => { + const handler = handlers.installations; + if (!handler) throw new Error("No installations client-global handler registered"); + return handler.confirm(params); + }); } diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 3b484f9e8c..733f2b2c43 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,11 +129,12 @@ export type SessionEvent = | CapabilitiesChangedEvent | ExitPlanModeRequestedEvent | ExitPlanModeCompletedEvent + | HumanResponseRecordedEvent | ToolsUpdatedEvent | BackgroundTasksChangedEvent - | FactoryRunUpdatedEvent - | FactoryRunStartedEvent - | FactoryRunSettledEvent + | WorkflowRunUpdatedEvent + | WorkflowRunStartedEvent + | WorkflowRunSettledEvent | SkillsLoadedEvent | CustomAgentsUpdatedEvent | McpServersLoadedEvent @@ -443,7 +445,9 @@ export type ModelChangeSource = /** The user selected the promoted model from the changeboarding card or its keyboard shortcut. */ | "changeboarding_shortcut" /** An SDK or RPC caller selected the model. */ - | "sdk"; + | "sdk" + /** The user accepted a CAPI-issued Auto tier recommendation. */ + | "auto_tier_recommendation"; /** * Why the session no longer has an explicitly selected model. */ @@ -910,7 +914,7 @@ export type AbortReason = /** Autopilot stopped the run because the active objective reached its user-set --max-ai-credits limit. */ | "autopilot_credit_limit"; /** - * Configuration source: user, workspace, plugin, builtin, or managed + * Configuration source: user, workspace, plugin, builtin, managed, or account */ export type McpServerSource = /** Server configured in the user's global MCP configuration. */ @@ -922,7 +926,9 @@ export type McpServerSource = /** Server bundled with the runtime. */ | "builtin" /** Server supplied by a trusted host-managed catalog. */ - | "managed"; + | "managed" + /** Server contributed by a signed-in account; enablement and organization policy still apply. */ + | "account"; /** * Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) */ @@ -943,6 +949,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 */ @@ -1069,7 +1097,7 @@ export type SystemNotification = | SystemNotificationShellCompleted | SystemNotificationShellDetachedCompleted | SystemNotificationInstructionDiscovered - | SystemNotificationFactoryCompleted + | SystemNotificationWorkflowCompleted | SystemNotificationUnclassified; /** * Whether the agent completed successfully or failed @@ -1080,12 +1108,12 @@ export type SystemNotificationAgentCompletedStatus = /** The agent failed. */ | "failed"; /** - * Durable metadata describing who initiated a factory pause. + * Durable metadata describing who initiated a workflow pause. */ -export type SystemNotificationFactoryPauseInfo = +export type SystemNotificationWorkflowPauseInfo = | { /** - * Factory pause initiator discriminator. + * Workflow pause initiator discriminator. */ type: "user"; } @@ -1095,23 +1123,23 @@ export type SystemNotificationFactoryPauseInfo = */ key: string; /** - * Factory pause initiator discriminator. + * Workflow pause initiator discriminator. */ type: "checkpoint"; }; /** - * Terminal status reached by a factory execution attempt. + * Terminal status reached by a workflow execution attempt. */ -export type SystemNotificationFactoryCompletedStatus = - /** The factory completed successfully. */ +export type SystemNotificationWorkflowCompletedStatus = + /** The workflow completed successfully. */ | "completed" - /** The factory was halted. */ + /** The workflow was halted. */ | "halted" - /** The factory attempt paused intentionally. */ + /** The workflow attempt paused intentionally. */ | "paused" - /** The factory was cancelled. */ + /** The workflow was cancelled. */ | "cancelled" - /** The factory failed. */ + /** The workflow failed. */ | "error"; /** * Details of the permission being requested @@ -1126,9 +1154,17 @@ export type PermissionRequest = | PermissionRequestCustomTool | PermissionRequestHook | PermissionRequestExtensionManagement - | PermissionRequestFactory + | PermissionRequestWorkflow | PermissionRequestExtensionPermissionAccess | PermissionRequestExtensionEnvAccess; +/** + * Access a sandbox path grant confers + */ +export type PermissionSandboxPathGrantAccess = + /** Read access: the path is added to readonlyPaths. */ + | "read" + /** Read and write access: the path is added to readwritePaths. */ + | "readWrite"; /** * Advisory recommendation the runtime attaches to a permission request whose origin it can vouch for by construction. Unlike the auto-approval judge this does not depend on auto mode and does not evaluate what the tool call does; its absence simply means the runtime has no opinion and the request follows the host's normal approval flow. */ @@ -1189,12 +1225,12 @@ export type PermissionRequestMemoryScope = /** Store the memory for the current user. */ | "user"; /** - * Operation gated by a factory permission request. + * Operation gated by a workflow permission request. */ -export type FactoryPermissionOperation = - /** Running a registered factory, which spends subagents, active time, and AI credits under the approved limits. */ +export type WorkflowPermissionOperation = + /** Running a registered workflow, which spends subagents, active time, and AI credits under the approved limits. */ | "run" - /** Authoring a factory, which writes JavaScript into a session-scoped extension and loads it. */ + /** Authoring a workflow, which writes JavaScript into a session-scoped extension and loads it. */ | "author"; /** * Derived user-facing permission prompt details for UI consumers @@ -1210,7 +1246,7 @@ export type PermissionPromptRequest = | PermissionPromptRequestPath | PermissionPromptRequestHook | PermissionPromptRequestExtensionManagement - | PermissionPromptRequestFactory + | PermissionPromptRequestWorkflow | PermissionPromptRequestExtensionPermissionAccess | PermissionPromptRequestExtensionEnvAccess; /** @@ -1235,13 +1271,14 @@ export type PermissionDecisionSource = | "host_policy" /** The host denied the request because no interactive user response was available. */ | "unattended_fallback" - /** A live authorization record from an earlier human decision in this session contained the proposal, so it ran without another prompt. This is not a new human decision and never mints authority of its own. */ + /** Historical compatibility value for sessions created while authorization carry-forward was executable. Current runtimes do not produce this source. */ | "authorization_carry_forward"; /** * The result of the permission request */ export type PermissionResult = | PermissionApproved + | PermissionApprovedReadOnlyForSession | PermissionApprovedForSession | PermissionApprovedForLocation | PermissionCancelled @@ -1261,17 +1298,17 @@ export type UserToolSessionApproval = | UserToolSessionApprovalMemory | UserToolSessionApprovalCustomTool | UserToolSessionApprovalExtensionManagement - | UserToolSessionApprovalFactory + | UserToolSessionApprovalWorkflow | UserToolSessionApprovalExtensionPermissionAccess | UserToolSessionApprovalExtensionEnvAccess; /** - * Which direction a message-backed authorization claim moves authority in. + * Direction stored in a historical extractor claim. Current runtimes do not apply it. */ /** @experimental */ export type PermissionMessageAuthorizationPolarity = - /** The human's words authorized an effect. */ + /** Historical claim recorded as a grant. */ | "grant" - /** The human's words refused an effect. */ + /** Historical claim recorded as a denial. */ | "denial"; /** * Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. @@ -1439,10 +1476,99 @@ export type ExitPlanModeAction = /** Exit plan mode and continue with parallel autonomous workers. */ | "autopilot_fleet"; /** - * Terminal status a factory run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. + * Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. */ -export type FactoryRunSettledStatus = - /** The factory body resolved and its result was committed. */ +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. + */ +export type WorkflowRunSettledStatus = + /** The workflow body resolved and its result was committed. */ | "completed" /** The run was stopped by a limit, an approval refusal or another policy decision. */ | "halted" @@ -1494,7 +1620,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"; @@ -1584,6 +1710,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 @@ -1736,6 +1866,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 @@ -2338,6 +2472,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; @@ -3464,11 +3602,17 @@ export interface CompactionCompleteEvent { */ export interface CompactionCompleteData { /** - * Authoritative active-factory reminder appended to the compacted context + * Legacy active-workflow reminder retained for replay compatibility * * @internal */ activeFactorySummary?: string; + /** + * Authoritative active-workflow reminder appended to the compacted context + * + * @internal + */ + activeWorkflowSummary?: string; /** * Canonical model identifier used for model-specific behavior when replaying compaction */ @@ -4005,6 +4149,12 @@ export interface FusionResolvedData { * Version of the validated HydraFusion event contract. */ contractVersion: number; + /** + * 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. + * + * @experimental + */ + critics?: FusionCritic[]; /** * Concrete model used when the planned primary model cannot execute. */ @@ -4024,6 +4174,12 @@ export interface FusionResolvedData { * @experimental */ hint?: string; + /** + * Concrete model selected for Cascade escalation-gate calls, when required. + * + * @experimental + */ + judgeModel?: string; /** * Version of the executable model universe used for selection. */ @@ -4051,6 +4207,12 @@ export interface FusionResolvedData { * Concrete model selected for the primary solver phase. */ primaryModel: string; + /** + * Concrete model selected for Cascade repair, when required. + * + * @experimental + */ + repairModel?: string; /** * Router implementation that supplied the plan. */ @@ -4073,7 +4235,7 @@ export interface FusionResolvedData { ruleName?: string; scores?: FusionScores; /** - * Concrete model selected for the review or judge phase, when required. + * Concrete model selected for Critique review, or the legacy Cascade judge/repair model when role-specific fields are absent. */ secondaryModel: string | null; /** @@ -4085,6 +4247,27 @@ export interface FusionResolvedData { */ turnId: string; } +/** @experimental */ +export interface FusionCritic { + /** + * Concrete model selected for this critic. + * + * @experimental + */ + model: string; + /** + * Unique execution phase identifier for this critic. + * + * @experimental + */ + phaseId: string; + /** + * Explicit reasoning effort selected for this critic, if supplied. + * + * @experimental + */ + reasoningEffort?: string; +} /** * Durable server recommendation for subsequent HydraFusion turns. */ @@ -4863,6 +5046,10 @@ export interface AssistantTurnStartData { * Model identifier used for this turn, when known */ model?: string; + /** + * Parent task tool call ID when this turn belongs to a sub-agent + */ + parentToolCallId?: string; /** * Identifier for this turn within the agentic loop, typically a stringified turn number */ @@ -4958,6 +5145,12 @@ export interface FusionPhaseStartedData { */ phaseId: string; phaseKind: FusionPhaseKind; + /** + * Explicit reasoning effort selected for this phase, if supplied. + * + * @experimental + */ + reasoningEffort?: string; /** * Semantic role assigned to the phase. */ @@ -5094,6 +5287,12 @@ export interface FusionPhaseCompletedData { * @internal */ projectionMode?: FusionProjectionMode; + /** + * Explicit reasoning effort selected for this phase, if supplied. + * + * @experimental + */ + reasoningEffort?: string; /** * Semantic role assigned to the completed phase. */ @@ -5219,6 +5418,12 @@ export interface FusionPhaseFailedData { * Stable machine-readable reason for the phase failure. */ reason: string; + /** + * Explicit reasoning effort selected for this phase, if supplied. + * + * @experimental + */ + reasoningEffort?: string; /** * Semantic role assigned to the failed phase. */ @@ -5775,6 +5980,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. */ @@ -5982,6 +6191,10 @@ export interface AssistantTurnEndData { * Model identifier used for this turn, when known */ model?: string; + /** + * Parent task tool call ID when this turn belongs to a sub-agent + */ + parentToolCallId?: string; /** * Identifier of the turn that has ended, matching the corresponding assistant.turn_start event */ @@ -6075,6 +6288,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 * @@ -6166,6 +6383,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 * @@ -6406,6 +6627,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 */ @@ -6457,6 +6682,14 @@ 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 + */ + parentToolCallId?: string; /** * GitHub request tracing ID (x-github-request-id header) for server-side log correlation */ @@ -6808,7 +7041,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 { /** @@ -6838,11 +7072,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; /** @@ -6850,6 +7085,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 */ @@ -6888,6 +7171,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 */ @@ -6928,6 +7217,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. * @@ -7006,6 +7301,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 */ @@ -7662,7 +7968,7 @@ export interface SubagentStartedData { */ executionMode?: string; /** - * Root id of the factory run that spawned this sub-agent, when it was spawned by one. + * Legacy root id of the workflow run that spawned this sub-agent. New consumers should use workflowRunId. */ factoryRunId?: string; /** @@ -7683,6 +7989,10 @@ export interface SubagentStartedData { * Tool call ID of the parent tool invocation that spawned this sub-agent */ toolCallId: string; + /** + * Root id of the workflow run that spawned this sub-agent, when it was spawned by one. + */ + workflowRunId?: string; } /** * Session event "subagent.configured". Resolved runtime configuration for a configured sub-agent @@ -8500,9 +8810,9 @@ export interface SystemNotificationInstructionDiscovered { type: "instruction_discovered"; } /** - * System notification metadata for a factory execution attempt that reached a terminal state. + * System notification metadata for a workflow execution attempt that reached a terminal state. */ -export interface SystemNotificationFactoryCompleted { +export interface SystemNotificationWorkflowCompleted { /** * Execution attempt that reached this terminal state. */ @@ -8519,32 +8829,32 @@ export interface SystemNotificationFactoryCompleted { * Accumulated active execution time in milliseconds. */ elapsedMs: number; - /** - * Persisted factory name. - */ - factoryName: string; /** * Machine-readable terminal failure details, when present. */ failure?: JsonValue; - pauseInfo?: SystemNotificationFactoryPauseInfo; + pauseInfo?: SystemNotificationWorkflowPauseInfo; /** * Bounded prompt-safe preview of the completed result. */ resultPreview?: string; /** - * Actionable run_factory resume guidance for a resource-limit failure. + * Actionable run_dynamic_workflow resume guidance for a resource-limit failure. */ retryGuidance?: string; /** - * Factory run identifier. + * Workflow run identifier. */ runId: string; - status: SystemNotificationFactoryCompletedStatus; + status: SystemNotificationWorkflowCompletedStatus; + /** + * Type discriminator. Always "workflow_completed". + */ + type: "workflow_completed"; /** - * Type discriminator. Always "factory_completed". + * Persisted workflow name. */ - type: "factory_completed"; + workflowName: string; } /** * System notification metadata from an external host that does not match a runtime-owned notification kind. @@ -8684,6 +8994,12 @@ export interface PermissionRequestShell { * @experimental */ resolvedWorkingDirectory?: string; + /** + * Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + * + * @experimental + */ + sandboxPathGrant?: PermissionSandboxPathGrant; /** * Tool call ID that triggered this permission request */ @@ -8728,6 +9044,25 @@ export interface PermissionRequestShellPossibleUrl { */ url: string; } +/** + * A sandbox filesystem policy edit that would let a blocked operation run inside the sandbox instead of outside it. Offered only on a sandbox escalation request whose denial adding this path lifts, and only when managed policy permits the grant. A host accepts it with session.sandbox.grantPathForRequest, which adds the path to the session's sandbox policy and re-runs the operation sandboxed; a host that persists sandbox settings may also save the path there. + */ +/** @experimental */ +export interface PermissionSandboxPathGrant { + access: PermissionSandboxPathGrantAccess; + /** + * The path the sandbox refused, present only when it differs from path. That happens when a write under a read-only folder moves the folder to the read-write paths, when a path that does not exist yet is granted through its nearest existing folder, because the OS sandbox cannot grant a path before it exists, and when either is spelled through a symlink, because a grant covers its path as written, so path is then the resolved location. Hosts should then name path in the offer, since the denial names this one. + */ + deniedPath?: string; + /** + * Absolute path to add to the sandbox filesystem policy + */ + path: string; + /** + * readonlyPaths entries the grant removes, exactly as written in the policy, because a read-only entry for the same location would otherwise keep the path read-only. A host that persists the path must remove these entries from its stored readonlyPaths too. + */ + removedReadonlyPaths?: string[]; +} /** * File write permission request */ @@ -8774,6 +9109,12 @@ export interface PermissionRequestWrite { * @experimental */ resolvedPath?: string; + /** + * Sandbox policy edit that would let the write run inside the sandbox. Only present when requestSandboxBypass is true. + * + * @experimental + */ + sandboxPathGrant?: PermissionSandboxPathGrant; /** * Tool call ID that triggered this permission request */ @@ -8813,6 +9154,12 @@ export interface PermissionRequestRead { * @experimental */ resolvedPath?: string; + /** + * Sandbox policy edit that would let the read run inside the sandbox. Only present when requestSandboxBypass is true. + * + * @experimental + */ + sandboxPathGrant?: PermissionSandboxPathGrant; /** * Tool call ID that triggered this permission request */ @@ -9008,6 +9355,8 @@ export interface PermissionApprovalEvaluation { | "action-too-long" /** The script path was not authorized for inspection. */ | "path-not-authorized" + /** A code source was excluded from review by content exclusion policy. */ + | "content-excluded" /** The script working directory was invalid. */ | "invalid-working-directory" /** The script snapshot could not be read. */ @@ -9032,6 +9381,18 @@ export interface PermissionApprovalEvaluation { | "unreviewable-script-invocation" /** The script argument binding could not be reviewed. */ | "argument-binding-unreviewable" + /** The shell command could not be analyzed for execution evidence. */ + | "unsupported-command-shape" + /** The shell command used a code source that cannot be bound for review. */ + | "unsupported-source" + /** The shell command used a code source computed at run time. */ + | "dynamic-source" + /** The shell command referenced more code sources than can be reviewed. */ + | "too-many-sources" + /** A code-bearing executable could not be inspected. */ + | "executable-unavailable" + /** A code-bearing executable exceeded the binding size limit. */ + | "executable-too-large" /** The script review metadata was malformed. */ | "malformed-script-action-review" /** The script snapshot manifest was malformed. */ @@ -9121,41 +9482,45 @@ export interface PermissionRequestExtensionManagement { toolCallId?: string; } /** - * Factory run or authoring permission request + * Workflow run or authoring permission request */ -export interface PermissionRequestFactory { +export interface PermissionRequestWorkflow { /** - * Canonical key used for scoped factory approvals + * Canonical key used for scoped workflow approvals */ approvalKey: string; /** - * Whether this factory is eligible for persistent approval + * Whether this workflow is eligible for persistent approval */ canPersistApproval: boolean; /** - * Factory-declared AI-credit limit before any run/resume caller override is applied. + * Workflow-declared AI-credit limit before any run/resume caller override is applied. */ declaredMaxAiCredits?: number; /** - * Factory-declared concurrent-subagent limit before any run/resume caller override is applied. + * Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. */ declaredMaxConcurrentSubagents?: number; /** - * Factory-declared total-subagent limit before any run/resume caller override is applied. + * Workflow-declared total-subagent limit before any run/resume caller override is applied. */ declaredMaxTotalSubagents?: number; /** - * Factory-declared active-time limit in seconds before any run/resume caller override is applied. + * Workflow-declared active-time limit in seconds before any run/resume caller override is applied. */ declaredTimeoutSeconds?: number; /** - * Factory description + * Workflow description */ description: string; /** * Permission kind discriminator */ - kind: "factory"; + kind: "workflow"; + /** + * Whether managed policy requires a human response and forbids host auto-approval + */ + managedApprovalRequired?: boolean; /** * Effective AI-credit limit; omitted means unlimited */ @@ -9169,14 +9534,14 @@ export interface PermissionRequestFactory { */ maxTotalSubagents?: number; /** - * Factory name + * Workflow name */ name: string; - operation: FactoryPermissionOperation; + operation: WorkflowPermissionOperation; /** - * Declared factory phases + * Declared workflow phases */ - phases: FactoryPermissionPhase[]; + phases: WorkflowPermissionPhase[]; /** * Effective active-time limit in seconds; omitted means unlimited */ @@ -9187,9 +9552,9 @@ export interface PermissionRequestFactory { toolCallId?: string; } /** - * A declared phase shown in a factory permission prompt. + * A declared phase shown in a workflow permission prompt. */ -export interface FactoryPermissionPhase { +export interface WorkflowPermissionPhase { /** * Optional phase detail */ @@ -9289,6 +9654,12 @@ export interface PermissionPromptRequestCommands { * True when the escalation is a permissive retry that keeps the sandbox and network policy attached while recording file and process accesses instead of blocking them. */ requestSandboxPermissive?: boolean; + /** + * Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + * + * @experimental + */ + sandboxPathGrant?: PermissionSandboxPathGrant; /** * Tool call ID that triggered this permission request */ @@ -9559,6 +9930,12 @@ export interface PermissionPromptRequestPath { * File paths that require explicit approval */ paths: string[]; + /** + * Canonical directory candidates that can be granted for file-tool read access in this logical session. Present only for read path prompts. + * + * @experimental + */ + readOnlyDirectories?: string[]; /** * Tool call ID that triggered this permission request */ @@ -9623,11 +10000,11 @@ export interface PermissionPromptRequestExtensionManagement { toolCallId?: string; } /** - * Factory run or authoring permission prompt + * Workflow run or authoring permission prompt */ -export interface PermissionPromptRequestFactory { +export interface PermissionPromptRequestWorkflow { /** - * Canonical key used for scoped factory approvals + * Canonical key used for scoped workflow approvals */ approvalKey: string; /** @@ -9637,33 +10014,33 @@ export interface PermissionPromptRequestFactory { */ assistedApproval?: PermissionAssistedApproval; /** - * Whether this factory is eligible for persistent approval + * Whether this workflow is eligible for persistent approval */ canPersistApproval: boolean; /** - * Factory-declared AI-credit limit before any run/resume caller override is applied. + * Workflow-declared AI-credit limit before any run/resume caller override is applied. */ declaredMaxAiCredits?: number; /** - * Factory-declared concurrent-subagent limit before any run/resume caller override is applied. + * Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. */ declaredMaxConcurrentSubagents?: number; /** - * Factory-declared total-subagent limit before any run/resume caller override is applied. + * Workflow-declared total-subagent limit before any run/resume caller override is applied. */ declaredMaxTotalSubagents?: number; /** - * Factory-declared active-time limit in seconds before any run/resume caller override is applied. + * Workflow-declared active-time limit in seconds before any run/resume caller override is applied. */ declaredTimeoutSeconds?: number; /** - * Factory description + * Workflow description */ description: string; /** * Prompt kind discriminator */ - kind: "factory"; + kind: "workflow"; /** * Whether managed policy requires a human response and forbids host auto-approval */ @@ -9681,14 +10058,14 @@ export interface PermissionPromptRequestFactory { */ maxTotalSubagents?: number; /** - * Factory name + * Workflow name */ name: string; - operation: FactoryPermissionOperation; + operation: WorkflowPermissionOperation; /** - * Declared factory phases + * Declared workflow phases */ - phases: FactoryPermissionPhase[]; + phases: WorkflowPermissionPhase[]; /** * Effective active-time limit in seconds; omitted means unlimited */ @@ -9822,6 +10199,21 @@ export interface PermissionApproved { */ managedApprovalHandled?: boolean; } +/** + * Permission response variant that approves a request and records file-tool read authority for specific directories in this logical session. + */ +export interface PermissionApprovedReadOnlyForSession { + /** + * Canonical directories covered by the session read-only grant + * + * @minItems 1 + */ + directories: [string, ...string[]]; + /** + * Approved with read-only directory authority for this session + */ + kind: "approved-read-only-for-session"; +} /** * Permission response variant that approves a request and remembers the provided approval for the rest of the session. */ @@ -9920,17 +10312,17 @@ export interface UserToolSessionApprovalExtensionManagement { operation?: string; } /** - * Session-scoped factory approval, optionally narrowed by approval key. + * Session-scoped workflow approval, optionally narrowed by approval key. */ -export interface UserToolSessionApprovalFactory { +export interface UserToolSessionApprovalWorkflow { /** - * Optional factory operation name or canonical approval key + * Optional workflow operation name or canonical approval key */ approvalKey?: string; /** - * Factory approval kind + * Workflow approval kind */ - kind: "factory"; + kind: "workflow"; } /** * Session-scoped tool-approval rule for an extension's permission-gated capability access, keyed by extension name. @@ -10082,7 +10474,7 @@ export interface PermissionDeniedByPermissionRequestHook { message?: string; } /** - * Session event "permission.carriedForward". Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. + * Session event "permission.carriedForward". Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions. */ /** @experimental */ export interface PermissionCarriedForwardEvent { @@ -10113,7 +10505,7 @@ export interface PermissionCarriedForwardEvent { type: "permission.carriedForward"; } /** - * Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. + * Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions. */ /** @experimental */ export interface PermissionCarriedForwardData { @@ -10143,7 +10535,7 @@ export interface PermissionCarriedForwardData { toolCallId: string; } /** - * Session event "permission.messageAuthorization". Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. + * Session event "permission.messageAuthorization". Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it. */ /** @experimental */ export interface PermissionMessageAuthorizationEvent { @@ -10174,7 +10566,7 @@ export interface PermissionMessageAuthorizationEvent { type: "permission.messageAuthorization"; } /** - * Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. + * Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it. */ /** @experimental */ export interface PermissionMessageAuthorizationData { @@ -10234,7 +10626,7 @@ export interface PermissionMessageAuthorizationData { world?: JsonValue; } /** - * Session event "permission.messageAuthorizationRead". Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction. + * Session event "permission.messageAuthorizationRead". Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it. */ /** @experimental */ export interface PermissionMessageAuthorizationReadEvent { @@ -10265,7 +10657,7 @@ export interface PermissionMessageAuthorizationReadEvent { type: "permission.messageAuthorizationRead"; } /** - * Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction. + * Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it. */ /** @experimental */ export interface PermissionMessageAuthorizationReadData { @@ -10283,7 +10675,7 @@ export interface PermissionMessageAuthorizationReadData { turnIndex: number; } /** - * Session event "permission.messageAuthorizationDegraded". Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. + * Session event "permission.messageAuthorizationDegraded". Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions. */ /** @experimental */ export interface PermissionMessageAuthorizationDegradedEvent { @@ -10314,7 +10706,7 @@ export interface PermissionMessageAuthorizationDegradedEvent { type: "permission.messageAuthorizationDegraded"; } /** - * Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. + * Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions. */ /** @experimental */ export interface PermissionMessageAuthorizationDegradedData { @@ -10326,7 +10718,7 @@ export interface PermissionMessageAuthorizationDegradedData { turnIndex: number; } /** - * Session event "permission.assentDetected". Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation. + * Session event "permission.assentDetected". Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow. */ /** @experimental */ export interface PermissionAssentDetectedEvent { @@ -10357,7 +10749,7 @@ export interface PermissionAssentDetectedEvent { type: "permission.assentDetected"; } /** - * Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation. + * Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow. */ /** @experimental */ export interface PermissionAssentDetectedData { @@ -10375,7 +10767,7 @@ export interface PermissionAssentDetectedData { turnIndex: number; } /** - * Session event "permission.contextualAuthorization". Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify. + * Session event "permission.contextualAuthorization". Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it. */ /** @experimental */ export interface PermissionContextualAuthorizationEvent { @@ -10406,7 +10798,7 @@ export interface PermissionContextualAuthorizationEvent { type: "permission.contextualAuthorization"; } /** - * Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify. + * Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it. */ /** @experimental */ export interface PermissionContextualAuthorizationData { @@ -11643,13 +12035,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 { @@ -11680,7 +12076,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 { @@ -11990,6 +12386,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. */ @@ -12064,15 +12505,15 @@ export interface BackgroundTasksChangedEvent { */ export interface BackgroundTasksChangedData {} /** - * Session event "factory.run_updated". Ephemeral invalidation signal for a changed factory run. + * Session event "workflow.run_updated". Ephemeral invalidation signal for a changed workflow run. */ /** @experimental */ -export interface FactoryRunUpdatedEvent { +export interface WorkflowRunUpdatedEvent { /** * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. */ agentId?: string; - data: FactoryRunUpdatedData; + data: WorkflowRunUpdatedData; /** * Always true for events that are transient and not persisted to the session event log on disk. */ @@ -12090,34 +12531,34 @@ export interface FactoryRunUpdatedEvent { */ timestamp: string; /** - * Type discriminator. Always "factory.run_updated". + * Type discriminator. Always "workflow.run_updated". */ - type: "factory.run_updated"; + type: "workflow.run_updated"; } /** - * Ephemeral invalidation signal for a changed factory run. + * Ephemeral invalidation signal for a changed workflow run. */ /** @experimental */ -export interface FactoryRunUpdatedData { +export interface WorkflowRunUpdatedData { /** * Monotonic revision now available for the run. */ revision: number; /** - * Factory run identifier. + * Workflow run identifier. */ runId: string; } /** - * Session event "factory.run_started". Ephemeral signal that a factory run attempt began executing. + * Session event "workflow.run_started". Ephemeral signal that a workflow run attempt began executing. */ /** @experimental */ -export interface FactoryRunStartedEvent { +export interface WorkflowRunStartedEvent { /** * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. */ agentId?: string; - data: FactoryRunStartedData; + data: WorkflowRunStartedData; /** * Always true for events that are transient and not persisted to the session event log on disk. */ @@ -12135,38 +12576,38 @@ export interface FactoryRunStartedEvent { */ timestamp: string; /** - * Type discriminator. Always "factory.run_started". + * Type discriminator. Always "workflow.run_started". */ - type: "factory.run_started"; + type: "workflow.run_started"; } /** - * Ephemeral signal that a factory run attempt began executing. + * Ephemeral signal that a workflow run attempt began executing. */ /** @experimental */ -export interface FactoryRunStartedData { +export interface WorkflowRunStartedData { /** * Attempt number this start committed; a resumed run increments it. */ attempt: number; /** - * Name of the factory this run executes. Low cardinality by construction. + * Identifier of the workflow run that started. */ - factoryName: string; + runId: string; /** - * Identifier of the factory run that started. + * Name of the workflow this run executes. Low cardinality by construction. */ - runId: string; + workflowName: string; } /** - * Session event "factory.run_settled". Ephemeral signal that a factory run reached a terminal status. + * Session event "workflow.run_settled". Ephemeral signal that a workflow run reached a terminal status. */ /** @experimental */ -export interface FactoryRunSettledEvent { +export interface WorkflowRunSettledEvent { /** * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. */ agentId?: string; - data: FactoryRunSettledData; + data: WorkflowRunSettledData; /** * Always true for events that are transient and not persisted to the session event log on disk. */ @@ -12184,15 +12625,15 @@ export interface FactoryRunSettledEvent { */ timestamp: string; /** - * Type discriminator. Always "factory.run_settled". + * Type discriminator. Always "workflow.run_settled". */ - type: "factory.run_settled"; + type: "workflow.run_settled"; } /** - * Ephemeral signal that a factory run reached a terminal status. + * Ephemeral signal that a workflow run reached a terminal status. */ /** @experimental */ -export interface FactoryRunSettledData { +export interface WorkflowRunSettledData { /** * AI credits this run consumed, in nano-AIU. */ @@ -12206,14 +12647,14 @@ export interface FactoryRunSettledData { */ elapsedMs: number; /** - * Typed failure class recorded on the run, when it failed with one (e.g. `factory_limit_reached`). + * Typed failure class recorded on the run, when it failed with one (e.g. `workflow_limit_reached`). */ failureType?: string; /** - * Identifier of the factory run that settled. + * Identifier of the workflow run that settled. */ runId: string; - status: FactoryRunSettledStatus; + status: WorkflowRunSettledStatus; } /** * Session event "session.skills_loaded". Payload of `session.skills_loaded` listing resolved skill metadata. @@ -12492,10 +12933,18 @@ export interface McpServerStatusChangedEvent { * Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. */ export interface McpServerStatusChangedData { + /** + * Runtime configuration provenance for a connected or failed server, or unknown when unavailable. Additional string values may be introduced. + */ + configSource?: string; /** * Error message if the server entered a failed state */ error?: string; + /** + * Runtime-produced classification for the final failed connection; unclassified means no classification was supplied. Additional string values may be introduced. + */ + errorClassification?: string; /** * Name of the MCP server whose status changed */ diff --git a/nodejs/src/host.ts b/nodejs/src/host.ts new file mode 100644 index 0000000000..28d64b89f6 --- /dev/null +++ b/nodejs/src/host.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { + HostExitedNotification, + HostStartResult, + HostPublishSessionResult, + HostLocalServerOptions, + HostGitHubEnvironmentOptions, +} from "./generated/rpc.js"; +import type { CopilotSession } from "./session.js"; +import type { SessionConfig, ResumeSessionConfig } from "./types.js"; + +/** Host-selected defaults for a fresh application-owned session. @experimental */ +export interface AhpSessionCreateRequest { + /** Pass these settings to createSession, adding your prompt, tools and callbacks. */ + config: Omit; + /** Aborted when participation ends, including timeout or owner disconnection. */ + signal: AbortSignal; +} + +/** Host-selected settings for resuming a durable application-owned AHP session. @experimental */ +export interface AhpSessionResumeRequest { + sessionId: string; + /** Pass to resumeSession, adding application tools, hooks and callbacks. */ + config: Omit; + /** Aborted when participation ends; late results are still released exactly once. */ + signal: AbortSignal; +} + +/** + * A listener-task exit report, or an owner disconnect that cannot acknowledge cleanup. + * `reason: "exited"` means the hosting task failed, not that the runtime process died. + * In-process hosts have no process exit code (`exitCode` is absent). + * @experimental + */ +export type AhpHostExit = HostExitedNotification; + +/** + * 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 { + /** 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; + /** + * Materialize a fresh session on this client. Preserve the supplied config + * (including its sessionId and workingDirectory); add your own prompt and tools. + * The callback and all tool/hook functions stay in the application. + */ + createSession?: (request: AhpSessionCreateRequest) => Promise; + /** + * Resume a durable session previously created by an application factory. + * Return the original object from this client's resumeSession, or an existing + * attached original without reconfiguring it. Published resident sessions + * attach directly and never invoke this callback. + */ + resumeSession?: (request: AhpSessionResumeRequest) => Promise; + /** + * Called once per handoff with the original returned object after AHP detaches + * or the handoff fails. The SDK never disconnects or destroys this object. + */ + onSessionReleased?: (session: CopilotSession) => void | Promise; +} + +/** + * A connection-owned AHP listener supervised by the runtime. + * + * The runtime hosts the listener in-process, without a companion executable. Disconnecting + * the owning client also stops the host; reconnecting does not reclaim it. + * Stopping a host does not delete its underlying sessions. + * Its durable AHP catalog is shared by successive hosts in the runtime's + * effective Copilot home; a concurrent host for that catalog is rejected. + * + * @experimental + */ +export class AhpHost { + readonly hostId: 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. */ + readonly pid: number | undefined; + + /** @internal */ + constructor( + info: HostStartResult, + private readonly disposeHost: () => Promise, + private readonly publishHostSession: ( + sessionId: string + ) => Promise + ) { + this.hostId = info.hostId; + this.url = info.url; + this.environmentId = info.environmentId; + this.token = info.token; + this.pid = info.pid; + } + + /** Ask the runtime to stop the listener and await its cleanup, on every call. */ + dispose(): Promise { + return this.disposeHost(); + } + + /** Publish an existing session attached to this client, until this listener stops. */ + publishSession(sessionId: string): Promise { + return this.publishHostSession(sessionId); + } + + async [Symbol.asyncDispose](): Promise { + await this.dispose(); + } +} diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index c46742d853..ef91cd9689 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -9,10 +9,17 @@ */ export { CopilotClient } from "./client.js"; +export type { HostLocalServerOptions, HostGitHubEnvironmentOptions } from "./generated/rpc.js"; +export { + AhpHost, + type AhpHostExit, + type AhpHostOptions, + type AhpSessionCreateRequest, + type AhpSessionResumeRequest, +} from "./host.js"; export { DisableBypassPermissionsModes, RuntimeConnection } from "./types.js"; export { BuiltInTools, ToolSet } from "./toolSet.js"; export { CopilotSession, type AssistantMessageEvent } from "./session.js"; -export { defineFactory, FactoryResumeError, isFactoryRunTerminal } from "./factory.js"; export { defineWorkflow, WorkflowResumeError, isWorkflowRunTerminal } from "./workflow.js"; export { Canvas, @@ -36,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 @@ -48,7 +56,7 @@ export { // `PermissionRequest` (re-exported below from `./types.js`), // `PermissionRequestedData`/`PermissionRequestedEvent` (also re-exported below // from `./types.js`), `AssistantMessageEvent` (re-exported above from -// `./session.js`), and `JsonValue` (re-exported below from `./factory.js`). +// `./session.js`), and `JsonValue` (re-exported below from `./workflow.js`). // Per the ECMAScript module spec, the explicit named re-exports // shadow the names arriving via `export type *`, so the hand-authored public API // surface for those six identifiers is preserved unchanged. @@ -61,6 +69,8 @@ export type { CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, + AuthIdentityMetadata, + AuthInfoType, ConnectorAccountRequest, ConnectorAvailability, ConnectorCapabilities, @@ -74,6 +84,8 @@ export type { ConnectorMcpStatus, ConnectorReconcileRequest, ConnectorRuntimeStatus, + ConnectorSessionAccount, + ConnectorSessionAccountResult, ConnectorStatus, AutoModeSwitchHandler, AutoModeSwitchRequest, @@ -81,12 +93,24 @@ export type { AgentStopHandler, AgentStopHookInput, AgentStopHookOutput, + SubagentStartHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHandler, + SubagentStopHookInput, + SubagentStopHookOutput, UserPromptTransformedHandler, UserPromptTransformedHookInput, UserPromptTransformedHookOutput, CopilotClientInfo, CopilotClientMode, CopilotClientOptions, + InstallationConfirmationHandler, + InstallationConfirmationRequest, + InstallationConfirmationResponse, + InstallationDecision, + InstallationReview, + McpInstallationReview, CopilotExpAssignmentResponse, StdioRuntimeConnection, InProcessRuntimeConnection, @@ -127,8 +151,6 @@ export type { LargeToolOutputConfig, MemoryConfiguration, UiInputOptions, - FactoryLimits, - FactoryMeta, MCPStdioServerConfig, MCPHTTPServerConfig, MCPServerConfig, @@ -165,10 +187,13 @@ export type { PermissionDecisionSurface, PermissionResponseCapability, ProviderConfig, + ProviderConfigModelProvider, ProviderModelConfig, ProviderTokenArgs, RemoteSessionMode, + SandboxConfigSource, ResumeSessionConfig, + TranscriptRecovery, SectionOverride, SectionOverrideAction, SectionTransformFn, @@ -201,6 +226,9 @@ export type { SessionFsSqliteProvider, SessionFsSqliteStatement, SessionFsSqliteTransactionErrorClass, + SkillProvider, + SkillProviderCallOptions, + SkillProviderDescriptor, CopilotRequestContext, SystemMessageAppendConfig, SystemMessageConfig, @@ -222,32 +250,7 @@ export type { ZodSchema, } from "./types.js"; export type { - RunOptions, - ResumeOptions, - FactoryLimitOverrides, - FactoryResumeErrorCode, - SessionFactoryApi, - FactoryAgentOptions, - FactoryContext, - FactoryDefinition, - FactoryHandle, - FactoryJsonSchema, JsonValue, - FactoryPipelineStage, - FactoryStepOptions, - FactoryRunResult, - FactoryRunStatus, - FactoryRunSummary, - FactoryListRunsOptions, - FactoryRunsPage, - FactoryRunDetail, - FactoryProgressPage, - FactoryProgressLine, - FactoryPhaseObservation, - FactoryPhaseStatus, - FactoryAgentSummary, -} from "./factory.js"; -export type { WorkflowRunOptions, WorkflowResumeOptions, WorkflowLimitOverrides, diff --git a/nodejs/src/installationConfirmation.ts b/nodejs/src/installationConfirmation.ts new file mode 100644 index 0000000000..897c81915f --- /dev/null +++ b/nodejs/src/installationConfirmation.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { + CancellationTokenSource, + ResponseError, + type CancellationToken, + type Disposable, + type MessageConnection, +} from "vscode-jsonrpc/node.js"; +import type { + InstallationConfirmationRequest, + InstallationDecision, + InstallationsConfirmResult, +} from "./generated/rpc.js"; + +/** The `installations.confirm` receiver, invoked with the transport's request cancellation. */ +export interface InstallationConfirmationReceiver { + confirm( + request: InstallationConfirmationRequest, + token: CancellationToken + ): Promise; +} + +/** + * Collects an explicit human decision for the complete runtime review. + * Match operationId and policySessionId to the exact action registered by the host + * before displaying it. Refuse unknown actions or incomplete reviews; never infer + * authority from the current session or a global pending slot. + * + * The SDK echoes the original challenge and fingerprint. Separately spawned UI + * work must observe the token to retire itself when the request or connection ends. + * Registering this callback does not enable installation capabilities. + * @experimental + */ +export type InstallationConfirmationHandler = ( + request: InstallationConfirmationRequest, + token: CancellationToken +) => InstallationDecision | Promise; + +export function createInstallationConfirmationAdapter( + connection: MessageConnection, + handler: InstallationConfirmationHandler +): InstallationConfirmationReceiver { + const closed = new CancellationTokenSource(); + const closeSubscription = connection.onClose(() => closed.cancel()); + const disposeSubscription = connection.onDispose(() => { + closed.cancel(); + closed.dispose(); + closeSubscription.dispose(); + disposeSubscription.dispose(); + }); + + return { + async confirm(request, token) { + if (!token) { + throw new Error("Installation confirmation request cancellation is unavailable"); + } + const { confirmationId, reviewFingerprint } = request; + const combined = new CancellationTokenSource(); + const subscriptions: Disposable[] = []; + const cancelRequest = () => combined.cancel(); + const cancelConnection = () => combined.cancel(); + const checkCancellation = () => { + if (closed.token.isCancellationRequested) { + throw new Error("Installation confirmation connection closed"); + } + if (token.isCancellationRequested) { + throw new ResponseError(-32800, "Installation confirmation request cancelled"); + } + }; + try { + subscriptions.push( + token.onCancellationRequested(cancelRequest), + closed.token.onCancellationRequested(cancelConnection) + ); + if (token.isCancellationRequested || closed.token.isCancellationRequested) { + combined.cancel(); + } + checkCancellation(); + const cancelled = new Promise((_resolve, reject) => { + subscriptions.push( + token.onCancellationRequested(() => + reject( + new ResponseError( + -32800, + "Installation confirmation request cancelled" + ) + ) + ), + closed.token.onCancellationRequested(() => + reject(new Error("Installation confirmation connection closed")) + ) + ); + }); + const decision = await Promise.race([ + Promise.resolve().then(() => { + checkCancellation(); + return handler(request, combined.token); + }), + cancelled, + ]); + checkCancellation(); + if (decision !== "confirm" && decision !== "decline" && decision !== "cancel") { + throw new Error("Invalid installation confirmation decision"); + } + return { confirmationId, reviewFingerprint, decision }; + } finally { + for (const subscription of subscriptions) subscription.dispose(); + combined.dispose(); + } + }, + }; +} 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 43b2128625..3b92a0a8ef 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -8,7 +8,7 @@ */ import { AsyncLocalStorage } from "node:async_hooks"; -import type { MessageConnection } from "vscode-jsonrpc/node.js"; +import type { CancellationToken, MessageConnection } from "vscode-jsonrpc/node.js"; import { ConnectionError, ErrorCodes, ResponseError } from "vscode-jsonrpc/node.js"; import { createInternalSessionRpc, createSessionRpc } from "./generated/rpc.js"; import type { @@ -16,8 +16,7 @@ import type { CanvasActionInvokeResult, CurrentToolMetadata, McpOauthPendingRequestResponse, - FactoryLogLine, - FactoryRunResult as WireFactoryRunResult, + ProtocolExternalToolDefinition, WorkflowLogLine, WorkflowRunResult as WireWorkflowRunResult, ModelSwitchAutoTierResult, @@ -61,32 +60,20 @@ import type { SessionEventType, SessionHooks, SessionUiApi, + SkillProvider, + SkillProviderCallOptions, + SkillProviderDescriptor, Tool, ToolHandler, ToolResult, ToolResultObject, TraceContextProvider, + TranscriptRecovery, TypedSessionEventHandler, UserInputHandler, UserInputRequest, UserInputResponse, } from "./types.js"; -import { - FACTORY_AGENT_OPTION_KEYS, - getFactoryDefinition, - FactoryResumeError, - isFactoryRunTerminal, - type FactoryResumeErrorCode, - type FactoryListRunsOptions, - type FactoryRunResult, - type FactoryAgentOptions, - type RunOptions, - type SessionFactoryApi, - type FactoryContext, - type FactoryHandle, - type JsonValue, - type FactoryStepOptions, -} from "./factory.js"; import { WORKFLOW_AGENT_OPTION_KEYS, getWorkflowDefinition, @@ -100,33 +87,10 @@ import { type SessionWorkflowApi, type WorkflowContext, type WorkflowHandle, + type JsonValue, type WorkflowStepOptions, } from "./workflow.js"; -function isFactoryResumeErrorCode(value: unknown): value is FactoryResumeErrorCode { - return ( - value === "not_found" || - value === "non_resumable" || - value === "already_active" || - value === "factory_already_running" || - value === "factory_limits_invalid" || - value === "factory_session_disposed" || - value === "factory_storage_unavailable" || - value === "factory_storage_corrupt" - ); -} - -function copyDefinedFactoryAgentOption( - source: FactoryAgentOptions, - target: FactoryAgentOptions, - key: TKey -): void { - const value = source[key]; - if (value !== undefined) { - target[key] = value; - } -} - function isWorkflowResumeErrorCode(value: unknown): value is WorkflowResumeErrorCode { return ( value === "not_found" || @@ -152,19 +116,21 @@ function copyDefinedWorkflowAgentOption } } -type FactoryExecutionContext = { - active: boolean; - helperScope?: "parallel" | "pipeline"; -}; - -const factoryExecutionStore = new AsyncLocalStorage(); - -function throwIfFactoryExecutionIsActive(): void { - if (factoryExecutionStore.getStore()?.active) { - throw new Error( - "factory.run, factory.resume, and factory.pause are not allowed while a factory body is running on this call path." - ); - } +/** + * 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 = { @@ -190,14 +156,6 @@ function runInWorkflowHelperScope( return workflowExecutionStore.run({ active: current?.active ?? false, helperScope }, callback); } -function runInFactoryHelperScope( - helperScope: "parallel" | "pipeline", - callback: () => Promise | TResult -): Promise | TResult { - const current = factoryExecutionStore.getStore(); - return factoryExecutionStore.run({ active: current?.active ?? false, helperScope }, callback); -} - /** * Convert a raw hook input received over the wire into its public-facing shape. * This deserializes the numeric Unix-ms `timestamp` field on BaseHookInput @@ -243,7 +201,7 @@ function isOpenCanvasInstance(value: unknown): value is OpenCanvasInstance { const FACTORY_LOG_FLUSH_DELAY_MS = 10; const MAX_FACTORY_FANOUT_ITEMS = 4096; -function assertFactoryFanoutSize(kind: "parallel" | "pipeline", size: number): void { +function assertWorkflowFanoutSize(kind: "parallel" | "pipeline", size: number): void { if (size > MAX_FACTORY_FANOUT_ITEMS) { throw new Error( `${kind}() accepts at most ${MAX_FACTORY_FANOUT_ITEMS} items; got ${size}.` @@ -251,74 +209,6 @@ function assertFactoryFanoutSize(kind: "parallel" | "pipeline", size: number): v } } -async function runFactoryParallel( - thunks: Array<() => Promise | TResult> -): Promise> { - if (!Array.isArray(thunks)) { - throw new Error( - "parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)" - ); - } - assertFactoryFanoutSize("parallel", thunks.length); - if (thunks.some((thunk) => typeof thunk !== "function")) { - throw new Error( - "parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)" - ); - } - return Promise.all( - thunks.map((thunk) => - Promise.resolve() - .then(() => runInFactoryHelperScope("parallel", thunk)) - .catch((error) => { - // Cancellation and hard runtime failures must propagate out - // of the combinator rather than be mapped to a successful - // `null`; otherwise an aborted run, or one that hit a - // resource ceiling or durable-state failure, could be - // reported as completed. An ordinary subagent failure never - // rejects — it already resolves `null`. - if (isFactoryFatalError(error)) { - throw error; - } - return null; - }) - ) - ); -} - -async function runFactoryPipeline( - items: unknown[], - ...stages: Array< - (previous: unknown, item: unknown, index: number) => Promise | unknown - > -): Promise { - if (!Array.isArray(items)) { - throw new Error("pipeline(items, ...stages): items must be an array"); - } - assertFactoryFanoutSize("pipeline", items.length); - return Promise.all( - items.map(async (item, index) => { - let previous = item; - for (const stage of stages) { - try { - previous = await runInFactoryHelperScope("pipeline", () => - stage(previous, item, index) - ); - } catch (error) { - // Propagate cancellation and hard runtime failures instead - // of mapping them to `null`, so an aborted stage — or one - // that hit a resource ceiling or durable-state failure — - // does not let the run report success. - if (isFactoryFatalError(error)) { - throw error; - } - return null; - } - } - return previous; - }) - ); -} - async function runWorkflowParallel( thunks: Array<() => Promise | TResult> ): Promise> { @@ -327,7 +217,7 @@ async function runWorkflowParallel( "parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)" ); } - assertFactoryFanoutSize("parallel", thunks.length); + assertWorkflowFanoutSize("parallel", thunks.length); if (thunks.some((thunk) => typeof thunk !== "function")) { throw new Error( "parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)" @@ -338,7 +228,7 @@ async function runWorkflowParallel( Promise.resolve() .then(() => runInWorkflowHelperScope("parallel", thunk)) .catch((error) => { - if (isFactoryFatalError(error)) { + if (isWorkflowFatalError(error)) { throw error; } return null; @@ -356,7 +246,7 @@ async function runWorkflowPipeline( if (!Array.isArray(items)) { throw new Error("pipeline(items, ...stages): items must be an array"); } - assertFactoryFanoutSize("pipeline", items.length); + assertWorkflowFanoutSize("pipeline", items.length); return Promise.all( items.map(async (item, index) => { let previous = item; @@ -366,7 +256,7 @@ async function runWorkflowPipeline( stage(previous, item, index) ); } catch (error) { - if (isFactoryFatalError(error)) { + if (isWorkflowFatalError(error)) { throw error; } return null; @@ -377,89 +267,6 @@ async function runWorkflowPipeline( ); } -class FactoryProgressBuffer { - private nextSeq = 0; - private pending: FactoryLogLine[] = []; - private flushTimer?: ReturnType; - private flushTail: Promise = Promise.resolve(); - private flushError: unknown; - private flushFailed = false; - private closed = false; - - constructor(private readonly send: (lines: FactoryLogLine[]) => Promise) {} - - enqueue(kind: FactoryLogLine["kind"], text: string): void { - if (this.closed) { - throw new Error("Cannot log after the factory run has settled"); - } - - this.pending.push({ seq: this.nextSeq++, kind, text }); - this.scheduleFlush(); - } - - async flush(): Promise { - this.clearFlushTimer(); - const lines = this.pending.splice(0); - if (lines.length > 0) { - this.flushTail = this.flushTail.then(async () => { - try { - await this.send(lines); - } catch (error) { - if (!this.flushFailed) { - this.flushFailed = true; - this.flushError = error; - } - } - }); - } - await this.flushTail; - if (this.flushFailed) { - throw this.flushError; - } - } - - async close(): Promise { - this.closed = true; - this.clearFlushTimer(); - const lines = this.pending.splice(0); - await this.flushTail; - if (this.flushFailed) { - console.warn( - "Ignoring a background factory progress flush failure after the factory body settled", - this.flushError - ); - } - if (lines.length > 0) { - try { - await this.send(lines); - } catch (error) { - console.warn( - "Failed to flush final factory progress after the factory body settled", - error - ); - } - } - } - - private scheduleFlush(): void { - if (this.flushTimer !== undefined) { - return; - } - this.flushTimer = setTimeout(() => { - this.flushTimer = undefined; - void this.flush().catch(() => {}); - }, FACTORY_LOG_FLUSH_DELAY_MS); - this.flushTimer.unref?.(); - } - - private clearFlushTimer(): void { - if (this.flushTimer !== undefined) { - clearTimeout(this.flushTimer); - this.flushTimer = undefined; - } - } -} - class WorkflowProgressBuffer { private nextSeq = 0; private pending: WorkflowLogLine[] = []; @@ -542,7 +349,7 @@ class WorkflowProgressBuffer { } } -async function awaitFactoryOperation( +async function awaitWorkflowOperation( operation: () => Promise, signal: AbortSignal ): Promise { @@ -553,32 +360,19 @@ async function awaitFactoryOperation( rejectAbort = reject; }); const onAbort = () => - rejectAbort?.(signal.reason ?? new DOMException("Factory run was aborted", "AbortError")); + rejectAbort?.(signal.reason ?? new DOMException("Workflow run was aborted", "AbortError")); // Register before the abort check and before dispatching, so an abort can // neither be missed by a not-yet-attached listener nor start work on an // already-cancelled run. signal.addEventListener("abort", onAbort, { once: true }); try { - throwIfFactoryAborted(signal); + throwIfWorkflowAborted(signal); return await Promise.race([operation(), abortPromise]); } finally { signal.removeEventListener("abort", onAbort); } } -async function awaitWorkflowOperation( - operation: () => Promise, - signal: AbortSignal -): Promise { - return awaitFactoryOperation(operation, signal); -} - -function throwIfFactoryAborted(signal: AbortSignal): void { - if (signal.aborted) { - throw signal.reason ?? new DOMException("Factory run was aborted", "AbortError"); - } -} - function throwIfWorkflowAborted(signal: AbortSignal): void { if (signal.aborted) { throw signal.reason ?? new DOMException("Workflow run was aborted", "AbortError"); @@ -586,11 +380,11 @@ function throwIfWorkflowAborted(signal: AbortSignal): void { } /** - * Whether an error represents factory run cancellation (an `AbortError`-shaped - * rejection from {@link awaitFactoryOperation}). Cancellation must bubble out of + * Whether an error represents workflow run cancellation (an `AbortError`-shaped + * rejection from {@link awaitWorkflowOperation}). Cancellation must bubble out of * `parallel`/`pipeline` rather than being flattened into a `null` result. */ -function isFactoryAbortError(error: unknown): boolean { +function isWorkflowAbortError(error: unknown): boolean { return ( typeof error === "object" && error !== null && @@ -600,7 +394,7 @@ function isFactoryAbortError(error: unknown): boolean { } /** - * Errors a factory combinator must never swallow into a `null` item. + * Errors a workflow combinator must never swallow into a `null` item. * * Cooperative cancellation aborts the run, and a rejected RPC is a hard * runtime failure — a reached limit, a durable-state failure, or a dropped @@ -608,9 +402,9 @@ function isFactoryAbortError(error: unknown): boolean { * successfully-`null` item. An ordinary subagent failure does not reject; the * runtime already resolves it as `null`. */ -function isFactoryFatalError(error: unknown): boolean { +function isWorkflowFatalError(error: unknown): boolean { return ( - isFactoryAbortError(error) || + isWorkflowAbortError(error) || error instanceof ResponseError || error instanceof ConnectionError ); @@ -653,16 +447,17 @@ 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(); private commandHandlers: Map = new Map(); - private factories = new Map>(); - private factoryAbortControllers = new Map>(); private workflows = new Map>(); private workflowAbortControllers = new Map>(); private permissionHandler?: PermissionHandler; @@ -671,6 +466,7 @@ export class CopilotSession { private elicitationHandler?: ElicitationHandler; private exitPlanModeHandler?: ExitPlanModeHandler; private autoModeSwitchHandler?: AutoModeSwitchHandler; + private skillProvider?: SkillProvider; private hooks?: SessionHooks; private transformCallbacks?: Map; private _rpc: ReturnType | null = null; @@ -688,81 +484,11 @@ export class CopilotSession { clientSessionApis: ClientSessionApiHandlers = {}; /** - * Friendly factory API for running registered factories by name or handle. + * Friendly workflow API for running registered workflows by name or handle. * - * @experimental Part of the experimental Agent Factories surface and may + * @experimental Part of the experimental Dynamic Workflows surface and may * change or be removed in future SDK or CLI releases. */ - readonly factory: SessionFactoryApi = { - run: (async ( - nameOrHandle: string | FactoryHandle, - options?: RunOptions - ): Promise => { - throwIfFactoryExecutionIsActive(); - const name = - typeof nameOrHandle === "string" - ? nameOrHandle - : getFactoryDefinition(nameOrHandle).meta.name; - if (options?.resumeFromRunId !== undefined) { - return this.factory.resume(options.resumeFromRunId, { - limits: options.limits, - notifyOnComplete: options.notifyOnComplete, - logPhaseNames: options.logPhaseNames, - }); - } - const envelope = await this.rpc.factory.run({ - name, - args: options?.args === undefined ? {} : options.args, - options: { - limits: options?.limits, - notifyOnComplete: options?.notifyOnComplete, - logPhaseNames: options?.logPhaseNames, - }, - }); - - return this.settleFactoryRun(envelope); - }) as SessionFactoryApi["run"], - resume: (async (runId: string, options?: Parameters[1]) => { - throwIfFactoryExecutionIsActive(); - let response; - try { - response = await this.rpc.factory.resume({ - runId, - limits: options?.limits, - notifyOnComplete: options?.notifyOnComplete, - logPhaseNames: options?.logPhaseNames, - }); - } catch (error) { - if ( - error instanceof ResponseError && - typeof error.data === "object" && - error.data !== null - ) { - const code = (error.data as { code?: unknown }).code; - if (isFactoryResumeErrorCode(code)) { - throw new FactoryResumeError(code, error.message); - } - } - throw error; - } - return this.settleFactoryRun(response.run); - }) as SessionFactoryApi["resume"], - getRun: async (runId) => this.rpc.factory.getRun({ runId }), - waitForRun: (runId, options) => this.waitForFactoryRun(runId, options?.signal), - listRuns: (async (options?: FactoryListRunsOptions) => { - const page = await this.rpc.factory.listRuns(options ?? {}); - return options === undefined ? page.runs : page; - }) as SessionFactoryApi["listRuns"], - getRunDetail: (runId) => this.rpc.factory.getRunDetail({ runId }), - getRunProgress: (runId, options = {}) => - this.rpc.factory.getRunProgress({ runId, ...options }), - pause: async (runId) => { - throwIfFactoryExecutionIsActive(); - return this.rpc.factory.pause({ runId }); - }, - cancel: async (runId) => this.rpc.factory.cancel({ runId }), - }; - readonly workflow: SessionWorkflowApi = { run: (async ( nameOrHandle: string | WorkflowHandle, @@ -799,130 +525,32 @@ export class CopilotSession { if ( error instanceof ResponseError && typeof error.data === "object" && - error.data !== null - ) { - const code = (error.data as { code?: unknown }).code; - if (isWorkflowResumeErrorCode(code)) { - throw new WorkflowResumeError(code, error.message); - } - } - throw error; - } - return this.settleWorkflowRun(response.run); - }) as SessionWorkflowApi["resume"], - getRun: async (runId) => this.rpc.workflow.getRun({ runId }), - waitForRun: (runId, options) => this.waitForWorkflowRun(runId, options?.signal), - listRuns: (async (options?: WorkflowListRunsOptions) => { - const page = await this.rpc.workflow.listRuns(options ?? {}); - return options === undefined ? page.runs : page; - }) as SessionWorkflowApi["listRuns"], - getRunDetail: (runId) => this.rpc.workflow.getRunDetail({ runId }), - getRunProgress: (runId, options = {}) => - this.rpc.workflow.getRunProgress({ runId, ...options }), - pause: async (runId) => { - throwIfWorkflowExecutionIsActive(); - return this.rpc.workflow.pause({ runId }); - }, - cancel: async (runId) => this.rpc.workflow.cancel({ runId }), - }; - - /** - * Resolve a start/resume envelope into the terminal envelope callers expect. - * - * The CLI may answer `session.factory.run` and `session.factory.resume` - * before the run settles, so a non-terminal envelope is followed by a wait - * on the run's terminal state. - */ - private settleFactoryRun(envelope: WireFactoryRunResult): Promise { - if (isFactoryRunTerminal(envelope.status)) { - return Promise.resolve(envelope); - } - return this.waitForFactoryRun(envelope.runId); - } - - /** - * Resolve when a factory run reaches a terminal status. - * - * The subscription is installed *before* the first read so a transition - * landing between the two cannot be missed, and re-reads are serialized so - * overlapping invalidation events cannot interleave — the run's revision - * advances once per operation, so a burst of events is common and must - * collapse into a single in-flight read. A bounded periodic re-read keeps a - * dropped invalidation from leaving the wait pending forever. - */ - private waitForFactoryRun(runId: string, signal?: AbortSignal): Promise { - const abortError = (): unknown => - signal?.reason ?? new DOMException("Factory run wait was aborted", "AbortError"); - if (signal?.aborted === true) { - return Promise.reject(abortError()); - } - - return new Promise((resolve, reject) => { - let settled = false; - let reading = false; - let rereadRequested = false; - let pollHandle: ReturnType | undefined; - let unsubscribe: (() => void) | undefined; - let onAbort: (() => void) | undefined; - - const finish = (complete: () => void): void => { - if (settled) { - return; - } - settled = true; - if (pollHandle !== undefined) { - clearInterval(pollHandle); - } - unsubscribe?.(); - if (onAbort !== undefined) { - signal?.removeEventListener("abort", onAbort); - } - complete(); - }; - - const read = async (): Promise => { - if (settled) { - return; - } - if (reading) { - rereadRequested = true; - return; - } - reading = true; - try { - do { - rereadRequested = false; - const envelope = await this.rpc.factory.getRun({ runId }); - if (isFactoryRunTerminal(envelope.status)) { - finish(() => resolve(envelope)); - return; - } - } while (rereadRequested && !settled); - } catch (error) { - finish(() => reject(error)); - } finally { - reading = false; - } - }; - - if (signal !== undefined) { - onAbort = (): void => finish(() => reject(abortError())); - signal.addEventListener("abort", onAbort, { once: true }); - } - - unsubscribe = this.on("factory.run_updated", (event) => { - if (event.data.runId === runId) { - void read(); - } - }); - - pollHandle = setInterval(() => void read(), 5_000); - // The re-read is a safety net, not work the process owes anyone: an - // outstanding wait must never keep Node alive on its own. - pollHandle.unref?.(); - void read(); - }); - } + error.data !== null + ) { + const code = (error.data as { code?: unknown }).code; + if (isWorkflowResumeErrorCode(code)) { + throw new WorkflowResumeError(code, error.message); + } + } + throw error; + } + return this.settleWorkflowRun(response.run); + }) as SessionWorkflowApi["resume"], + getRun: async (runId) => this.rpc.workflow.getRun({ runId }), + waitForRun: (runId, options) => this.waitForWorkflowRun(runId, options?.signal), + listRuns: (async (options?: WorkflowListRunsOptions) => { + const page = await this.rpc.workflow.listRuns(options ?? {}); + return options === undefined ? page.runs : page; + }) as SessionWorkflowApi["listRuns"], + getRunDetail: (runId) => this.rpc.workflow.getRunDetail({ runId }), + getRunProgress: (runId, options = {}) => + this.rpc.workflow.getRunProgress({ runId, ...options }), + pause: async (runId) => { + throwIfWorkflowExecutionIsActive(); + return this.rpc.workflow.pause({ runId }); + }, + cancel: async (runId) => this.rpc.workflow.cancel({ runId }), + }; private settleWorkflowRun(envelope: WireWorkflowRunResult): Promise { if (isWorkflowRunTerminal(envelope.status)) { @@ -1001,7 +629,7 @@ export class CopilotSession { signal.addEventListener("abort", onAbort, { once: true }); } - unsubscribe = this.on("factory.run_updated", (event) => { + unsubscribe = this.on("workflow.run_updated", (event) => { if (event.data.runId === runId) { void read(); } @@ -1068,6 +696,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. @@ -1401,15 +1039,9 @@ export class CopilotSession { this.elicitationHandler = undefined; this.exitPlanModeHandler = undefined; this.autoModeSwitchHandler = undefined; + this.skillProvider = undefined; this.commandHandlers.clear(); this.canvases.clear(); - this.factories.clear(); - for (const controllersForRun of this.factoryAbortControllers.values()) { - for (const controller of controllersForRun.values()) { - controller.abort(); - } - } - this.factoryAbortControllers.clear(); this.workflows.clear(); for (const controllersForRun of this.workflowAbortControllers.values()) { for (const controller of controllersForRun.values()) { @@ -1927,7 +1559,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); + } } } } @@ -2000,213 +1654,6 @@ export class CopilotSession { }; } - /** - * Registers factory closures and reverse-RPC handlers for this session. - * - * @param factories - Factory handles declared by the joining extension. - * @internal Called by the SDK when an extension joins a session. - */ - registerFactories(factories?: FactoryHandle[]): void { - this.factories.clear(); - if (!factories || factories.length === 0) { - delete this.clientSessionApis.factory; - return; - } - - for (const handle of factories) { - const definition = getFactoryDefinition(handle); - if (this.factories.has(definition.meta.name)) { - throw new Error( - `Duplicate factory name "${definition.meta.name}". Factory names must be unique within a joinSession call.` - ); - } - this.factories.set(definition.meta.name, definition); - } - - const self = this; - this.clientSessionApis.factory = { - async execute(params) { - const definition = self.factories.get(params.name); - if (!definition) { - const message = `No factory registered with name "${params.name}"`; - throw new ResponseError(ErrorCodes.InvalidParams, message, { - code: "factory_not_found", - name: params.name, - }); - } - - const controller = new AbortController(); - // Keyed by execution token as well as run ID so overlapping - // attempts for one run stay individually addressable. - let controllersForRun = self.factoryAbortControllers.get(params.runId); - if (controllersForRun === undefined) { - controllersForRun = new Map(); - self.factoryAbortControllers.set(params.runId, controllersForRun); - } - controllersForRun.set(params.executionToken, controller); - const progress = new FactoryProgressBuffer(async (lines) => { - await self.rpc.factory.log({ - runId: params.runId, - executionToken: params.executionToken, - lines, - }); - }); - try { - const context: FactoryContext = { - runId: params.runId, - args: params.args, - session: self, - signal: controller.signal, - phase: (title: string) => { - throwIfFactoryAborted(controller.signal); - progress.enqueue("phase", title); - }, - log: (message: string) => { - throwIfFactoryAborted(controller.signal); - progress.enqueue("log", message); - }, - agent: async (prompt, options = {}) => { - await progress.flush(); - const opts: FactoryAgentOptions = {}; - for (const key of FACTORY_AGENT_OPTION_KEYS) { - copyDefinedFactoryAgentOption(options, opts, key); - } - const response = await awaitFactoryOperation( - () => - self.rpc.factory.agent({ - factoryRunId: params.runId, - executionToken: params.executionToken, - prompt, - opts, - }), - controller.signal - ); - return response.result ?? null; - }, - step: async ( - key: string, - producer: () => Promise | JsonValue, - options: FactoryStepOptions = {} - ): Promise => { - await progress.flush(); - if (options.volatile) { - // The flush above is an await point, so an abort can land - // between entering step() and running the producer. The - // journaled branch is covered by awaitFactoryOperation; - // this one has to check for itself, or a cancelled run - // would still start new extension work. - throwIfFactoryAborted(controller.signal); - return producer(); - } - const cached = await awaitFactoryOperation( - () => - self.rpc.factory.journal.get({ - runId: params.runId, - executionToken: params.executionToken, - key, - }), - controller.signal - ); - if (cached.hit) { - if (cached.resultJson === undefined) { - throw new Error( - `step("${key}") journal returned a hit without a result` - ); - } - assertFactoryStepResult(cached.resultJson, key); - return cached.resultJson; - } - - // Producers are best-effort at-least-once across crashes or - // concurrent callers, so authors must make side effects idempotent. - const result = await producer(); - assertFactoryStepResult(result, key); - await awaitFactoryOperation( - () => - self.rpc.factory.journal.put({ - runId: params.runId, - executionToken: params.executionToken, - key, - resultJson: result, - }), - controller.signal - ); - return result; - }, - pause: async (key: string): Promise => { - if (typeof key !== "string" || key.length === 0) { - throw new Error("Factory pause checkpoint key must not be empty"); - } - const helperScope = factoryExecutionStore.getStore()?.helperScope; - if (helperScope !== undefined) { - throw new Error( - `Factory pause checkpoints are not allowed inside ${helperScope}() branches` - ); - } - await progress.flush(); - const response = await awaitFactoryOperation( - () => - self.internalRpc.factory.pauseAtCheckpoint({ - runId: params.runId, - executionToken: params.executionToken, - key, - }), - controller.signal - ); - switch (response.action) { - case "continue": - return; - case "pause": - await awaitFactoryOperation( - () => new Promise(() => {}), - controller.signal - ); - } - }, - parallel: runFactoryParallel, - pipeline: runFactoryPipeline, - factory: async () => { - throw new Error("nested factories are not supported"); - }, - }; - const execution = { active: true }; - const result = await factoryExecutionStore.run(execution, async () => { - try { - return await definition.run(context); - } finally { - execution.active = false; - } - }); - if (result === undefined) { - return {}; - } - assertFactoryResult(result); - return { result }; - } finally { - try { - await progress.close(); - } finally { - const controllersForRun = self.factoryAbortControllers.get(params.runId); - if (controllersForRun?.get(params.executionToken) === controller) { - controllersForRun.delete(params.executionToken); - if (controllersForRun.size === 0) { - self.factoryAbortControllers.delete(params.runId); - } - } - } - } - }, - async abort(params) { - const controllersForRun = self.factoryAbortControllers.get(params.runId); - const controller = controllersForRun?.get(params.executionToken); - if (controller !== undefined) { - controller.abort(new DOMException("Factory run was aborted", "AbortError")); - } - return {}; - }, - }; - } - /** * Registers workflow closures and reverse-RPC handlers for this session. * @@ -2692,6 +2139,82 @@ export class CopilotSession { this.userInputHandler = handler; } + /** + * Registers the session's skill provider. + * + * @param provider - The skill provider, or undefined to remove it + * @internal This method is typically called internally when creating a session. + */ + registerSkillProvider(provider?: SkillProvider): void { + this.skillProvider = provider; + } + + /** + * Handles a `skillProvider.list` request from the runtime. + * + * @internal This method is for internal use by the SDK. + */ + async _handleSkillProviderList( + token?: CancellationToken + ): Promise<{ skills: SkillProviderDescriptor[] }> { + const provider = this.requireSkillProvider(); + return await this.callSkillProvider("listSkills", token, async (options) => ({ + skills: (await provider.listSkills(options)) ?? [], + })); + } + + /** + * Handles a `skillProvider.read` request from the runtime. + * + * @internal This method is for internal use by the SDK. + */ + async _handleSkillProviderRead( + name: string, + token?: CancellationToken + ): Promise<{ markdown: string | null }> { + const provider = this.requireSkillProvider(); + return await this.callSkillProvider("readSkill", token, async (options) => ({ + markdown: (await provider.readSkill(name, options)) ?? null, + })); + } + + private requireSkillProvider(): SkillProvider { + if (!this.skillProvider) { + throw new Error(`No skill provider for session: ${this.sessionId}`); + } + return this.skillProvider; + } + + /** + * Runs a provider call with an abort signal tied to the runtime's request + * cancellation. Failures are logged locally and reported generically. + */ + private async callSkillProvider( + operation: string, + token: CancellationToken | undefined, + call: (options: SkillProviderCallOptions) => Promise + ): Promise { + const controller = new AbortController(); + const subscription = token?.onCancellationRequested(() => controller.abort()); + if (token?.isCancellationRequested) { + controller.abort(); + } + try { + return await call({ signal: controller.signal }); + } catch (error) { + if (controller.signal.aborted) { + throw new ResponseError(-32800, `Skill provider ${operation} cancelled`); + } + console.error(`Skill provider ${operation} failed`, { + sessionId: this.sessionId, + error, + }); + throw skillProviderFailure(operation); + } finally { + subscription?.dispose(); + } + } + /** * Registers hook handlers for session lifecycle events. * @@ -2808,6 +2331,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]; @@ -3002,6 +2527,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 @@ -3065,7 +2644,12 @@ function toCanvasRpcError(error: unknown): ResponseError { return new ResponseError(ErrorCodes.InternalError, message, { code, message }); } -type FactoryResultValidationCategory = +// The provider's own error text is deliberately not forwarded to the runtime. +function skillProviderFailure(operation: string): ResponseError { + return new ResponseError(ErrorCodes.InternalError, `Skill provider ${operation} failed`); +} + +type WorkflowResultValidationCategory = | "unsupported_type" | "non_finite_number" | "negative_zero" @@ -3074,21 +2658,17 @@ type FactoryResultValidationCategory = | "unsupported_object"; interface StrictJsonValidationContext { - code: - | "factory_result_not_json" - | "factory_step_not_json" - | "workflow_result_not_json" - | "workflow_step_not_json"; + code: "workflow_result_not_json" | "workflow_step_not_json"; label: string; allowTopLevelUndefined: boolean; } function strictJsonValidationError( context: StrictJsonValidationContext, - category: FactoryResultValidationCategory, + category: WorkflowResultValidationCategory, message: string, path: string -): ResponseError<{ code: string; category: FactoryResultValidationCategory; path: string }> { +): ResponseError<{ code: string; category: WorkflowResultValidationCategory; path: string }> { return new ResponseError(ErrorCodes.InternalError, message, { code: context.code, category, @@ -3251,22 +2831,6 @@ function assertStrictJson( visit(value, "$", context.allowTopLevelUndefined); } -function assertFactoryResult(value: unknown): asserts value is JsonValue | undefined { - assertStrictJson(value, { - code: "factory_result_not_json", - label: "Factory result", - allowTopLevelUndefined: true, - }); -} - -function assertFactoryStepResult(value: unknown, key: string): asserts value is JsonValue { - assertStrictJson(value, { - code: "factory_step_not_json", - label: `Factory step "${key}" result`, - allowTopLevelUndefined: false, - }); -} - function assertWorkflowResult(value: unknown): asserts value is JsonValue | undefined { assertStrictJson(value, { code: "workflow_result_not_json", 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 98e594356c..b4259454cb 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -10,6 +10,8 @@ import type { Canvas } from "./canvas.js"; import type { SessionFsProvider } from "./sessionFsProvider.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; +import type { InstallationConfirmationHandler } from "./installationConfirmation.js"; +export type { InstallationConfirmationHandler } from "./installationConfirmation.js"; import type { AttachmentExtensionContext as GeneratedExtensionContextAttachment, AutoTier, @@ -21,7 +23,7 @@ import type { SessionEvent as GeneratedSessionEvent, } from "./generated/session-events.js"; import type { CopilotSession } from "./session.js"; -import type { FactoryJsonSchema, JsonValue } from "./factory.js"; +import type { JsonValue } from "./workflow.js"; import type { ExtensionLaunchProviderHandler as GeneratedExtensionLaunchProvider, GitHubTokenAcquireRequest, @@ -36,7 +38,10 @@ import type { import type { ToolSet } from "./toolSet.js"; 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, @@ -50,6 +55,8 @@ export type { ConnectorMcpStatus, ConnectorReconcileRequest, ConnectorRuntimeStatus, + ConnectorSessionAccount, + ConnectorSessionAccountResult, ConnectorStatus, ExtensionLaunchProfile, ExtensionLaunchProviderResolveRequest, @@ -59,6 +66,11 @@ export type { GitHubTelemetryNotification, GitHubTelemetryEvent, GitHubTelemetryClientInfo, + InstallationConfirmationRequest, + InstallationConfirmationResponse, + InstallationDecision, + InstallationReview, + McpInstallationReview, } from "./generated/rpc.js"; /** @@ -108,7 +120,10 @@ 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"; +import type { SkillProviderDescriptor } from "./generated/rpc.js"; +export type { SkillProviderDescriptor }; export type { PermissionDecisionContext, PermissionDecisionOutcome, @@ -414,6 +429,13 @@ export interface CopilotClientOptions { */ extensionLaunchProvider?: ExtensionLaunchProvider; + /** + * Connection-global human review for experimental installation operations. + * Does not register or enable installation capabilities on the runtime. + * @experimental + */ + installationConfirmationHandler?: InstallationConfirmationHandler; + /** * Log level for the Copilot runtime. When omitted, the runtime uses its * own default (currently `"info"`). @@ -1137,11 +1159,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 */ @@ -1836,6 +1911,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; } // ============================================================================ @@ -2149,68 +2233,6 @@ export interface CanvasProviderIdentity { name?: string; } -/** - * Static resource ceilings declared by a factory before it runs. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryLimits { - /** Maximum number of factory subagents that may run concurrently. Must be positive when present. */ - maxConcurrentSubagents?: number; - /** Maximum total number of factory subagents that may be spawned. Must be positive when present. */ - maxTotalSubagents?: number; - /** Maximum AI credits consumed by factory subagents and descendants. This post-paid ceiling is soft. */ - maxAiCredits?: number; - /** - * Maximum accumulated active-execution time, in seconds. Active execution includes the entire extension body, - * subprocess waits, queued-agent waits, and sleeps. The limit is armed from the remaining headroom when a run - * resumes; time between attempts is not counted. Must be finite and positive when present. - */ - timeoutSeconds?: number; -} - -/** - * Registration metadata for an extension-authored factory. - * - * @experimental Part of the experimental Agent Factories surface and may - * change or be removed in future SDK or CLI releases. - */ -export interface FactoryMeta { - /** Stable factory name used for invocation. */ - name: string; - /** Human-readable factory description. */ - description: string; - /** Display metadata for the progress phases the factory may report. */ - phases: Array<{ title: string; detail?: string }>; - /** - * Optional declared shape of the arguments this factory expects as `ctx.args`. - * - * Declaring one is strongly recommended for any factory that reads `ctx.args`. - * When the model invokes the factory through the `run_factory` tool, the CLI - * validates `args` against this declaration **before** the run starts, so a - * malformed call is rejected with a correction hint and retried without ever - * creating a run row, prompting the user for permission, or spending credits. A - * factory that declares nothing is never validated: a malformed call starts, - * takes an approval, spends credits, and then fails inside the factory body. - * `factories_manage` with `operation: "inspect"` reports the declared shape so an - * agent can read it before invoking. - * - * This covers the model's `run_factory` path only. `session.factory.run(...)` is - * not validated against the declaration, so a factory should still check - * `ctx.args` rather than assume the declared shape held. - * - * Enforcement covers structure — types, required properties, and enum/const - * values. Finer constraints such as `minLength`, `pattern`, and - * `additionalProperties` are recorded in the declaration but not enforced. See - * {@link FactoryJsonSchema} for the accepted subset. A declaration outside that - * subset is rejected at registration. - */ - argsSchema?: FactoryJsonSchema; - /** Optional resource ceilings presented to the user before execution. */ - limits?: FactoryLimits; -} - /** * Provider-scoped options for the Copilot API (CAPI). * @@ -2338,6 +2360,13 @@ export interface ManagedSettingsPermissions { * (across managed layers) must admit an operation for it to be allowed. */ allow?: string[]; + /** + * Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, + * or `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and + * fragments are rejected. Multiple managed layers intersect their lists; + * an empty list denies all hosts. + */ + limitTo?: string[]; } /** @@ -2354,12 +2383,73 @@ export interface ManagedSettings { /** Selects the model-facing shape of the built-in `ask_user` tool. */ export type AskUserVariant = "legacy" | "elicitation"; +/** + * Supplies session-scoped skills from the host's own storage, such as a + * database, instead of `SKILL.md` files on disk. + * + * The runtime calls {@link SkillProvider.listSkills} when it builds the + * session's skill catalog, and {@link SkillProvider.readSkill} each time it + * needs a skill's instructions: when the model loads the skill through the + * `skill` tool, when a user runs `/skill-name`, or when a custom agent lists + * the skill. Calls may run concurrently, including on behalf of sub-agents, and + * each call must finish within 30 seconds. Errors thrown by a provider are + * never shown to the model. + * + * Supply a provider with {@link SessionConfigBase.skillProvider}. Providers + * aren't supported for cloud sessions. + * + * @experimental Session-scoped skill providers are experimental and may change + * or be removed in future SDK or CLI releases. + */ +export interface SkillProvider { + /** + * Returns catalog metadata for every skill the provider supplies. The + * descriptors are authoritative: names must be unique ignoring case, and + * the runtime validates their limits. + */ + listSkills( + options: SkillProviderCallOptions + ): SkillProviderDescriptor[] | Promise; + + /** + * Returns the `SKILL.md` text for the named skill, or `null`/`undefined` + * when the skill no longer exists. + * + * YAML frontmatter is optional. Fields it omits come from the skill's + * descriptor, fields it declares must match the descriptor, and + * `allowed-tools` can only be set there. Text whose first line is `---` is + * parsed as frontmatter. + * + * @param name - The skill's name, as listed by {@link SkillProvider.listSkills}. + */ + readSkill( + name: string, + options: SkillProviderCallOptions + ): string | null | undefined | Promise; +} + +/** + * Per-call options passed to {@link SkillProvider} methods. + * + * @experimental Session-scoped skill providers are experimental and may change + * or be removed in future SDK or CLI releases. + */ +export interface SkillProviderCallOptions { + /** + * Aborted when the runtime cancels the call, for example when it times out + * or the session disconnects. The runtime ignores any later result. + */ + signal: AbortSignal; +} + /** * Shared configuration fields used by both {@link SessionConfig} (for * creating a new session) and {@link ResumeSessionConfig} (for resuming * an existing one). */ export interface SessionConfigBase { + /** Exact model IDs allowed by the host. Omission preserves runtime policy. */ + allowedModels?: string[]; /** * Client name to identify the application using the SDK. * Included in the User-Agent header for API requests. @@ -2830,6 +2920,23 @@ export interface SessionConfigBase { */ skillDirectories?: string[]; + /** + * Supplies skills from the host's own storage instead of `SKILL.md` files on + * disk. Provider skills join the session's skill catalog alongside file-based + * skills. See {@link SkillProvider}. + * + * Supplying a provider enables skills unless `enableSkills` is set explicitly, + * so set `enableSkills: true` when the client runs in `"empty"` mode. With + * `enableSkills: false` the provider stays bound but receives no calls. + * + * The provider isn't persisted. Pass it again when resuming the session; + * resuming without one removes the provider's skills. + * + * @experimental Session-scoped skill providers are experimental and may change + * or be removed in future SDK or CLI releases. + */ + skillProvider?: SkillProvider; + /** * Local filesystem paths to Open Plugins-format directories * (https://open-plugins.com/) to load for this session. @@ -3060,6 +3167,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. @@ -3069,7 +3181,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, @@ -3086,6 +3199,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. @@ -3141,6 +3264,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. */ @@ -3166,6 +3303,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 */ @@ -3272,6 +3415,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. */ @@ -3545,6 +3694,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/src/workflow.ts b/nodejs/src/workflow.ts index 6065c710ec..f7c0abe2d1 100644 --- a/nodejs/src/workflow.ts +++ b/nodejs/src/workflow.ts @@ -14,9 +14,16 @@ import type { } from "./generated/rpc.js"; import type { ContextTier } from "./generated/session-events.js"; import type { CopilotSession } from "./session.js"; -import type { JsonValue } from "./factory.js"; export type { WorkflowRunResult }; +/** A value that can be represented losslessly on the SDK JSON wire. */ +export type JsonValue = + | null + | boolean + | number + | string + | JsonValue[] + | { [key: string]: JsonValue }; export type { WorkflowAgentSummary, WorkflowPhaseStatus, @@ -404,7 +411,7 @@ export interface SessionWorkflowApi { * snapshot: resuming the same durable run can later change the envelope * returned by {@link SessionWorkflowApi.getRun}. * - * This watches the runtime's `factory.run_updated` compatibility event and + * This watches the runtime's `workflow.run_updated` event and * periodically re-reads the durable envelope so a missed event cannot * leave the wait hanging. Pass a `signal` to stop waiting; aborting rejects * and has no effect on the run itself, which keeps executing. Use diff --git a/nodejs/test/client-api-codegen.test.ts b/nodejs/test/client-api-codegen.test.ts index 9331ad7689..bb0c9c8fe6 100644 --- a/nodejs/test/client-api-codegen.test.ts +++ b/nodejs/test/client-api-codegen.test.ts @@ -1,8 +1,17 @@ import { describe, expect, it } from "vitest"; -import { emitClientSessionApiRegistration as emitGoClientSessionApiRegistration } from "../../scripts/codegen/go.ts"; -import { emitClientSessionApiRegistration as emitPythonClientSessionApiRegistration } from "../../scripts/codegen/python.ts"; -import { emitClientSessionApiRegistration as emitTypeScriptClientSessionApiRegistration } from "../../scripts/codegen/typescript.ts"; +import { + emitClientGlobalApiRegistration as emitTypeScriptClientGlobalApiRegistration, + emitClientSessionApiRegistration as emitTypeScriptClientSessionApiRegistration, +} from "../../scripts/codegen/typescript.ts"; +import { + emitClientGlobalApiRegistration as emitGoClientGlobalApiRegistration, + emitClientSessionApiRegistration as emitGoClientSessionApiRegistration, +} from "../../scripts/codegen/go.ts"; +import { + emitClientGlobalApiRegistration as emitPythonClientGlobalApiRegistration, + emitClientSessionApiRegistration as emitPythonClientSessionApiRegistration, +} from "../../scripts/codegen/python.ts"; const clientSessionSchema: Record = { mixed: { @@ -73,6 +82,59 @@ describe("client-session API codegen", () => { expect(allInternalCode).not.toContain("InternalOnlyHandler"); }); + describe("client-global API codegen", () => { + it("keeps internal methods out of global registration", () => { + const code = emitTypeScriptClientGlobalApiRegistration(clientSessionSchema).join("\n"); + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + }); + }); + + describe("client-global host API codegen", () => { + const schema = { + host: { + shutdown: { + rpcMethod: "host.shutdown", + visibility: "internal", + params: { $ref: "#/definitions/HostEmptyResult" }, + result: { $ref: "#/definitions/HostEmptyResult" }, + }, + exited: { + rpcMethod: "host.exited", + params: { $ref: "#/definitions/HostExitedNotification" }, + result: { type: "null" }, + notification: true, + }, + }, + }; + + it("uses named request references for Go child and exit handlers", () => { + const lines: string[] = []; + emitGoClientGlobalApiRegistration(lines, schema, (name) => name, new Map()); + const code = lines.join("\n"); + expect(code).toContain('"host.exited"'); + expect(code).toContain("request *HostEmptyResult"); + expect(code).toContain("request *HostExitedNotification"); + expect(code).not.toContain("HostShutdownRequest"); + }); + + it("uses named Python exit references and dictionary child shutdown payloads", () => { + const lines: string[] = []; + emitPythonClientGlobalApiRegistration(lines, schema, (name) => + name === "HostEmptyResult" ? "dict" : name + ); + const code = lines.join("\n"); + expect(code).toContain("request = HostExitedNotification.from_dict(params)"); + expect(code).toContain('"host.exited"'); + expect(code).not.toContain("HostExitedRequest"); + expect(code).toContain("request = dict(params)"); + expect(code).toContain("async def shutdown(self, params: dict) -> dict:"); + expect(code).not.toContain("dict.from_dict"); + expect(code).not.toContain("HostShutdownResult"); + expect(code).not.toContain("HostShutdownRequest"); + }); + }); + it("excludes internal methods from Go handlers", () => { const lines: string[] = []; emitGoClientSessionApiRegistration(lines, clientSessionSchema, (name) => name, new Map()); diff --git a/nodejs/test/client-global-api.test.ts b/nodejs/test/client-global-api.test.ts new file mode 100644 index 0000000000..32d2bdf6b9 --- /dev/null +++ b/nodejs/test/client-global-api.test.ts @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +import { PassThrough } from "node:stream"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node.js"; +import { + registerClientGlobalApiHandlers, + type ClientGlobalApiHandlers, + type ExtensionLaunchProviderResolveRequest, + type ExtensionLaunchProviderResolveResult, +} from "../src/generated/rpc.js"; + +function connect(handlers: ClientGlobalApiHandlers) { + const outbound = new PassThrough(); + const inbound = new PassThrough(); + const client = createMessageConnection( + new StreamMessageReader(inbound), + new StreamMessageWriter(outbound) + ); + const server = createMessageConnection( + new StreamMessageReader(outbound), + new StreamMessageWriter(inbound) + ); + onTestFinished(() => { + client.dispose(); + server.dispose(); + inbound.destroy(); + outbound.destroy(); + }); + registerClientGlobalApiHandlers(client, handlers); + client.listen(); + server.listen(); + return server; +} + +const request: ExtensionLaunchProviderResolveRequest = { + id: "project:extension", + modulePath: "/extensions/example/index.js", + name: "Example", + source: "project", +}; + +describe("client-global API transport", () => { + it("awaits a handler and returns its result without a session", async () => { + let release!: (result: ExtensionLaunchProviderResolveResult) => void; + const result = new Promise((resolve) => { + release = resolve; + }); + const handler = vi.fn(async () => result); + const server = connect({ extensionLaunchProvider: { resolve: handler } }); + const response = server.sendRequest("extensionLaunchProvider.resolve", request); + const completed = vi.fn(); + void response.then(completed); + + await vi.waitFor(() => expect(handler).toHaveBeenCalledWith(request)); + expect(completed).not.toHaveBeenCalled(); + const launch = { executable: "/app/extension-host", args: ["example"], env: {} }; + release({ launch }); + + await expect(response).resolves.toEqual({ launch }); + expect(handler).toHaveBeenCalledTimes(1); + }); + + it("keeps global handlers and responses on their original connection", async () => { + const firstHandler = vi.fn(async () => ({ + launch: { executable: "/first/host", args: [], env: {} }, + })); + const secondHandler = vi.fn(async () => ({ + launch: { executable: "/second/host", args: [], env: {} }, + })); + const first = connect({ extensionLaunchProvider: { resolve: firstHandler } }); + const second = connect({ extensionLaunchProvider: { resolve: secondHandler } }); + + const responses = await Promise.all([ + first.sendRequest("extensionLaunchProvider.resolve", request), + second.sendRequest("extensionLaunchProvider.resolve", request), + ]); + + expect(responses).toEqual([ + { launch: { executable: "/first/host", args: [], env: {} } }, + { launch: { executable: "/second/host", args: [], env: {} } }, + ]); + expect(firstHandler).toHaveBeenCalledTimes(1); + expect(secondHandler).toHaveBeenCalledTimes(1); + }); + + it("continues dispatching requests while a global handler is pending", async () => { + let release!: (result: ExtensionLaunchProviderResolveResult) => void; + const result = new Promise((resolve) => { + release = resolve; + }); + const handler = vi.fn(async () => result); + const tokenHandler = vi.fn(async () => ({ kind: "cancelled" as const })); + const server = connect({ + extensionLaunchProvider: { resolve: handler }, + gitHubToken: { getToken: tokenHandler }, + }); + const pending = server.sendRequest("extensionLaunchProvider.resolve", request); + await vi.waitFor(() => expect(handler).toHaveBeenCalledOnce()); + + await expect( + server.sendRequest("gitHubToken.getToken", { + registrationId: "registration", + host: "github.com", + reason: "initial", + }) + ).resolves.toEqual({ kind: "cancelled" }); + + release({}); + await expect(pending).resolves.toEqual({}); + }); + + it("returns an error when no global handler is registered", async () => { + const server = connect({}); + + await expect( + server.sendRequest("extensionLaunchProvider.resolve", request) + ).rejects.toThrow("No extensionLaunchProvider client-global handler registered"); + }); + + it("propagates handler failure without substituting a successful result", async () => { + const server = connect({ + extensionLaunchProvider: { + resolve: async () => { + throw new Error("Host review unavailable"); + }, + }, + }); + + await expect( + server.sendRequest("extensionLaunchProvider.resolve", request) + ).rejects.toThrow("Host review unavailable"); + }); +}); diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 3cb97160c9..06ee845738 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", {}); } ); }); @@ -3715,7 +4126,6 @@ describe("CopilotClient", () => { }, }); - // Wait for the async handler to complete await vi.waitFor(() => expect(handler).toHaveBeenCalledTimes(1)); expect(handler).toHaveBeenCalledWith( expect.objectContaining({ @@ -4347,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", () => { @@ -4389,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[] = []; @@ -4485,6 +5348,7 @@ describe("managedSettings serialization", () => { deny: ["Shell(git push)"], ask: ["Domain(publish.example)"], allow: ["Read(**)"], + limitTo: ["Domain(api.github.com)"], }, } satisfies ManagedSettings; const params = await captureCreateParams({ @@ -4496,6 +5360,7 @@ describe("managedSettings serialization", () => { deny: ["Shell(git push)"], ask: ["Domain(publish.example)"], allow: ["Read(**)"], + limitTo: ["Domain(api.github.com)"], }, }); }); @@ -4561,9 +5426,11 @@ describe("managedSettings serialization", () => { it("preserves empty arrays in the permissions object", async () => { const params = await captureCreateParams({ - managedSettings: { permissions: { deny: [], ask: [], allow: [] } }, + managedSettings: { permissions: { deny: [], ask: [], allow: [], limitTo: [] } }, + }); + expect(params.managedSettings).toEqual({ + permissions: { deny: [], ask: [], allow: [], limitTo: [] }, }); - expect(params.managedSettings).toEqual({ permissions: { deny: [], ask: [], allow: [] } }); }); it("forwards managedSettings on session.resume", async () => { diff --git a/nodejs/test/connectors.test.ts b/nodejs/test/connectors.test.ts index b6046a434d..cf83c7959b 100644 --- a/nodejs/test/connectors.test.ts +++ b/nodejs/test/connectors.test.ts @@ -1,8 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + import { describe, expect, it, vi } from "vitest"; import type { MessageConnection } from "vscode-jsonrpc"; +import { createServerRpc } from "../src/generated/rpc.js"; +import type { + AuthIdentityMetadata, + ConnectorSessionAccount, + ConnectorSessionAccountResult, +} from "../src/index.js"; import { CopilotSession } from "../src/session.js"; +describe("client Connector discovery RPC", () => { + it("lists Connectors without creating a session", async () => { + const sendRequest = vi.fn(async (method: string) => { + if (method === "connectors.getCapabilities") { + return { + apiVersion: 1, + availability: "enabled", + opaqueAccountSelection: true, + conditionalCache: true, + }; + } + if (method === "connectors.getAccounts") { + return { + availability: "enabled", + accounts: [ + { + accountId: "account-1", + authInfo: { + type: "user", + host: "https://github.com", + login: "octocat", + }, + }, + ], + }; + } + return { + accountId: "account-1", + revision: 1, + refreshedAtMs: 1, + connectors: [ + { + name: "calendar", + displayName: "Calendar", + status: "connected", + }, + ], + }; + }); + const rpc = createServerRpc({ + sendRequest, + } as unknown as MessageConnection); + + expect(await rpc.connectors.getCapabilities()).toMatchObject({ + apiVersion: 1, + availability: "enabled", + }); + expect(await rpc.connectors.getAccounts()).toEqual({ + availability: "enabled", + accounts: [ + { + accountId: "account-1", + authInfo: { + type: "user", + host: "https://github.com", + login: "octocat", + }, + }, + ], + }); + expect(await rpc.connectors.list({ accountId: "account-1" })).toMatchObject({ + revision: 1, + }); + expect(await rpc.connectors.refresh({ accountId: "account-1" })).toMatchObject({ + revision: 1, + }); + + expect(sendRequest.mock.calls).toEqual([ + ["connectors.getCapabilities", {}], + ["connectors.getAccounts", {}], + ["connectors.list", { accountId: "account-1" }], + ["connectors.refresh", { accountId: "account-1" }], + ]); + }); +}); + describe("session Connector RPC", () => { it("exposes the host-owned experimental Connector lifecycle", async () => { const sendRequest = vi.fn(async (method: string) => { @@ -135,8 +221,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 2f666be125..9592bf66da 100644 --- a/nodejs/test/csharp-codegen.test.ts +++ b/nodejs/test/csharp-codegen.test.ts @@ -2,7 +2,188 @@ import type { JSONSchema7 } from "json-schema"; import { describe, expect, it } from "vitest"; import { generateRpcCode, generateSessionEventsCode } from "../../scripts/codegen/csharp.ts"; -import type { ApiSchema } from "../../scripts/codegen/utils.ts"; +import type { ApiSchema, RpcMethod } from "../../scripts/codegen/utils.ts"; +import { legacyRequestSchema } from "./legacy-parameters-fixture.ts"; + +function legacySignatureFixture(scope: "server" | "session", legacy: unknown) { + const params: JSONSchema7 & { "x-legacy-parameters"?: unknown } = { + title: "SampleRequest", + type: "object", + properties: { + ...(scope === "session" ? { sessionId: { type: "string" as const } } : {}), + name: { type: "string" }, + config: { "x-opaque-json": true } as JSONSchema7, + expectedIdentity: { type: "string" }, + }, + required: scope === "session" ? ["sessionId", "name"] : ["name"], + additionalProperties: false, + }; + if (legacy !== undefined) params["x-legacy-parameters"] = legacy; + const methods: Record = { + run: { + rpcMethod: `${scope === "session" ? "session." : ""}sample.run`, + params, + result: null, + }, + }; + const schema: ApiSchema = { [scope]: { sample: methods } }; + return { schema, params, methods }; +} + +describe("C# additive request entry compatibility", () => { + it.each(["server", "session"] as const)( + "preserves the exact legacy %s signature and adds a same-name request overload", + (scope) => { + const { schema } = legacySignatureFixture(scope, ["name", "config"]); + const code = generateRpcCode(schema); + expect(code).toContain( + "RunAsync(string name, object? config = null, CancellationToken cancellationToken = default)" + ); + expect(code).toContain( + "RunAsync(SampleRequest request, CancellationToken cancellationToken = default)" + ); + expect(code).not.toContain("WithRequestAsync"); + expect(code).toContain("public sealed class SampleRequest\n"); + expect(code).toContain("public required string Name { get; set; }"); + expect(code).toContain("ArgumentNullException.ThrowIfNull(request);"); + expect(code).toContain("ArgumentNullException.ThrowIfNull(request.Name);"); + expect(code).toContain("Config = CopilotClient.ToJsonElementForWire(config)"); + expect(code).not.toContain("ExpectedIdentity = expectedIdentity"); + if (scope === "session") { + expect(code).toContain("internal sealed class SampleRequestWithSession\n"); + expect(code).toContain( + "new SampleRequestWithSession { SessionId = _session.SessionId, Name = request.Name, Config = request.Config, ExpectedIdentity = request.ExpectedIdentity }" + ); + const publicRequest = code.match( + /public sealed class SampleRequest\n\{[\s\S]*?\n\}/ + )?.[0]; + expect(publicRequest).toBeDefined(); + expect(publicRequest).not.toContain("SessionId"); + expect(code).toContain("[JsonSerializable(typeof(SampleRequestWithSession))]"); + } else { + expect(code).toContain('(_rpc, "sample.run", [request], cancellationToken)'); + } + } + ); + + it.each(["server", "session"] as const)( + "leaves unmarked %s method output unchanged", + (scope) => { + const { schema, methods, params } = legacySignatureFixture(scope, undefined); + methods.control = { + ...methods.run, + rpcMethod: methods.run.rpcMethod.replace(/run$/, "control"), + params: { ...params, title: "ControlRequest" }, + }; + const before = generateRpcCode(schema); + params["x-legacy-parameters"] = ["name", "config"]; + const after = generateRpcCode(schema); + const method = / public async Task ControlAsync\([\s\S]*?\n \}/; + const request = /internal sealed class ControlRequest\n\{[\s\S]*?\n\}/; + expect(before.match(method)?.[0]).toBeDefined(); + expect(after.match(method)?.[0]).toBe(before.match(method)?.[0]); + expect(before.match(request)?.[0]).toBeDefined(); + expect(after.match(request)?.[0]).toBe(before.match(request)?.[0]); + expect(before).not.toContain("WithRequestAsync"); + expect(after).not.toContain("ControlWithRequestAsync"); + } + ); + + it.each([ + ["not an array", "name"], + ["non-string", ["name", 1]], + ["duplicate", ["name", "name"]], + ["unknown", ["name", "missing"]], + ["session envelope", ["sessionId", "name"]], + ["missing required", ["config"]], + ["optional before required", ["config", "name"]], + ["no new arguments", ["name", "config", "expectedIdentity"]], + ])("rejects invalid legacy metadata: %s", (_reason, legacy) => { + const { schema } = legacySignatureFixture("session", legacy); + expect(() => generateRpcCode(schema)).toThrow("Invalid x-legacy-parameters"); + }); + + it("rejects nullable request metadata rather than changing the existing request entry", () => { + const { schema, methods, params } = legacySignatureFixture("session", ["name", "config"]); + methods.run.params = { anyOf: [params, { type: "null" }] }; + expect(() => generateRpcCode(schema)).toThrow("nullable requests"); + }); + + it("keeps both overloads unchanged across two successive optional additions", () => { + const signatures = (additions: 1 | 2) => { + const code = generateRpcCode(legacyRequestSchema(additions).schema); + return { + code, + overloads: code.match(/public async Task<[A-Za-z]+> PlanAsync\([^)]*\)/g), + }; + }; + const one = signatures(1); + const two = signatures(2); + expect(one.overloads).toEqual([ + "public async Task PlanAsync(string contract, string source, string? scope = null, CancellationToken cancellationToken = default)", + "public async Task PlanAsync(SamplePlanRequest request, CancellationToken cancellationToken = default)", + ]); + expect(two.overloads).toEqual(one.overloads); + expect(one.code).not.toContain("TraceId"); + expect(two.code).toContain("public string? TraceId { get; set; }"); + expect(two.code).toContain("public required string Contract { get; set; }"); + }); +}); + +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) => { @@ -51,7 +232,137 @@ 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: { + host: { + createSession: { + rpcMethod: "host.createSession", + params: { + type: "object", + properties: { + config: { + type: "object", + properties: {}, + additionalProperties: true, + }, + }, + required: ["config"], + }, + result: { type: "null" }, + }, + }, + }, + } as ApiSchema); + expect(code).toContain("IDictionary Config"); + expect(code).not.toContain("class HostCreateSessionParamsConfig"); + }); + + it("maps nullable host authentication options to nullable booleans", () => { + const code = generateRpcCode({ + server: { + host: { + start: { + rpcMethod: "host.start", + params: { + type: "object", + properties: { requireConnectionToken: { type: ["boolean", "null"] } }, + }, + result: { type: "null" }, + }, + }, + }, + } as ApiSchema); + expect(code).toContain("bool? RequireConnectionToken"); + }); + it.each(["uninstall", "update"])( "separates the session wire envelope from the shared plugins %s request", (method) => { @@ -474,3 +785,71 @@ describe("C# RPC codegen", () => { } ); }); + +describe("C# RPC class visibility", () => { + const sharedResultSchema = (): ApiSchema => ({ + definitions: { + FolderTrustCheckResult: { + type: "object", + title: "FolderTrustCheckResult", + properties: { trusted: { type: "boolean" } }, + required: ["trusted"], + }, + }, + // The server tree is emitted before the session tree, so the internal + // method reaches the shared result type first. + server: { + permissions: { + isTrusted: { + rpcMethod: "permissions.folderTrust.isTrusted", + visibility: "internal", + result: { $ref: "#/definitions/FolderTrustCheckResult" }, + }, + }, + }, + session: { + permissions: { + isTrusted: { + rpcMethod: "session.permissions.folderTrust.isTrusted", + result: { $ref: "#/definitions/FolderTrustCheckResult" }, + }, + }, + }, + }); + + it("keeps a type shared with an internal method public", () => { + const code = generateRpcCode(sharedResultSchema()); + + expect(code).toContain("public sealed class FolderTrustCheckResult\n"); + expect(code).not.toContain("internal sealed class FolderTrustCheckResult\n"); + }); + + it("still emits a request wrapper as internal", () => { + const code = generateRpcCode({ + definitions: {}, + server: { + git: { + workingDirectoryContext: { + rpcMethod: "git.workingDirectoryContext", + params: { + type: "object", + properties: { cwd: { type: "string" } }, + required: ["cwd"], + }, + }, + }, + }, + }); + + expect(code).toContain("internal sealed class GitWorkingDirectoryContextRequest\n"); + }); + + it("honors an explicitly internal schema over public reachability", () => { + const schema = sharedResultSchema(); + (schema.definitions!.FolderTrustCheckResult as Record).visibility = + "internal"; + const code = generateRpcCode(schema); + + expect(code).toContain("internal sealed class FolderTrustCheckResult\n"); + }); +}); diff --git a/nodejs/test/e2e/accounts_login.e2e.test.ts b/nodejs/test/e2e/accounts_login.e2e.test.ts new file mode 100644 index 0000000000..f0b08e7759 --- /dev/null +++ b/nodejs/test/e2e/accounts_login.e2e.test.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it, onTestFinished } from "vitest"; +import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const { openAiEndpoint, env, workDir } = await createSdkTestContext({ + useStdio: true, + copilotClientOptions: { gitHubToken: undefined, useLoggedInUser: false }, +}); +const loginEnv = { + ...env, + GH_TOKEN: "", + GITHUB_TOKEN: "", + COPILOT_GITHUB_TOKEN: "", + COPILOT_SDK_AUTH_TOKEN: "", + GITHUB_COPILOT_API_TOKEN: "", + COPILOT_HMAC_KEY: "", + CAPI_HMAC_KEY: "", + COPILOT_DISABLE_KEYTAR: "1", + COPILOT_DEBUG_ENTRA_BROKER_URL: `${env.COPILOT_API_URL}/entra-broker`, +}; +const githubToken = "ghu_sdk_entra_login"; +await openAiEndpoint.setEntraLogin("sdk-entra-subject", githubToken); +await openAiEndpoint.setCopilotUserByToken(githubToken, { + login: "sdk_entra_user", + copilot_plan: "business", + is_staff: false, + is_mcp_enabled: true, + endpoints: { api: env.COPILOT_API_URL, telemetry: "https://localhost:1/telemetry" }, +}); + +function clientForTest(): CopilotClient { + const client = new CopilotClient({ + workingDirectory: workDir, + env: loginEnv, + useLoggedInUser: false, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + onTestFinished(() => client.stop()); + return client; +} + +describe("Shared Entra login", () => { + it("returns a cancellable flow before discovery and preserves session ownership", async () => { + const client = clientForTest(); + await using session = await client.createSession({ onPermissionRequest: approveAll }); + await using other = await client.createSession({ onPermissionRequest: approveAll }); + const providers = await session.rpc.accounts.enumerate({ query: { kind: "providers" } }); + expect(providers.kind).toBe("providers"); + if (providers.kind !== "providers") throw new Error("Expected provider descriptors"); + expect(providers.items).toContainEqual( + expect.objectContaining({ kind: "entra", available: true }) + ); + const begun = await session.rpc.accounts.login.begin({ kind: "entra" }); + expect(begun.step).toEqual({ kind: "awaiting" }); + await expect(other.rpc.accounts.login.cancel({ flowId: begun.flowId })).rejects.toThrow( + "different session" + ); + await session.rpc.accounts.login.cancel({ flowId: begun.flowId }); + await expect(session.rpc.accounts.login.advance({ flowId: begun.flowId })).rejects.toThrow( + "Unknown or completed" + ); + }); + + it("completes Entra sign-in through the public API and applies the live session", async () => { + const client = clientForTest(); + await using session = await client.createSession({ onPermissionRequest: approveAll }); + const begun = await session.rpc.accounts.login.begin({ kind: "entra" }); + const request = { flowId: begun.flowId }; + expect(await session.rpc.accounts.login.advance(request)).toEqual({ + kind: "needs-interaction", + }); + expect(await session.rpc.accounts.login.advance(request)).toEqual({ + kind: "completed", + result: { status: "needs-plaintext-consent" }, + }); + const completed = await session.rpc.accounts.login.advance({ ...request, input: "allow" }); + expect(completed).toEqual({ + kind: "completed", + result: { status: "completed", host: "https://github.com", login: "sdk_entra_user" }, + }); + expect(await session.rpc.gitHubAuth.getStatus()).toMatchObject({ + isAuthenticated: true, + login: "sdk_entra_user", + }); + const active = await session.rpc.accounts.get({ query: { kind: "activeAccount" } }); + expect(active).toMatchObject({ + kind: "activeAccount", + account: { login: "sdk_entra_user", kind: "entraEmu", active: true }, + authInfo: { host: "https://github.com", login: "sdk_entra_user" }, + }); + expect(JSON.stringify(active)).not.toContain(githubToken); + expect(JSON.stringify(active)).not.toContain("sdk-entra-subject"); + await expect(session.rpc.accounts.login.advance(request)).rejects.toThrow( + "Unknown or completed" + ); + }); + + it("declines storage consent without activating an account", async () => { + const client = clientForTest(); + await using session = await client.createSession({ onPermissionRequest: approveAll }); + const { flowId } = await session.rpc.accounts.login.begin({ kind: "entra" }); + await session.rpc.accounts.login.advance({ flowId }); + await session.rpc.accounts.login.advance({ flowId }); + expect(await session.rpc.accounts.login.advance({ flowId, input: "decline" })).toEqual({ + kind: "completed", + result: { status: "declined" }, + }); + expect(await session.rpc.accounts.get({ query: { kind: "activeAccount" } })).toEqual({ + kind: "activeAccount", + account: null, + }); + }); +}); 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/auth_host.e2e.test.ts b/nodejs/test/e2e/auth_host.e2e.test.ts new file mode 100644 index 0000000000..4d1cf182d0 --- /dev/null +++ b/nodejs/test/e2e/auth_host.e2e.test.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe.each([ + ["tenant.ghe.example", "fallback.ghe.example", "https://tenant.ghe.example"], + ["", "fallback.ghe.example", "https://fallback.ghe.example"], + ["", "", "https://github.com"], +])( + "auth host with COPILOT_GH_HOST=%s and GH_HOST=%s", + async (copilotHost, ghHost, expectedHost) => { + const { copilotClient: client } = await createSdkTestContext({ + copilotClientOptions: { + env: { + COPILOT_GH_HOST: copilotHost, + GH_HOST: ghHost, + }, + }, + }); + + it("reports the selected authentication host", async () => { + await client.start(); + const status = await client.getAuthStatus(); + expect(status.isAuthenticated).toBe(true); + expect(status.host).toBe(expectedHost); + }); + } +); 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 bbcd35e174..544ab712e7 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -29,6 +29,7 @@ function saveCapture() { clientResponses, env: { COPILOT_HOME: process.env.COPILOT_HOME, + COPILOT_RUNTIME_PROCESS_FILE_LOGGING: process.env.COPILOT_RUNTIME_PROCESS_FILE_LOGGING, COPILOT_SDK_AUTH_TOKEN: process.env.COPILOT_SDK_AUTH_TOKEN, COPILOT_OTEL_ENABLED: process.env.COPILOT_OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, @@ -205,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({ @@ -259,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 }), @@ -349,6 +346,7 @@ describe("Client options", async () => { expect(path.resolve(capture.cwd)).toBe(path.resolve(workDir)); expect(capture.env.COPILOT_HOME).toBe(copilotHomeFromOption); + expect(capture.env.COPILOT_RUNTIME_PROCESS_FILE_LOGGING).toBe("1"); expect(capture.env.COPILOT_SDK_AUTH_TOKEN).toBe("process-option-token"); expect(capture.env.COPILOT_OTEL_ENABLED).toBe("true"); expect(capture.env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("http://127.0.0.1:4318"); @@ -404,6 +402,34 @@ describe("Client options", async () => { await resumed.disconnect(); }); + it("preserves an explicit process-file logging setting in empty mode", async () => { + const cliPath = path.join(workDir, `fake-cli-empty-logging-${Date.now()}.js`); + const capturePath = path.join(workDir, `fake-cli-empty-logging-${Date.now()}.json`); + fs.writeFileSync(cliPath, FAKE_STDIO_CLI_SCRIPT); + const client = new CopilotClient({ + mode: "empty", + baseDirectory: workDir, + logLevel: "debug", + env: { + ...env, + COPILOT_RUNTIME_PROCESS_FILE_LOGGING: "1", + }, + connection: RuntimeConnection.forStdio({ + path: cliPath, + args: ["--capture-file", capturePath], + }), + }); + onTestFinished(() => client.stop()); + await client.start(); + + const capture = JSON.parse(fs.readFileSync(capturePath, "utf8")) as { + env: Record; + args: string[]; + }; + expect(capture.env.COPILOT_RUNTIME_PROCESS_FILE_LOGGING).toBe("1"); + assertArgumentValue(capture.args, "--log-level", "debug"); + }); + it("should register and invoke an extension launch provider during startup", async () => { const cliPath = path.join(workDir, `fake-cli-extension-provider-${Date.now()}.js`); const capturePath = path.join(workDir, `fake-cli-extension-provider-${Date.now()}.json`); 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..2b66896538 100644 --- a/nodejs/test/e2e/compaction.e2e.test.ts +++ b/nodejs/test/e2e/compaction.e2e.test.ts @@ -1,9 +1,253 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + import { describe, expect, it } from "vitest"; -import { approveAll, type CopilotSession, type SessionEvent } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { + approveAll, + CopilotRequestHandler, + type CopilotSession, + type SessionEvent, +} from "../../src/index.js"; +import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; +import { testBackend } from "./harness/testBackend.js"; const compactionTimeoutMs = 60_000; +class RefusalBudgetReplayHandler extends CopilotRequestHandler { + readonly requests: { model: string; max_tokens: number }[] = []; + + protected override async sendRequest(request: Request): Promise { + const path = new URL(request.url).pathname; + if (path === "/models") { + return Response.json({ + data: [ + ["claude-sonnet-5.5", 250_000, 384_000, 128_000], + ["claude-sonnet-5", 112_000, 128_000, 64_000], + ].map(([id, prompt, context, output]) => ({ + id, + name: id, + object: "model", + vendor: "Anthropic", + version: "1", + model_picker_enabled: true, + supported_endpoints: ["/v1/messages"], + capabilities: { + type: "chat", + family: id, + tokenizer: "o200k_base", + supports: { streaming: true, tool_calls: true, vision: false }, + limits: { + max_prompt_tokens: prompt, + max_context_window_tokens: context, + max_output_tokens: output, + }, + }, + })), + }); + } + if (path.endsWith("/models/session")) return Response.json({}); + if (path.includes("/policy")) return Response.json({ state: "enabled" }); + expect(path).toBe("/v1/messages"); + const body = (await request.json()) as { + model: string; + max_tokens: number; + stream?: boolean; + }; + this.requests.push(body); + const refuses = body.model === "claude-sonnet-5.5"; + const content = refuses ? "PRIMARY_REFUSAL" : "FALLBACK_BUDGET_OK"; + const message = { + id: "refusal-budget-replay", + type: "message", + role: "assistant", + model: body.model, + content: [{ type: "text", text: content }], + stop_reason: refuses ? "refusal" : "end_turn", + stop_sequence: null, + usage: { input_tokens: 100, output_tokens: 10 }, + }; + if (!body.stream) return Response.json(message); + const events = [ + { + type: "message_start", + message: { ...message, content: [], stop_reason: null }, + }, + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: content }, + }, + { type: "content_block_stop", index: 0 }, + { + type: "message_delta", + delta: { stop_reason: message.stop_reason, stop_sequence: null }, + usage: { output_tokens: 10 }, + }, + { type: "message_stop" }, + ]; + return new Response( + events + .map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`) + .join(""), + { headers: { "content-type": "text/event-stream" } } + ); + } +} + +class BudgetReplayHandler extends CopilotRequestHandler { + readonly requests: unknown[] = []; + + protected override async sendRequest(request: Request): Promise { + expect(request.url).toMatch(/\/chat\/completions$/); + const body = (await request.json()) as { stream?: boolean }; + this.requests.push(body); + if (!body.stream) { + return Response.json({ + id: "budget-replay", + object: "chat.completion", + created: 1, + model: "uncatalogued-budget-model", + choices: [ + { + index: 0, + message: { role: "assistant", content: "BUDGET_CONTROL_OK" }, + finish_reason: "stop", + }, + ], + usage: { prompt_tokens: 100, completion_tokens: 10, total_tokens: 110 }, + }); + } + const chunk = { + id: "budget-replay", + object: "chat.completion.chunk", + created: 1, + model: "uncatalogued-budget-model", + choices: [{ index: 0, delta: { content: "BUDGET_CONTROL_OK" }, finish_reason: null }], + }; + return new Response( + `data: ${JSON.stringify(chunk)}\n\ndata: ${JSON.stringify({ + ...chunk, + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\ndata: [DONE]\n\n`, + { headers: { "content-type": "text/event-stream" } } + ); + } +} + +describe("Refusal fallback output budget", async () => { + const { createClient } = await createSdkTestContext({ + copilotClientOptions: { + env: { + ANTHROPIC_REFUSAL_FALLBACK: "true", + COPILOT_EXP_COPILOT_CLI_ANTHROPIC_REFUSAL_FALLBACK: "true", + }, + }, + }); + + it("should clamp fallback output before admission and retain its effective budget", async () => { + const handler = new RefusalBudgetReplayHandler(); + const budgetClient = createClient({ requestHandler: handler }); + try { + await budgetClient.start(); + expect( + (await budgetClient.listModels()).find((model) => model.id === "claude-sonnet-5") + ).toMatchObject({ + capabilities: { + limits: { + max_prompt_tokens: 112_000, + max_context_window_tokens: 128_000, + max_output_tokens: 64_000, + }, + }, + }); + const session = await budgetClient.createSession({ + onPermissionRequest: approveAll, + model: "claude-sonnet-5.5", + // Refusal fallback is a first-party contract, not a BYOK switch. + providers: [], + infiniteSessions: { enabled: false }, + }); + const events: SessionEvent[] = []; + session.on((event) => events.push(event)); + expect( + (await session.sendAndWait({ prompt: "Exercise refusal fallback admission." })) + ?.data.content + ).toBe("FALLBACK_BUDGET_OK"); + expect( + handler.requests.map(({ model, max_tokens }) => ({ model, max_tokens })) + ).toEqual([ + { model: "claude-sonnet-5.5", max_tokens: 128_000 }, + { model: "claude-sonnet-5", max_tokens: 64_000 }, + ]); + expect(events.filter((event) => event.type === "session.error")).toEqual([]); + // Recorded model changes can arrive after the directly forwarded idle event. + await expect + .poll(() => events.filter((event) => event.type === "session.model_change")) + .toEqual([ + expect.objectContaining({ + data: expect.objectContaining({ + newModel: "claude-sonnet-5", + previousModel: "claude-sonnet-5.5", + cause: "refusal_fallback", + source: "automatic", + }), + }), + ]); + expect((await session.rpc.model.getCurrent()).modelId).toBe("claude-sonnet-5"); + const usage = events.filter((event) => event.type === "assistant.usage"); + expect( + usage.map(({ data: { model, maxPromptTokens, maxOutputTokens } }) => ({ + model, + maxPromptTokens, + maxOutputTokens, + })) + ).toEqual([ + { model: "claude-sonnet-5.5", maxPromptTokens: 250_000, maxOutputTokens: 128_000 }, + { model: "claude-sonnet-5", maxPromptTokens: 64_000, maxOutputTokens: 64_000 }, + ]); + expect( + ( + await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 64_000, + }) + ).contextInfo + ).toMatchObject({ + modelName: "claude-sonnet-5", + limit: 112_000, + promptTokenLimit: 64_000, + }); + // The next turn uses Sonnet 5's normal 32k cap, not the one-shot retry's 64k cap. + expect( + ( + await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 0, + }) + ).contextInfo + ).toMatchObject({ limit: 112_000, promptTokenLimit: 96_000 }); + expect( + (await session.sendAndWait({ prompt: "Check the retained fallback budget." }))?.data + .content + ).toBe("FALLBACK_BUDGET_OK"); + expect(handler.requests).toHaveLength(3); + expect(handler.requests[2]).toMatchObject({ + model: "claude-sonnet-5", + max_tokens: 32_000, + }); + await session.disconnect(); + } finally { + await budgetClient.stop(); + } + }); +}); + function getNextSessionEvent( session: CopilotSession, eventType: TEventType, @@ -35,12 +279,227 @@ function getNextSessionEvent( } describe("Compaction", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { copilotClient: client, openAiEndpoint, createClient } = await createSdkTestContext(); + + it("should reject an oversized provider prompt before any inference request", async () => { + const handler = new BudgetReplayHandler(); + const budgetClient = createClient({ requestHandler: handler }); + const createSession = (maxPromptTokens: number) => + budgetClient.createSession({ + onPermissionRequest: approveAll, + model: "uncatalogued-budget-model", + provider: { + type: "openai", + wireApi: "completions", + baseUrl: "https://budget-replay.invalid/v1", + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: "uncatalogued-budget-model", + maxPromptTokens, + maxOutputTokens: 200_000, + }, + infiniteSessions: { enabled: false }, + }); + try { + const prompt = "budget-boundary ".repeat(16_384); + const control = await createSession(250_000); + expect((await control.sendAndWait({ prompt }))?.data.content).toBe("BUDGET_CONTROL_OK"); + expect(handler.requests).toHaveLength(1); + await control.disconnect(); + // Leave room for static context so this reaches final request admission. + const session = await createSession(32_000); + const errors: Extract[] = []; + session.on((event) => { + if (event.type === "session.error") errors.push(event); + }); + const admissionError = + /Request exceeds the model's prompt budget after reserving output \(\d+ tokens; limit 32000\)\. Reduce context or compact before continuing\./; + await expect(session.sendAndWait({ prompt })).rejects.toThrow(admissionError); + expect(errors).toHaveLength(1); + expect(errors[0].data.message).toMatch(admissionError); + expect(handler.requests).toHaveLength(1); + expect((await session.rpc.model.getCurrent()).modelId).toBe( + "uncatalogued-budget-model" + ); + await session.disconnect(); + } finally { + await budgetClient.stop(); + } + }); + + it("should report the effective prompt budget after manual compaction", async () => { + const handler = new BudgetReplayHandler(); + const budgetClient = createClient({ requestHandler: handler }); + try { + const session = await budgetClient.createSession({ + onPermissionRequest: approveAll, + model: "uncatalogued-budget-model", + provider: { + type: "openai", + wireApi: "completions", + baseUrl: "https://budget-replay.invalid/v1", + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: "uncatalogued-budget-model", + }, + modelCapabilities: { + limits: { + max_prompt_tokens: 112_000, + max_context_window_tokens: 128_000, + max_output_tokens: 32_000, + }, + }, + infiniteSessions: { enabled: false }, + }); + expect( + (await session.sendAndWait({ prompt: "Initialize manual compaction." }))?.data + .content + ).toBe("BUDGET_CONTROL_OK"); + const events: SessionEvent[] = []; + session.on((event) => { + if ( + event.type === "session.compaction_start" || + event.type === "session.compaction_complete" + ) { + events.push(event); + } + }); + const result = await session.rpc.history.compact({ trigger: "manual" }); + expect(result).toMatchObject({ + success: true, + summaryContent: "BUDGET_CONTROL_OK", + contextWindow: { tokenLimit: 96_000 }, + }); + expect(events.map((event) => [event.type, event.data])).toEqual([ + ["session.compaction_start", expect.objectContaining({ tokenLimit: 96_000 })], + [ + "session.compaction_complete", + expect.objectContaining({ success: true, tokenLimit: 96_000 }), + ], + ]); + expect(handler.requests).toHaveLength(2); + await session.disconnect(); + } finally { + await budgetClient.stop(); + } + }); + + it("should reject capacity-consuming output caps without an inference request", async () => { + const handler = new BudgetReplayHandler(); + const budgetClient = createClient({ requestHandler: handler }); + try { + const session = await budgetClient.createSession({ + onPermissionRequest: approveAll, + model: "uncatalogued-budget-model", + provider: { + type: "openai", + wireApi: "completions", + baseUrl: "https://budget-replay.invalid/v1", + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: "uncatalogued-budget-model", + }, + modelCapabilities: { + limits: { + max_prompt_tokens: 112_000, + max_context_window_tokens: 128_000, + max_output_tokens: 128_000, + }, + }, + infiniteSessions: { enabled: false }, + }); + const noCapacity = + "Output token reservation 128000 leaves no room in the 128000-token context window"; + await expect( + session.sendAndWait({ prompt: "Initialize an invalid context budget." }) + ).rejects.toThrow(noCapacity); + for (const outputTokenLimit of [128_000, 0]) { + await expect( + session.rpc.metadata.contextInfo({ promptTokenLimit: 0, outputTokenLimit }) + ).rejects.toThrow(noCapacity); + } + await expect(session.rpc.history.compact({ trigger: "manual" })).rejects.toThrow( + noCapacity + ); + const context = await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 32_000, + }); + expect(context.contextInfo).toMatchObject({ + limit: 112_000, + promptTokenLimit: 96_000, + }); + expect(handler.requests).toHaveLength(0); + await session.disconnect(); + } finally { + await budgetClient.stop(); + } + }); + + it("should resolve model switch budgets after the next request initializes context", async () => { + const handler = new BudgetReplayHandler(); + const budgetClient = createClient({ requestHandler: handler }); + try { + const session = await budgetClient.createSession({ + onPermissionRequest: approveAll, + model: "uncatalogued-budget-model", + provider: { + type: "openai", + wireApi: "completions", + baseUrl: "https://budget-replay.invalid/v1", + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: "uncatalogued-budget-model", + }, + modelCapabilities: { + limits: { + max_prompt_tokens: 112_000, + max_context_window_tokens: 128_000, + max_output_tokens: 32_000, + }, + }, + infiniteSessions: { enabled: false }, + }); + for (const promptLimit of [112_000, 100_000]) { + if (promptLimit === 100_000) { + await session.setModel("uncatalogued-budget-model", { + modelCapabilities: { + limits: { + max_prompt_tokens: promptLimit, + max_context_window_tokens: 128_000, + max_output_tokens: 32_000, + }, + }, + }); + } + expect( + (await session.sendAndWait({ prompt: "Initialize the current budget." }))?.data + .content + ).toBe("BUDGET_CONTROL_OK"); + const context = await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 0, + }); + expect(context.contextInfo).toMatchObject({ + modelName: "uncatalogued-budget-model", + limit: promptLimit, + promptTokenLimit: 96_000, + }); + } + expect(handler.requests).toHaveLength(2); + await session.disconnect(); + } finally { + await budgetClient.stop(); + } + }); it("should trigger compaction with low threshold and emit events", async () => { // Create session with very low compaction thresholds to trigger compaction quickly const session = await client.createSession({ onPermissionRequest: approveAll, + modelCapabilities: { + limits: { + max_prompt_tokens: 112_000, + max_context_window_tokens: 128_000, + max_output_tokens: 32_000, + }, + }, infiniteSessions: { enabled: true, // Trigger background compaction at 0.5% context usage (~1000 tokens) @@ -59,6 +518,10 @@ describe("Compaction", async () => { "session.compaction_start", "session.compaction_start" ); + const usageEvents: Extract[] = []; + session.on((event) => { + if (event.type === "session.usage_info") usageEvents.push(event); + }); // Wait specifically for a *successful* compaction_complete so that any transient // failed compaction event the daemon may emit before a successful retry is ignored // (mirrors the dotnet/rust references). @@ -84,7 +547,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(""); @@ -100,6 +579,60 @@ describe("Compaction", async () => { // Should remember it was about a dragon (context preserved via summary) expect(content).toContain("kaedrith"); expect(content).toContain("dragon"); + + // OpenAI transports omit the output cap; Anthropic sends its advertised maximum. + // Both must reserve the same output headroom. + expect(usageEvents.length).toBeGreaterThan(0); + for (const event of usageEvents) expect(event.data.tokenLimit).toBe(96_000); + const context = await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 0, + }); + expect(context.contextInfo).toMatchObject({ limit: 112_000, promptTokenLimit: 96_000 }); + const attribution = await session.rpc.metadata.getContextAttribution(); + expect(attribution.contextAttribution).toMatchObject({ + modelId: context.contextInfo?.modelName, + limit: 112_000, + promptTokenLimit: 96_000, + bufferTokens: context.contextInfo?.bufferTokens, + compactionThreshold: context.contextInfo?.compactionThreshold, + }); + for (const outputTokenLimit of [-1, 32_001]) { + await expect( + session.rpc.metadata.contextInfo({ promptTokenLimit: 0, outputTokenLimit }) + ).rejects.toThrow( + outputTokenLimit < 0 + ? /Invalid output token limit/ + : /exceeds the advertised maximum/ + ); + } + const afterInvalidCaps = await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 0, + }); + expect(afterInvalidCaps.contextInfo).toMatchObject({ + limit: 112_000, + promptTokenLimit: 96_000, + }); + const endpoint = { + capi: "/chat/completions", + "openai-completions": "/chat/completions", + "openai-responses": "/responses", + "anthropic-messages": "/v1/messages", + }[testBackend]; + const requests = (await openAiEndpoint.getRequests()).filter( + (request) => request.method === "POST" && request.url.endsWith(endpoint) + ); + expect(requests.length).toBeGreaterThan(0); + for (const request of requests) { + const body = JSON.parse(request.body); + if (testBackend === "anthropic-messages") { + expect(body.max_tokens).toBe(32_000); + } else { + expect(body).not.toHaveProperty("max_tokens"); + expect(body).not.toHaveProperty("max_output_tokens"); + } + } }, 120000); it("should not emit compaction events when infinite sessions disabled", async () => { @@ -159,4 +692,153 @@ describe("Compaction", async () => { await session.disconnect(); } }); + it("should resolve selected-tier defaults and confirm a smaller-tier switch", async () => { + const modelId = "gpt-5.4-mini"; + class TierBudgetReplayHandler extends BudgetReplayHandler { + protected override async sendRequest(request: Request): Promise { + const path = new URL(request.url).pathname; + if (path === "/models") { + return Response.json({ + data: [ + { + id: modelId, + name: "Tier budget fixture", + object: "model", + vendor: "OpenAI", + version: "1", + model_picker_enabled: true, + supported_endpoints: ["/chat/completions"], + capabilities: { + type: "chat", + family: modelId, + tokenizer: "o200k_base", + supports: { vision: false, streaming: true, tool_calls: true }, + limits: { + max_prompt_tokens: 112_000, + max_context_window_tokens: 128_000, + max_output_tokens: 32_000, + }, + }, + billing: { + token_prices: { + default: { max_prompt_tokens: 88_000 }, + long_context: { max_prompt_tokens: 112_000 }, + }, + }, + }, + ], + }); + } + if (path.endsWith("/models/session")) return Response.json({}); + if (path.includes("/policy")) return Response.json({ state: "enabled" }); + return super.sendRequest(request); + } + } + const handler = new TierBudgetReplayHandler(); + const budgetClient = createClient({ requestHandler: handler }); + try { + await budgetClient.start(); + expect( + (await budgetClient.listModels()).find((model) => model.id === modelId) + ).toMatchObject({ + capabilities: { + limits: { max_prompt_tokens: 112_000 }, + }, + }); + const session = await budgetClient.createSession({ + onPermissionRequest: approveAll, + model: modelId, + // Exercise CAPI pricing tiers even in the provider transport matrix. + providers: [], + contextTier: "long_context", + infiniteSessions: { enabled: false }, + }); + expect( + (await session.sendAndWait({ prompt: "tier-budget-boundary ".repeat(16_384) })) + ?.data.content + ).toBe("BUDGET_CONTROL_OK"); + const longContext = await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 0, + }); + expect(longContext.contextInfo).toMatchObject({ + modelName: modelId, + limit: 112_000, + promptTokenLimit: 96_000, + }); + const attribution = await session.rpc.metadata.getContextAttribution(); + expect(attribution.contextAttribution).toMatchObject({ + modelId, + limit: 112_000, + promptTokenLimit: 96_000, + bufferTokens: longContext.contextInfo?.bufferTokens, + }); + expect((await session.rpc.model.getCurrent()).contextTier).toBe("long_context"); + const target = { + modelId, + contextTier: "default", + modelCapabilities: { limits: { max_output_tokens: 64_000 } }, + runCompactionPreflight: true, + } as const; + const preflight = await session.rpc.model.switchTo(target); + expect(preflight).toMatchObject({ + status: "confirmation_required", + modelState: { contextTier: "long_context" }, + confirmation: { + targetModelDisplayName: modelId, + targetLimit: 64_000, + }, + }); + expect(preflight.confirmation?.currentTokens).toBeGreaterThan(64_000); + expect(preflight.confirmation?.currentTokens).toBeLessThanOrEqual(88_000); + expect(handler.requests).toHaveLength(1); + expect( + await session.rpc.model.switchTo({ ...target, compactionDecision: "cancel" }) + ).toMatchObject({ + status: "cancelled", + modelState: { contextTier: "long_context" }, + }); + expect(handler.requests).toHaveLength(1); + const compactionEvents: SessionEvent[] = []; + session.on((event) => { + if ( + event.type === "session.compaction_start" || + event.type === "session.compaction_complete" + ) { + compactionEvents.push(event); + } + }); + expect( + await session.rpc.model.switchTo({ ...target, compactionDecision: "compact" }) + ).toMatchObject({ + status: "applied", + modelState: { contextTier: "default" }, + }); + expect(compactionEvents.map((event) => [event.type, event.data])).toEqual([ + ["session.compaction_start", expect.objectContaining({ tokenLimit: 64_000 })], + [ + "session.compaction_complete", + expect.objectContaining({ success: true, tokenLimit: 64_000 }), + ], + ]); + expect(handler.requests).toHaveLength(2); + expect( + (await session.sendAndWait({ prompt: "Check the compacted default tier." }))?.data + .content + ).toBe("BUDGET_CONTROL_OK"); + const defaultContext = await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 0, + }); + expect(defaultContext.contextInfo).toMatchObject({ + modelName: modelId, + limit: 88_000, + promptTokenLimit: 64_000, + }); + expect(handler.requests).toHaveLength(3); + await session.disconnect(); + } finally { + await budgetClient.stop(); + } + }); }); 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/exp_assignments.e2e.test.ts b/nodejs/test/e2e/exp_assignments.e2e.test.ts new file mode 100644 index 0000000000..e3caab765f --- /dev/null +++ b/nodejs/test/e2e/exp_assignments.e2e.test.ts @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { + approveAll, + type CopilotExpAssignmentResponse, + type MCPStdioServerConfig, +} from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { retry } from "./harness/sdkTestHelper.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TEST_MCP_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-server.mjs"); + +describe("create-time ExP assignments", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it("selects the semantic search mode", async () => { + for (const [mode, expected] of [ + ["absent", false], + ["eager", true], + ] as const) { + const session = await client.createSession({ + onPermissionRequest: approveAll, + expAssignments: { + Features: ["copilot_cli_semantic_code_search_mode"], + Flights: { + copilot_cli_semantic_code_search_mode: mode, + }, + Configs: [ + { + Id: "default", + Parameters: { + copilot_cli_semantic_code_search_mode: mode, + }, + }, + ], + AssignmentContext: `sdk-semantic-${mode}`, + }, + }); + + try { + await session.rpc.tools.initializeAndValidate(); + const { tools } = await session.rpc.tools.getCurrentMetadata(); + const names = tools.map((tool) => tool.name); + expect(names.includes("semantic_code_search")).toBe(expected); + } finally { + await session.disconnect(); + } + } + }); + + it("drops malformed assignments and preserves a resident assignment", async () => { + const malformed = { + Features: [], + Flights: "invalid", + Configs: [], + AssignmentContext: "malformed", + } as unknown as CopilotExpAssignmentResponse; + const malformedCreate = await client.createSession({ + onPermissionRequest: approveAll, + expAssignments: malformed, + }); + try { + await malformedCreate.rpc.tools.initializeAndValidate(); + const { tools } = await malformedCreate.rpc.tools.getCurrentMetadata(); + expect(tools.map((tool) => tool.name)).not.toContain("semantic_code_search"); + } finally { + await malformedCreate.disconnect(); + } + + const eager = await client.createSession({ + onPermissionRequest: approveAll, + expAssignments: { + Features: ["copilot_cli_semantic_code_search_mode"], + Flights: { + copilot_cli_semantic_code_search_mode: "eager", + }, + Configs: [ + { + Id: "default", + Parameters: { + copilot_cli_semantic_code_search_mode: "eager", + }, + }, + ], + AssignmentContext: "sdk-semantic-eager", + }, + }); + try { + await eager.rpc.tools.initializeAndValidate(); + expect( + (await eager.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name) + ).toContain("semantic_code_search"); + + const resumed = await client.resumeSession(eager.sessionId, { + onPermissionRequest: approveAll, + expAssignments: malformed, + }); + try { + await resumed.rpc.tools.initializeAndValidate(); + expect( + (await resumed.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name) + ).toContain("semantic_code_search"); + } finally { + await resumed.disconnect(); + } + } finally { + await eager.disconnect(); + } + }); + + it("selects native lexical search and suppresses GitHub MCP code search", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + expAssignments: { + Features: ["copilot_cli_blackbird_lexical_search"], + Flights: { + copilot_cli_blackbird_lexical_search: "treatment", + }, + Configs: [ + { + Id: "default", + Parameters: { + copilot_cli_blackbird_lexical_search: true, + }, + }, + ], + AssignmentContext: "sdk-lexical-treatment", + }, + mcpServers: { + "github-mcp-server": { + type: "local", + command: "node", + args: [ + TEST_MCP_SERVER, + "--server-name", + "github-mcp-server", + "--tool-name", + "search_code", + ], + workingDirectory: dirname(TEST_MCP_SERVER), + tools: ["*"], + } satisfies MCPStdioServerConfig, + }, + }); + + try { + await retry( + "connect GitHub MCP test server", + async () => { + const servers = await session.rpc.mcp.list(); + expect( + servers.servers.find((server) => server.name === "github-mcp-server") + ?.status + ).toBe("connected"); + }, + 1_200 + ); + + for (const operation of ["initialize first turn", "reuse catalog on second turn"]) { + await session.rpc.tools.initializeAndValidate(); + const { tools } = await session.rpc.tools.getCurrentMetadata(); + const names = tools.map((tool) => tool.name); + expect(names).toContain("lexical_code_search"); + expect( + names, + `${operation}: GitHub MCP search_code must be suppressed` + ).not.toContain("github-mcp-server-search_code"); + } + } finally { + await session.disconnect(); + } + }); +}); 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/extension_startup_permission.e2e.test.ts b/nodejs/test/e2e/extension_startup_permission.e2e.test.ts new file mode 100644 index 0000000000..abc068a2a2 --- /dev/null +++ b/nodejs/test/e2e/extension_startup_permission.e2e.test.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; +import type { PermissionRequest } from "../../src/index.js"; +import { RuntimeConnection } from "../../src/index.js"; +import { createSdkTestContext, getLegacyCliPathForTests } from "./harness/sdkTestContext.js"; +import { retry } from "./harness/sdkTestHelper.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +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, "..", "..", "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 { copilotClient, createClient, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio({ path: cliPath }), + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", + SDK_ASYNC_EXTENSION_INITIALIZATION: "false", + COPILOT_EXP_COPILOT_SDK_ASYNC_EXTENSION_INITIALIZATION: "false", + }, + extensionLaunchProvider, + }, +}); + +async function installHookExtension(name: string): Promise { + const extensionDir = join(workDir, ".github", "extensions", name); + const readyFile = join(extensionDir, "joined"); + await mkdir(extensionDir, { recursive: true }); + await writeFile( + join(extensionDir, "extension.mjs"), + ` +import { writeFileSync } from "node:fs"; +import { joinSession } from "@github/copilot-sdk/extension"; + +await joinSession({ + tools: [{ + name: "${name}_tool", + description: "Proves the extension joined after permission approval.", + parameters: { type: "object", properties: {} }, + handler: async () => "ready", + }], + hooks: { onPreToolUse: async () => ({}) }, +}); +writeFileSync(${JSON.stringify(readyFile)}, "joined"); +setInterval(() => {}, 60_000); +` + ); + return readyFile; +} + +it("routes legacy create and explicit resume startup permissions through the Node SDK", async () => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir }); + const createReadyFile = await installHookExtension("create_hook"); + const createPermissions: Array<{ + request: PermissionRequest; + sessionId: string; + beforeResponse: boolean; + }> = []; + let createReturned = false; + const createPromise = copilotClient + .createSession({ + // Explicit extension requests initialize asynchronously; legacy discovery keeps create waiting. + enableConfigDiscovery: true, + extensionSdkPath: sdkDistDirectory, + disabledMcpServers: ["github-mcp-server"], + onPermissionRequest: (request, invocation) => { + createPermissions.push({ + request, + sessionId: invocation.sessionId, + beforeResponse: !createReturned, + }); + return { kind: "approve-once", approvedInteractively: true }; + }, + }) + .finally(() => { + createReturned = true; + }); + const created = await createPromise; + await retry( + "wait for create extension to join", + async () => { + expect(existsSync(createReadyFile) && readFileSync(createReadyFile, "utf8")).toBe( + "joined" + ); + }, + 300, + 100 + ); + expect(createPermissions).toEqual([ + { + request: expect.objectContaining({ + kind: "extension-permission-access", + extensionName: "project:create_hook", + }), + sessionId: created.sessionId, + beforeResponse: true, + }, + ]); + expect((await created.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name)).toContain( + "create_hook_tool" + ); + const sessionId = created.sessionId; + await created.disconnect(); + expect(await copilotClient.stop()).toEqual([]); + + await rm(join(workDir, ".github", "extensions", "create_hook"), { + recursive: true, + force: true, + }); + const resumeReadyFile = await installHookExtension("resume_hook"); + const resumeClient = createClient({ extensionLaunchProvider }); + try { + const resumePermissions: Array<{ + request: PermissionRequest; + sessionId: string; + }> = []; + const resumed = await resumeClient.resumeSession(sessionId, { + requestExtensions: true, + extensionSdkPath: sdkDistDirectory, + disabledMcpServers: ["github-mcp-server"], + onPermissionRequest: (request, invocation) => { + resumePermissions.push({ request, sessionId: invocation.sessionId }); + return { kind: "approve-once", approvedInteractively: true }; + }, + }); + try { + expect(resumed.sessionId).toBe(sessionId); + await retry( + "wait for resume extension to join", + async () => { + expect( + existsSync(resumeReadyFile) && readFileSync(resumeReadyFile, "utf8") + ).toBe("joined"); + }, + 300, + 100 + ); + expect(resumePermissions).toEqual([ + { + request: expect.objectContaining({ + kind: "extension-permission-access", + extensionName: "project:resume_hook", + }), + sessionId, + }, + ]); + expect( + (await resumed.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name) + ).toContain("resume_hook_tool"); + } finally { + await resumed.disconnect(); + } + } finally { + expect(await resumeClient.stop()).toEqual([]); + } +}); diff --git a/nodejs/test/e2e/fixtures/factory-extension.mjs b/nodejs/test/e2e/fixtures/factory-extension.mjs deleted file mode 100644 index b082599bc9..0000000000 --- a/nodejs/test/e2e/fixtures/factory-extension.mjs +++ /dev/null @@ -1,234 +0,0 @@ -import { closeSync, existsSync, fstatSync, openSync, writeFileSync, writeSync } from "node:fs"; -import { defineFactory, joinSession } from "@github/copilot-sdk/extension"; - -const marker = (name) => new URL(`./${name}`, import.meta.url); - -async function waitForMarker(name, timeoutMs) { - const deadline = Date.now() + timeoutMs; - while (!existsSync(marker(name))) { - if (Date.now() >= deadline) { - throw new Error(`Timed out waiting for ${name}`); - } - await new Promise((resolve) => setTimeout(resolve, 50)); - } -} - -function incrementMarker(name) { - const descriptor = openSync(marker(name), "a+"); - try { - writeSync(descriptor, "1"); - return fstatSync(descriptor).size; - } finally { - closeSync(descriptor); - } -} - -const argumentEcho = defineFactory({ - meta: { - name: "argument-echo", - description: "Return the invocation arguments verbatim.", - phases: [], - // Proves a declared shape survives the SDK boundary and registers against a - // real runtime. It does not exercise enforcement: `argsSchema` is checked by - // the model's `run_factory` tool, and these tests invoke `session.factory.run`, - // which does not validate. The declaration stays as wide as this factory's - // actual contract — it echoes any JsonValue, and is called with an array, an - // object, and nothing — so it cannot constrain the runs below. - argsSchema: { - type: ["object", "array", "string", "number", "integer", "boolean", "null"], - }, - }, - run: async ({ args }) => args, -}); - -const arrayResult = defineFactory({ - meta: { - name: "array-result", - description: "Return an array result.", - phases: [], - }, - run: async () => [1, "two", false], -}); - -const phased = defineFactory({ - meta: { - name: "phased", - description: "Record named phases and ordinary progress.", - phases: [{ title: "Collect" }, { title: "Summarize" }], - }, - run: async ({ phase, log }) => { - phase("Collect"); - log("Collected"); - phase("Summarize"); - log("Summarized"); - return "finished"; - }, -}); - -const forwardsSubagentOptions = defineFactory({ - meta: { - name: "forwards-subagent-options", - description: "Send every declared subagent option to the runtime.", - phases: [], - }, - run: async ({ agent }) => { - // Only the runtime's acceptance of the payload is under test. A refused - // request rejects quickly, because the runtime parses the options before - // it starts a subagent. A subagent that is merely slow to reach a model - // proves the payload was accepted, so waiting for it adds nothing and - // hangs wherever no model is reachable. - const call = agent("Confirm that this request is accepted.", { - agent: "reviewer", - reasoningEffort: "high", - contextTier: "long_context", - }); - // A rejection that lands after the race still needs a handler. - call.catch(() => {}); - let settleTimer; - const stillPending = new Promise((resolve) => { - settleTimer = setTimeout(() => resolve(undefined), 3000); - settleTimer.unref?.(); - }); - try { - await Promise.race([call, stillPending]); - return { didThrow: false }; - } catch { - return { didThrow: true }; - } finally { - clearTimeout(settleTimer); - } - }, -}); - -const startsFromContextSession = defineFactory({ - meta: { - name: "starts-from-context-session", - description: "Try to start a factory through the context session.", - phases: [], - }, - run: async ({ session }) => { - try { - await session.factory.run("argument-echo"); - return "unexpectedly started a factory"; - } catch (error) { - return error instanceof Error ? error.message : String(error); - } - }, -}); - -let session; - -const startsFromModuleSession = defineFactory({ - meta: { - name: "starts-from-module-session", - description: "Try to start a factory through the module session.", - phases: [], - }, - run: async () => { - try { - await session.factory.run("argument-echo"); - return "unexpectedly started a factory"; - } catch (error) { - return error instanceof Error ? error.message : String(error); - } - }, -}); - -const parked = defineFactory({ - meta: { - name: "parked", - description: "Wait for a test-controlled release marker.", - phases: [], - }, - run: async () => { - writeFileSync(marker("entered"), "entered"); - await waitForMarker("release", 30_000); - return "released"; - }, -}); - -const failsOnce = defineFactory({ - meta: { - name: "fails-once", - description: "Fails its first attempt and succeeds when resumed.", - phases: [], - }, - run: async () => { - if (!existsSync(marker("fails-once-attempted"))) { - writeFileSync(marker("fails-once-attempted"), "attempted"); - throw new Error("first attempt failed"); - } - return "resumed"; - }, -}); - -const externallyPaused = defineFactory({ - meta: { - name: "externally-paused", - description: "Wait until the calling session pauses this run.", - phases: [], - }, - run: async ({ signal }) => { - writeFileSync(marker("external-pause-entered"), "entered"); - await new Promise((_, reject) => { - const abort = () => reject(signal.reason ?? new Error("Factory aborted")); - if (signal.aborted) { - abort(); - return; - } - signal.addEventListener("abort", abort, { once: true }); - }); - return "unexpectedly completed"; - }, -}); - -const durablePauseCheckpoint = defineFactory({ - meta: { - name: "durable-pause-checkpoint", - description: "Pause once after journaled preparation, then complete after resume.", - phases: [], - }, - run: async ({ pause, step }) => { - const attempt = incrementMarker("checkpoint-attempts"); - const prepared = await step("prepare", () => incrementMarker("checkpoint-preparations")); - await pause("review-ready"); - return { attempt, prepared }; - }, -}); - -session = await joinSession({ - factories: [ - argumentEcho, - arrayResult, - phased, - forwardsSubagentOptions, - startsFromContextSession, - startsFromModuleSession, - parked, - failsOnce, - externallyPaused, - durablePauseCheckpoint, - ], -}); - -void waitForMarker("start-b", 30_000) - .then(async () => { - const result = await session.factory.run("argument-echo", { - args: { source: "module-watcher" }, - notifyOnComplete: false, - }); - writeFileSync(marker("b-result"), JSON.stringify({ status: "success", result })); - }) - .catch((error) => { - if (existsSync(marker("start-b"))) { - writeFileSync( - marker("b-result"), - JSON.stringify({ - status: "error", - error: error instanceof Error ? error.message : String(error), - }) - ); - } - }); - -writeFileSync(marker("ready"), "ready"); diff --git a/nodejs/test/e2e/fixtures/workflow-extension.mjs b/nodejs/test/e2e/fixtures/workflow-extension.mjs index e8e0820a4b..e6b35228bb 100644 --- a/nodejs/test/e2e/fixtures/workflow-extension.mjs +++ b/nodejs/test/e2e/fixtures/workflow-extension.mjs @@ -1,6 +1,28 @@ -import { writeFileSync } from "node:fs"; +import { closeSync, existsSync, fstatSync, openSync, writeFileSync, writeSync } from "node:fs"; import { defineWorkflow, joinSession } from "@github/copilot-sdk/extension"; +const marker = (name) => new URL(`./${name}`, import.meta.url); + +async function waitForMarker(name, timeoutMs) { + const deadline = Date.now() + timeoutMs; + while (!existsSync(marker(name))) { + if (Date.now() >= deadline) { + throw new Error(`Timed out waiting for ${name}`); + } + await new Promise((resolve) => setTimeout(resolve, 50)); + } +} + +function incrementMarker(name) { + const descriptor = openSync(marker(name), "a+"); + try { + writeSync(descriptor, "1"); + return fstatSync(descriptor).size; + } finally { + closeSync(descriptor); + } +} + const argumentEcho = defineWorkflow({ meta: { name: "argument-echo", @@ -13,12 +35,192 @@ const argumentEcho = defineWorkflow({ run: async ({ args }) => args, }); -const session = await joinSession({ - workflows: [argumentEcho], +const arrayResult = defineWorkflow({ + meta: { + name: "array-result", + description: "Return an array result.", + phases: [], + }, + run: async () => [1, "two", false], +}); + +const phased = defineWorkflow({ + meta: { + name: "phased", + description: "Record named phases and ordinary progress.", + phases: [{ title: "Collect" }, { title: "Summarize" }], + }, + run: async ({ phase, log }) => { + phase("Collect"); + log("Collected"); + phase("Summarize"); + log("Summarized"); + return "finished"; + }, +}); + +const forwardsSubagentOptions = defineWorkflow({ + meta: { + name: "forwards-subagent-options", + description: "Send every declared subagent option to the runtime.", + phases: [], + }, + run: async ({ agent }) => { + const call = agent("Confirm that this request is accepted.", { + agent: "reviewer", + reasoningEffort: "high", + contextTier: "long_context", + }); + call.catch(() => {}); + let settleTimer; + const stillPending = new Promise((resolve) => { + settleTimer = setTimeout(() => resolve(undefined), 3000); + settleTimer.unref?.(); + }); + try { + await Promise.race([call, stillPending]); + return { didThrow: false }; + } catch { + return { didThrow: true }; + } finally { + clearTimeout(settleTimer); + } + }, +}); + +const startsFromContextSession = defineWorkflow({ + meta: { + name: "starts-from-context-session", + description: "Try to start a workflow through the context session.", + phases: [], + }, + run: async ({ session }) => { + try { + await session.workflow.run("argument-echo"); + return "unexpectedly started a workflow"; + } catch (error) { + return error instanceof Error ? error.message : String(error); + } + }, +}); + +let session; + +const startsFromModuleSession = defineWorkflow({ + meta: { + name: "starts-from-module-session", + description: "Try to start a workflow through the module session.", + phases: [], + }, + run: async () => { + try { + await session.workflow.run("argument-echo"); + return "unexpectedly started a workflow"; + } catch (error) { + return error instanceof Error ? error.message : String(error); + } + }, +}); + +const parked = defineWorkflow({ + meta: { + name: "parked", + description: "Wait for a test-controlled release marker.", + phases: [], + }, + run: async () => { + writeFileSync(marker("entered"), "entered"); + await waitForMarker("release", 30_000); + return "released"; + }, +}); + +const failsOnce = defineWorkflow({ + meta: { + name: "fails-once", + description: "Fails its first attempt and succeeds when resumed.", + phases: [], + }, + run: async () => { + if (!existsSync(marker("fails-once-attempted"))) { + writeFileSync(marker("fails-once-attempted"), "attempted"); + throw new Error("first attempt failed"); + } + return "resumed"; + }, +}); + +const externallyPaused = defineWorkflow({ + meta: { + name: "externally-paused", + description: "Wait until the calling session pauses this run.", + phases: [], + }, + run: async ({ signal }) => { + writeFileSync(marker("external-pause-entered"), "entered"); + await new Promise((_, reject) => { + const abort = () => reject(signal.reason ?? new Error("Workflow aborted")); + if (signal.aborted) { + abort(); + return; + } + signal.addEventListener("abort", abort, { once: true }); + }); + return "unexpectedly completed"; + }, +}); + +const durablePauseCheckpoint = defineWorkflow({ + meta: { + name: "durable-pause-checkpoint", + description: "Pause once after journaled preparation, then complete after resume.", + phases: [], + }, + run: async ({ pause, step }) => { + const attempt = incrementMarker("checkpoint-attempts"); + const prepared = await step("prepare", () => incrementMarker("checkpoint-preparations")); + await pause("review-ready"); + return { attempt, prepared }; + }, +}); + +session = await joinSession({ + workflows: [ + argumentEcho, + arrayResult, + phased, + forwardsSubagentOptions, + startsFromContextSession, + startsFromModuleSession, + parked, + failsOnce, + externallyPaused, + durablePauseCheckpoint, + ], }); if (!session.workflow) { throw new Error("Workflow API was not registered"); } -writeFileSync(new URL("./ready", import.meta.url), "ready"); +void waitForMarker("start-b", 30_000) + .then(async () => { + const result = await session.workflow.run("argument-echo", { + args: { source: "module-watcher" }, + notifyOnComplete: false, + }); + writeFileSync(marker("b-result"), JSON.stringify({ status: "success", result })); + }) + .catch((error) => { + if (existsSync(marker("start-b"))) { + writeFileSync( + marker("b-result"), + JSON.stringify({ + status: "error", + error: error instanceof Error ? error.message : String(error), + }) + ); + } + }); + +writeFileSync(marker("ready"), "ready"); 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..70737a0781 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,68 @@ export class CapiProxy { async updateConfig(config: { filePath: string; workDir: string; + backend?: ReplayBackend; + replayOnly?: boolean; 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 +225,24 @@ 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); + } + + async setEntraLogin(subjectToken: string, githubToken: string): Promise { + const response = await fetch(`${this.proxyUrl}/entra-login-config`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ subjectToken, githubToken }), + }); + 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 new file mode 100644 index 0000000000..13fea3ef5a --- /dev/null +++ b/nodejs/test/e2e/harness/ahpClient.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; +import { once } from "node:events"; +import { pathToFileURL } from "node:url"; +import { + ActionType, + MessageKind, + PROTOCOL_VERSION, + ResponsePartKind, + ToolCallConfirmationReason, + ToolCallContributorKind, + ToolResultContentType, + type ClientCapabilities, + type RootState, + type SessionState, + type SessionActiveClient, + type StateAction, +} from "@microsoft/agent-host-protocol-v09"; +import { AhpClient, type Subscription } from "@microsoft/agent-host-protocol-v09/client"; +import { WebSocketTransport } from "@microsoft/agent-host-protocol-v09/ws"; +import WebSocket from "ws"; + +export async function withDeadline(promise: Promise, label: string, ms = 30_000): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timed out: ${label}`)), ms); + }), + ]); + } finally { + clearTimeout(timer); + } +} + +export async function connectAhp( + 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 }); + try { + await once(socket, "open"); + const transport = WebSocketTransport.fromSocket(socket as unknown as globalThis.WebSocket); + const client = new AhpClient(transport, { requestTimeoutMs: 15_000 }); + client.connect(); + // The initialize() convenience method does not forward capabilities. + const initialized = await client.request("initialize", { + channel: "ahp-root://", + clientId, + protocolVersions: [PROTOCOL_VERSION], + capabilities, + }); + assert.equal(initialized.protocolVersion, PROTOCOL_VERSION); + return { client, clientId, transport }; + } catch (error) { + socket.terminate(); + throw error; + } +} + +export async function authenticateAhp( + ahp: Awaited>, + githubToken: string +) { + assert(githubToken, "Session creation requires a GitHub credential"); + const { result: root } = await ahp.client.subscribe("ahp-root://"); + const agent = (root.snapshot?.state as RootState | undefined)?.agents.find( + (entry) => entry.provider === "copilot" + ); + const resource = agent?.protectedResources?.find( + (entry) => entry.resource_name === "GitHub API" + ); + assert(resource, "The Copilot agent must advertise its GitHub protected resource"); + await ahp.client.request("authenticate", { + channel: "ahp-root://", + resource: resource.resource, + token: githubToken, + }); +} + +export async function createAhpSession( + ahp: Awaited>, + workDir: string, + githubToken: string, + tools: SessionActiveClient["tools"] = [] +) { + await authenticateAhp(ahp, githubToken); + const sessionId = randomUUID(); + const sessionUri = `ahp-session:/${sessionId}`; + await ahp.client.request("createSession", { + channel: sessionUri, + provider: "copilot", + workingDirectories: [pathToFileURL(workDir).href], + activeClient: { clientId: ahp.clientId, displayName: "Runtime host E2E", tools }, + }); + const { result } = await ahp.client.subscribe(sessionUri); + const chatUri = (result.snapshot?.state as SessionState | undefined)?.defaultChat; + assert(chatUri, "AHP createSession must create a default chat"); + const { subscription } = await ahp.client.subscribe(chatUri); + return { sessionId, sessionUri, chatUri, subscription }; +} + +export async function streamedTurn( + client: AhpClient, + chatUri: string, + subscription: Subscription, + prompt: string, + model = "claude-sonnet-5", + onAction?: (action: StateAction) => void, + clientTools?: { clientId: string; handlers: Record string> } +) { + const turnId = randomUUID(); + const pendingClientCalls = new Map(); + const completedClientCalls = new Set(); + const parts = new Map(); + let deltas = 0; + client.dispatch(chatUri, { + type: ActionType.ChatTurnStarted, + turnId, + startedAt: new Date().toISOString(), + message: { text: prompt, origin: { kind: MessageKind.User }, model: { id: model } }, + }); + return withDeadline( + (async () => { + for await (const event of subscription) { + if (event.type !== "action") continue; + if (event.params.rejectionReason) throw new Error(event.params.rejectionReason); + const action = event.params.action; + if (!("turnId" in action) || action.turnId !== turnId) continue; + onAction?.(action); + if ( + clientTools && + action.type === ActionType.ChatToolCallStart && + action.contributor?.kind === ToolCallContributorKind.Client + ) { + assert.equal(action.contributor.clientId, clientTools.clientId); + assert( + clientTools.handlers[action.toolName], + `Unexpected client tool ${action.toolName}` + ); + assert(!pendingClientCalls.has(action.toolCallId)); + assert(!completedClientCalls.has(action.toolCallId)); + pendingClientCalls.set(action.toolCallId, action.toolName); + } else if ( + clientTools && + action.type === ActionType.ChatToolCallReady && + pendingClientCalls.has(action.toolCallId) + ) { + assert.equal(action.confirmed, ToolCallConfirmationReason.NotNeeded); + assert.equal(typeof action.toolInput, "string"); + const name = pendingClientCalls.get(action.toolCallId)!; + pendingClientCalls.delete(action.toolCallId); + completedClientCalls.add(action.toolCallId); + const text = clientTools.handlers[name](JSON.parse(action.toolInput as string)); + client.dispatch(chatUri, { + type: ActionType.ChatToolCallComplete, + turnId, + toolCallId: action.toolCallId, + result: { + success: true, + pastTenseMessage: `Ran ${name}`, + content: [{ type: ToolResultContentType.Text, text }], + }, + }); + } + if ( + action.type === ActionType.ChatResponsePart && + action.part.kind === ResponsePartKind.Markdown + ) { + parts.set(action.part.id, action.part.content); + } else if (action.type === ActionType.ChatDelta) { + deltas++; + parts.set(action.partId, (parts.get(action.partId) ?? "") + action.content); + } else if (action.type === ActionType.ChatError) { + throw new Error(action.part.error.message); + } else if (action.type === ActionType.ChatTurnComplete) { + return { text: [...parts.values()].join("").trim(), deltas }; + } + } + throw new Error("AHP subscription closed before chat/turnComplete"); + })(), + "streamed AHP turn" + ); +} diff --git a/nodejs/test/e2e/harness/ahpTestDriver.ts b/nodejs/test/e2e/harness/ahpTestDriver.ts new file mode 100644 index 0000000000..2fc186dfb8 --- /dev/null +++ b/nodejs/test/e2e/harness/ahpTestDriver.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { setTimeout as delay } from "node:timers/promises"; +import { connect } from "node:net"; +import { createInterface } from "node:readline"; +import { ActionType, type ChatState, type SessionState } from "@microsoft/agent-host-protocol-v09"; +import { authenticateAhp, connectAhp, createAhpSession, streamedTurn } from "./ahpClient.js"; + +export type AhpTestCommand = + | { + op: "connect"; + url: string; + token?: string; + githubToken: string; + clientId?: string; + } + | { op: "create"; clientId: string; workDir: string; clientTools?: boolean } + | { op: "attach"; clientId: string; sessionId: string; clientTools?: boolean } + | { + op: "turn"; + clientId: string; + sessionId: string; + prompt: string; + clientTools?: boolean; + } + | { op: "stopped"; clientId: string; url: string } + | { op: "close"; clientId: string }; + +const echoTool = { + name: "client_echo", + description: "Echoes text from the AHP client", + inputSchema: { + type: "object" as const, + properties: { text: { type: "string" } }, + required: ["text"], + }, +}; + +type Connection = Awaited>; +type Subscription = Awaited>["subscription"]; + +/** Test-side adapter around the standard AHP 0.9 client; no CAPI proxy involvement. */ +export class AhpTestDriver { + private connections = new Map< + string, + { + ahp: Connection; + githubToken: string; + chats: Map; + } + >(); + + async request(command: AhpTestCommand): Promise { + if (command.op === "connect") { + const url = new URL(command.url); + assert( + ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname), + "AHP fixtures must use a local listener" + ); + const ahp = await connectAhp(command, undefined, command.clientId); + try { + assert(!this.connections.has(ahp.clientId), "Close the previous AHP client first"); + await authenticateAhp(ahp, command.githubToken); + this.connections.set(ahp.clientId, { + ahp, + githubToken: command.githubToken, + chats: new Map(), + }); + return { clientId: ahp.clientId }; + } catch (error) { + await ahp.client.shutdown(); + throw error; + } + } + const connection = this.connections.get(command.clientId); + assert(connection, "Unknown AHP test connection"); + const { ahp, chats } = connection; + switch (command.op) { + case "create": { + const session = await createAhpSession( + ahp, + command.workDir, + connection.githubToken, + command.clientTools ? [echoTool] : [] + ); + chats.set(session.sessionId, { + uri: session.chatUri, + subscription: session.subscription, + }); + return { sessionId: session.sessionId, sessionUri: session.sessionUri }; + } + case "attach": { + const uri = `ahp-session:/${command.sessionId}`; + const listed = await ahp.client.request("listSessions", { + channel: "ahp-root://", + }); + assert( + listed.items.some((item) => item.resource === uri), + "Session must be discoverable" + ); + const session = await ahp.client.subscribe(uri); + const state = session.result.snapshot?.state as SessionState | undefined; + assert.equal(state?.lifecycle, "ready"); + assert(state?.defaultChat, "Attached session must have a default chat"); + const chat = await ahp.client.subscribe(state.defaultChat); + chats.set(command.sessionId, { + uri: state.defaultChat, + subscription: chat.subscription, + }); + if (command.clientTools) { + ahp.client.dispatch(uri, { + type: ActionType.SessionActiveClientSet, + activeClient: { + clientId: ahp.clientId, + displayName: "Resumed tool owner", + tools: [echoTool], + }, + }); + await waitFor(async () => { + const { result } = await ahp.client.subscribe(uri); + const active = (result.snapshot?.state as SessionState).activeClients; + return active.some((client) => client.clientId === ahp.clientId); + }, "active AHP client"); + } + return { history: (chat.result.snapshot?.state as ChatState).turns }; + } + case "turn": { + const target = chats.get(command.sessionId); + assert(target, "Create or attach the session before sending a turn"); + let clientToolCalls = 0; + const response = await streamedTurn( + ahp.client, + target.uri, + target.subscription, + command.prompt, + "claude-sonnet-5", + undefined, + command.clientTools + ? { + clientId: ahp.clientId, + handlers: { + client_echo: (input) => { + assert.deepEqual(input, { text: "ping" }); + clientToolCalls++; + return "CLIENT_ECHO_ping"; + }, + }, + } + : undefined + ); + return { ...response, clientToolCalls }; + } + case "stopped": + await waitFor(() => ahp.transport.lastClose !== null, "AHP client disconnect"); + await assertListenerClosed(command.url); + return {}; + case "close": + this.connections.delete(command.clientId); + await ahp.client.shutdown(); + return {}; + default: + throw new Error("Unknown AHP test command"); + } + } + + async close(): Promise { + const pending = [...this.connections.values()]; + this.connections.clear(); + await Promise.all(pending.map(({ ahp }) => ahp.client.shutdown())); + } +} + +async function waitFor(condition: () => boolean | Promise, label: string): Promise { + const deadline = Date.now() + 30_000; + while (!(await condition())) { + assert(Date.now() < deadline, `Timed out: ${label}`); + await delay(10); + } +} + +async function assertListenerClosed(address: string): Promise { + const url = new URL(address); + const refusal = await new Promise((resolve, reject) => { + const socket = connect({ + host: url.hostname.replace(/^\[|\]$/g, ""), + port: Number(url.port), + }); + socket.setTimeout(5_000, () => { + socket.destroy(); + reject(new Error("Timed out probing stopped listener")); + }); + socket.once("connect", () => { + socket.destroy(); + resolve(undefined); + }); + socket.once("error", (error: NodeJS.ErrnoException) => resolve(error.code)); + }); + assert.equal(refusal, "ECONNREFUSED", "Listener must close, not merely reject authentication"); +} + +const driver = new AhpTestDriver(); +const input = createInterface({ input: process.stdin, crlfDelay: Infinity }); +process.stdout.write(JSON.stringify({ ready: true }) + "\n"); +try { + for await (const line of input) { + try { + const result = await driver.request(JSON.parse(line) as AhpTestCommand); + process.stdout.write(JSON.stringify({ result }) + "\n"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stdout.write(JSON.stringify({ error: message }) + "\n"); + } + } +} finally { + await driver.close(); +} 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/runtimeHost.ts b/nodejs/test/e2e/harness/runtimeHost.ts new file mode 100644 index 0000000000..27d39d0221 --- /dev/null +++ b/nodejs/test/e2e/harness/runtimeHost.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { accessSync, constants, realpathSync } from "node:fs"; +import { isAbsolute, sep } from "node:path"; +import type { AhpHost } from "../../../src/index.js"; +import { waitForCondition } from "./sdkTestHelper.js"; +import { candidateHostArtifacts } from "./runtimeHostCandidate.js"; +import type { connectAhp } from "./ahpClient.js"; +import { assertListenerClosed } from "./runtimeHostTopology.js"; +export { assertRuntimeListener } from "./runtimeHostTopology.js"; +export { + authenticateAhp, + connectAhp, + createAhpSession, + streamedTurn, + withDeadline, +} from "./ahpClient.js"; + +export function localHostArtifacts() { + const candidate = process.env.COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST; + if (candidate) return candidateHostArtifacts(candidate); + function artifact(name: string, executable = true): string { + const value = process.env[name]; + assert(value && isAbsolute(value), `${name} must name an absolute, locally built artifact`); + const path = realpathSync(value); + assert( + !path.split(sep).includes("node_modules"), + `${name} must not use a released runtime package` + ); + accessSync(path, executable ? constants.X_OK : constants.R_OK); + return path; + } + const artifacts = { + runtimePath: artifact("COPILOT_CLI_PATH"), + providerPath: artifact("COPILOT_RUNTIME_PROVIDER_LIB", false), + }; + return { + ...artifacts, + bundled: false, + env: { + COPILOT_RUNTIME_PROVIDER_LIB: artifacts.providerPath, + }, + }; +} + +export async function assertProcessStopped(pid: number, label: string) { + await waitForCondition( + () => { + try { + process.kill(pid, 0); + return false; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; + return true; + } + }, + { timeoutMessage: `${label} was not reaped` } + ); +} + +export async function assertHostStopped( + host: AhpHost, + ahp: Awaited>, + runtimePid: number | false +) { + await waitForCondition(() => ahp.transport.lastClose !== null, { + timeoutMessage: "Existing AHP client was not disconnected", + }); + await assertListenerClosed(host, runtimePid); +} diff --git a/nodejs/test/e2e/harness/runtimeHostCandidate.ts b/nodejs/test/e2e/harness/runtimeHostCandidate.ts new file mode 100644 index 0000000000..ae24d68ac2 --- /dev/null +++ b/nodejs/test/e2e/harness/runtimeHostCandidate.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { + accessSync, + closeSync, + constants, + openSync, + readFileSync, + readSync, + realpathSync, +} from "node:fs"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getRuntimePlatform, materializeRuntimeBundle } from "../../../src/runtimeArtifacts.js"; +import { + validateCandidateSources, + type CandidateSources, +} from "../../../../samples/runtime-host/candidate-sources.js"; + +/** Local-build attestation accompanying an assembled, unpublished runtime package. */ +interface LocalHostCandidate { + schemaVersion: 3; + kind: "local-runtime-host-candidate"; + packageRoot: string; + platform: string; + sources: CandidateSources; + artifacts: Record<"runtime" | "provider", { path: string; sourcePath: string; sha256: string }>; +} + +function inside(root: string, path: string): boolean { + const suffix = relative(root, path); + return ( + suffix !== "" && !isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`) + ); +} + +function sha256(path: string): string { + const hash = createHash("sha256"); + const descriptor = openSync(path, "r"); + try { + const buffer = Buffer.alloc(1024 * 1024); + let length: number; + while ((length = readSync(descriptor, buffer, 0, buffer.length, null)) !== 0) { + hash.update(buffer.subarray(0, length)); + } + return hash.digest("hex"); + } finally { + closeSync(descriptor); + } +} + +export function candidateHostArtifacts(manifestPath: string) { + assert(isAbsolute(manifestPath), "Candidate manifest path must be absolute"); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as LocalHostCandidate; + assert.equal( + manifest.schemaVersion, + 3, + "Restage candidates with linked-library source provenance" + ); + assert.equal(manifest.kind, "local-runtime-host-candidate"); + assert.equal(manifest.platform, getRuntimePlatform()); + assert(isAbsolute(manifest.packageRoot), "Candidate package root must be absolute"); + const packageRoot = realpathSync(manifest.packageRoot); + const sourceRoots = validateCandidateSources( + manifest.sources, + fileURLToPath(new URL("../../../../", import.meta.url)) + ); + + const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")); + assert.equal(packageJson.copilotRuntime?.sourceRepository, manifest.sources.runtime.repository); + assert.equal(packageJson.copilotRuntime?.sourceSha, manifest.sources.runtime.commit); + assert( + [ + `@github/copilot-${manifest.platform}`, + `@github/copilot-sdk-${manifest.platform}`, + ].includes(packageJson.name), + "Candidate must be an assembled platform runtime package" + ); + + const prebuilds = join("prebuilds", manifest.platform); + for (const [name, filename, source] of [ + ["runtime", "copilot-runtime", "runtime"], + ["provider", "runtime.node", "runtime"], + ] as const) { + const artifact = manifest.artifacts[name]; + assert.equal(artifact.path, join(prebuilds, filename)); + assert.match(artifact.sha256, /^[a-f0-9]{64}$/, `${name} needs a SHA-256 checksum`); + assert(isAbsolute(artifact.sourcePath), `${name} source artifact must be absolute`); + const sourcePath = realpathSync(artifact.sourcePath); + assert( + inside(sourceRoots[source], sourcePath), + `${name} must come from its local checkout` + ); + assert( + !sourcePath.includes(`${sep}node_modules${sep}`), + `${name} cannot be a released build` + ); + const stagedPath = realpathSync(resolve(packageRoot, artifact.path)); + assert(inside(packageRoot, stagedPath), `${name} must be inside the candidate package`); + assert.equal(sha256(sourcePath), artifact.sha256, `${name} local build checksum mismatch`); + assert.equal(sha256(stagedPath), artifact.sha256, `${name} candidate checksum mismatch`); + } + + // Exercise the SDK's real materializer, without a companion host executable. + const runtimePath = materializeRuntimeBundle( + { packageRoot, platform: manifest.platform }, + join(dirname(manifestPath), ".runtime-host-materialized") + ); + const providerPath = join(dirname(runtimePath), "runtime.node"); + for (const [name, path] of [ + ["runtime", runtimePath], + ["provider", providerPath], + ] as const) { + assert.equal( + sha256(path), + manifest.artifacts[name].sha256, + `${name} materialization mismatch` + ); + accessSync(path, name === "provider" ? constants.R_OK : constants.X_OK); + } + return { + runtimePath, + providerPath, + bundled: true, + // Undefined values are omitted by Node's child_process.spawn; empty + // strings would still select the runtime's development-override branch. + env: { + COPILOT_CLI_PATH: undefined, + COPILOT_RUNTIME_PROVIDER_LIB: undefined, + }, + }; +} diff --git a/nodejs/test/e2e/harness/runtimeHostTopology.ts b/nodejs/test/e2e/harness/runtimeHostTopology.ts new file mode 100644 index 0000000000..1676af9e10 --- /dev/null +++ b/nodejs/test/e2e/harness/runtimeHostTopology.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +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 }; + +/** Verify the listener's socket and loaded provider, independently of the RPC PID. */ +export async function assertRuntimeListener( + host: Listener, + 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"); + const command = await readFile(`/proc/${runtimePid}/cmdline`, "utf8"); + assert(!command.includes("copilotd"), "No companion host process"); + if (host.token) assert(!command.includes(host.token), "Listener tokens must not enter argv"); + if (artifacts.bundled) { + const environment = (await readFile(`/proc/${runtimePid}/environ`, "utf8")).split("\0"); + for (const name of ["COPILOT_RUNTIME_PROVIDER_LIB"]) { + assert( + !environment.some((entry) => entry.startsWith(`${name}=`)), + `Candidate runtime must not receive the ${name} development override` + ); + } + } + + const descendants = [String(runtimePid)]; + while (descendants.length > 0) { + const parent = descendants.pop()!; + const children = new Set(); + const tasks = await readdir(`/proc/${parent}/task`).catch( + (error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") throw error; + return []; + } + ); + for (const tid of tasks) { + const list = await readFile(`/proc/${parent}/task/${tid}/children`, "utf8").catch( + (error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") throw error; + return ""; + } + ); + for (const pid of list.trim().split(/\s+/).filter(Boolean)) children.add(pid); + } + for (const pid of children) { + try { + const childCommand = await readFile(`/proc/${pid}/cmdline`, "utf8"); + assert( + !/copilotd|copilot-runtime/.test(childCommand), + "No host or second runtime child" + ); + // Embedded CLI tools may also be Node processes; only one may load the provider. + if (!artifacts.embedded) { + assert.notEqual(await readlink(`/proc/${pid}/exe`), artifacts.runtimePath); + } + const childMaps = await readFile(`/proc/${pid}/maps`, "utf8"); + assert( + !childMaps.includes(artifacts.providerPath), + "No second provider-loaded runtime" + ); + descendants.push(pid); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + } + } + + 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( + (error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") throw error; + return ""; + } + ); + const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1]; + if (inode) sockets.add(inode); + } + const tables = await Promise.all( + ["tcp", "tcp6"].map((name) => readFile(`/proc/${runtimePid}/net/${name}`, "utf8")) + ); + 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({ + host: url.hostname.replace(/^\[|\]$/g, ""), + port: Number(url.port), + }); + socket.setTimeout(5_000, () => { + socket.destroy(); + reject(new Error("Timed out probing stopped listener")); + }); + socket.once("connect", () => { + socket.destroy(); + resolve(undefined); + }); + socket.once("error", (error: NodeJS.ErrnoException) => resolve(error.code)); + }); + assert.equal(refusal, "ECONNREFUSED", "Listener must close, not merely reject authentication"); + if (runtimePid !== false) process.kill(runtimePid, 0); +} 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 4bd25fe5c3..d4fec0a4eb 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,15 @@ export async function createSdkTestContext({ logLevel, useStdio, copilotClientOptions, + replayOnly = process.env.COPILOT_SDK_E2E_REPLAY_ONLY === "true", + modelNames, }: { logLevel?: "error" | "none" | "warning" | "info" | "debug" | "all"; cliPath?: string; useStdio?: boolean; copilotClientOptions?: CopilotClientOptions; + replayOnly?: boolean; + modelNames?: Record; } = {}) { const homeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-config-"))); const copilotHomeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-home-"))); @@ -74,6 +81,7 @@ export async function createSdkTestContext({ const proxyUrl = await openAiEndpoint.start(); await openAiEndpoint.setCopilotUserByToken(DEFAULT_GITHUB_TOKEN, { login: "e2e-test-user", + id: 12345, copilot_plan: "individual_pro", is_mcp_enabled: true, endpoints: { @@ -120,7 +128,7 @@ export async function createSdkTestContext({ if (userConn) { // Caller supplied a RuntimeConnection — merge in the harness-managed // CLI path (and stay on the same transport variant). Strip `kind` - // before forwarding to the factory opts since the factories don't + // before forwarding to the workflow opts since the workflows don't // accept it in their argument shape. if (userConn.kind === "tcp") { const { kind: _k, ...tcp } = userConn; @@ -158,6 +166,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). @@ -209,7 +227,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 @@ -223,7 +241,7 @@ export async function createSdkTestContext({ env: effectiveInProcess ? undefined : mergedEnv, logLevel: logLevel || "error", connection: effectiveConnection, - gitHubToken: authTokenToUse, + gitHubToken: effectiveConnection.kind === "uri" ? undefined : authTokenToUse, ...rest, }); } @@ -244,8 +262,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 @@ -268,8 +319,10 @@ export async function createSdkTestContext({ } await openAiEndpoint.updateConfig({ + replayOnly, filePath: getTrafficCapturePath(testContext), workDir, + modelNames, testInfo: { file: testContext.task.file.filepath, line: testContext.task.location?.line, @@ -292,13 +345,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/harness/webSocketResponsesServer.ts b/nodejs/test/e2e/harness/webSocketResponsesServer.ts new file mode 100644 index 0000000000..219100a97f --- /dev/null +++ b/nodejs/test/e2e/harness/webSocketResponsesServer.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import http from "node:http"; +import { WebSocketServer } from "ws"; + +export type WebSocketEnvelope = Record; +type WebSocketReply = WebSocketEnvelope | (() => Promise); + +/** Controlled provider terminals cannot be reproduced by ordinary successful-conversation replay. */ +export async function startWebSocketResponsesServer() { + const state = { + messages: [] as Array>, + messageConnectionIds: [] as number[], + responses: [] as WebSocketReply[], + openConnectionIds: new Set(), + connectionCount: 0, + closedConnectionCount: 0, + }; + const errors: unknown[] = []; + const forwardedRequests = new Set(); + const forwardedResponses = new Set(); + const forwardingAgent = new http.Agent(); + let stopping = false; + let httpTargetUrl: string | undefined; + const server = http.createServer((req, res) => { + if (stopping) { + res.destroy(); + return; + } + if (req.url === "/models") { + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify({ + data: [ + { + id: "gpt-5-responses", + name: "GPT-5 Responses", + supported_endpoints: ["/responses", "ws:/responses"], + capabilities: { + supports: { streaming: true, tool_calls: true }, + limits: { + max_context_window_tokens: 128000, + max_output_tokens: 16384, + }, + }, + }, + ], + }) + ); + return; + } + if (!httpTargetUrl) { + res.writeHead(503); + res.end("The test CAPI proxy has not been attached."); + return; + } + // Absolute-form request paths must not replace the configured proxy origin. + const target = new URL(httpTargetUrl); + const upstream = http.request( + { + protocol: target.protocol, + hostname: target.hostname, + port: target.port, + path: req.url ?? "/", + method: req.method, + headers: { ...req.headers, host: target.host }, + agent: forwardingAgent, + }, + (response) => { + forwardedResponses.add(response); + response.once("close", () => forwardedResponses.delete(response)); + response.on("error", (error) => { + if (!stopping) errors.push(error); + res.destroy(); + }); + if (stopping) { + response.destroy(); + return; + } + res.writeHead(response.statusCode ?? 500, response.headers); + response.pipe(res); + } + ); + forwardedRequests.add(upstream); + upstream.once("close", () => forwardedRequests.delete(upstream)); + upstream.on("error", (error) => { + if (stopping) return; + errors.push(error); + if (!res.headersSent) res.writeHead(502); + res.end(String(error)); + }); + req.pipe(upstream); + }); + const sockets = new WebSocketServer({ server }); + sockets.on("connection", (socket) => { + const connectionId = ++state.connectionCount; + state.openConnectionIds.add(connectionId); + socket.on("close", () => { + state.closedConnectionCount++; + state.openConnectionIds.delete(connectionId); + }); + socket.on("error", (error) => errors.push(error)); + socket.on("message", (raw) => { + void (async () => { + state.messages.push(JSON.parse(raw.toString()) as Record); + state.messageConnectionIds.push(connectionId); + const reply = state.responses.shift(); + if (!reply) { + throw new Error("Unexpected WebSocket response.create request."); + } + const envelope = typeof reply === "function" ? await reply() : reply; + if (stopping) return; + await new Promise((resolve, reject) => { + socket.send(JSON.stringify(envelope), (error) => + error ? reject(error) : resolve() + ); + }); + })().catch((error: unknown) => { + errors.push(error); + socket.terminate(); + }); + }); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Expected a bound TCP address for the WebSocket provider."); + } + return { + baseUrl: `http://127.0.0.1:${address.port}`, + state, + forwardHttpTo(url: string) { + httpTargetUrl = url; + }, + async stop() { + stopping = true; + const forwarded = [...forwardedRequests, ...forwardedResponses]; + const forwardedClosed = Promise.all( + forwarded.map( + (stream) => new Promise((resolve) => stream.once("close", resolve)) + ) + ); + for (const stream of forwarded) stream.destroy(); + forwardingAgent.destroy(); + for (const socket of sockets.clients) socket.terminate(); + await new Promise((resolve, reject) => + sockets.close((error) => (error ? reject(error) : resolve())) + ); + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + await forwardedClosed; + if (errors.length > 0) + throw new AggregateError(errors, "WebSocket provider fixture failed."); + }, + }; +} + +export function createCompletedResponse(text: string, id: string) { + return { + id, + object: "response", + model: "gpt-5-responses", + status: "completed", + output: [ + { + type: "message", + id: "msg-1", + role: "assistant", + content: [{ type: "output_text", text, annotations: [] }], + status: "completed", + }, + ], + output_text: text, + usage: null, + }; +} diff --git a/nodejs/test/e2e/hooks.e2e.test.ts b/nodejs/test/e2e/hooks.e2e.test.ts index 4fce7d2acf..05baed6acf 100644 --- a/nodejs/test/e2e/hooks.e2e.test.ts +++ b/nodejs/test/e2e/hooks.e2e.test.ts @@ -5,17 +5,25 @@ import { readFile, writeFile } from "fs/promises"; import { join } from "path"; import { describe, expect, it } from "vitest"; +import { z } from "zod"; import type { + CopilotClient, + PermissionRequest, PreToolUseHookInput, PreToolUseHookOutput, PostToolUseHookInput, PostToolUseHookOutput, } from "../../src/index.js"; -import { approveAll } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; +import { + createSdkTestContext, + getLegacyCliPathForTests, + isInProcessTransport, +} from "./harness/sdkTestContext.js"; describe("Session hooks", async () => { - const { copilotClient: client, workDir } = await createSdkTestContext(); + const ctx = await createSdkTestContext(); + const { copilotClient: client, workDir } = ctx; it("should invoke preToolUse hook when model runs a tool", async () => { const preToolUseInputs: PreToolUseHookInput[] = []; @@ -161,4 +169,78 @@ describe("Session hooks", async () => { await session.disconnect(); }); + + // Disconnecting the last owner tears the session down, so each resume must register + // the SDK's hooks with the freshly restored hook service. + async function expectPreToolUseAfterResume(target: CopilotClient): Promise { + const preToolUseInputs: PreToolUseHookInput[] = []; + const permissionRequests: PermissionRequest[] = []; + const sessionOptions = () => ({ + tools: [ + defineTool("encrypt_string", { + description: "Encrypts a string", + parameters: z.object({ + input: z.string().describe("String to encrypt"), + }), + handler: ({ input }: { input: string }) => input.toUpperCase(), + }), + ], + // Records rather than denies so a regression surfaces as a clear assertion + // failure instead of a model-dependent denial transcript. + onPermissionRequest: (request: PermissionRequest) => { + permissionRequests.push(request); + return { kind: "approve-once" } as const; + }, + hooks: { + onPreToolUse: async (input: PreToolUseHookInput) => { + preToolUseInputs.push(input); + return { permissionDecision: "allow" } as PreToolUseHookOutput; + }, + }, + }); + + let sessionId: string; + { + await using session1 = await target.createSession(sessionOptions()); + sessionId = session1.sessionId; + await session1.sendAndWait({ + prompt: "Use encrypt_string to encrypt this string: Hello", + }); + expect(preToolUseInputs.map((input) => input.toolName)).toEqual(["encrypt_string"]); + } + + await using session2 = await target.resumeSession(sessionId, sessionOptions()); + const answer = await session2.sendAndWait({ + prompt: "Use encrypt_string to encrypt this string: World", + }); + + expect(preToolUseInputs.map((input) => input.toolName)).toEqual([ + "encrypt_string", + "encrypt_string", + ]); + expect(permissionRequests).toEqual([]); + // Validate the final assistant response arrived (guards against truncated captures) + expect(answer?.data.content).toContain("WORLD"); + } + + it("should invoke preToolUse hook for a custom tool after disconnect and resume", async () => { + await expectPreToolUseAfterResume(client); + }); + + // The legacy JavaScript CLI host tears sessions down through a separate path. + it.skipIf(isInProcessTransport)( + "should invoke preToolUse hook for a custom tool after disconnect and resume on the legacy CLI", + async () => { + const legacyClient = ctx.createClient({ + connection: RuntimeConnection.forStdio({ + path: await getLegacyCliPathForTests(), + }), + }); + try { + await expectPreToolUseAfterResume(legacyClient); + } finally { + await legacyClient.stop(); + } + } + ); }); diff --git a/nodejs/test/e2e/hydrafusion_max.e2e.test.ts b/nodejs/test/e2e/hydrafusion_max.e2e.test.ts new file mode 100644 index 0000000000..a4a7c52c93 --- /dev/null +++ b/nodejs/test/e2e/hydrafusion_max.e2e.test.ts @@ -0,0 +1,266 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { + approveAll, + CopilotRequestHandler, + RuntimeConnection, + type SessionEvent, +} from "../../src/index.js"; +import { + createSdkTestContext, + DEFAULT_GITHUB_TOKEN, + getLegacyCliPathForTests, +} from "./harness/sdkTestContext.js"; +import { isByokBackend } from "./harness/testBackend.js"; + +const MODEL = "gpt-5.6-sol"; +const REPLY = "SDK_HYDRAFUSION_MAX_DONE: Au"; + +class FusionRequestHandler extends CopilotRequestHandler { + readonly plans: unknown[] = []; + readonly inference: unknown[] = []; + + protected override async sendRequest(request: Request): Promise { + const path = new URL(request.url).pathname; + if (path === "/models") { + return Response.json({ + data: [ + { + id: MODEL, + name: MODEL, + model_picker_enabled: true, + policy: { state: "enabled" }, + supported_endpoints: ["/chat/completions"], + capabilities: { + supports: { streaming: true, tool_calls: true, vision: true }, + limits: { + max_prompt_tokens: 128000, + max_output_tokens: 16384, + max_context_window_tokens: 144384, + }, + }, + }, + ], + }); + } + if (path === "/model/fusion") { + this.plans.push(await request.json()); + return Response.json({ + fusion_mode: "hydrafusion-max", + fusion_pattern: "solo", + plan_version: "2", + steps: [{ role: "generation", model_id: MODEL }], + session: { token: "jwt.fake.sdk.max.plan", expires_at: 0 }, + }); + } + if (path === "/chat/completions") { + const body = (await request.json()) as { model?: string; stream?: boolean }; + this.inference.push(body); + if (body.model !== MODEL) { + throw new Error(`Expected concrete Fusion constituent, received ${body.model}`); + } + const completion = { + id: "chatcmpl-sdk-max", + object: "chat.completion", + created: 0, + model: MODEL, + choices: [ + { + index: 0, + message: { role: "assistant", content: REPLY }, + finish_reason: "stop", + }, + ], + usage: { prompt_tokens: 11, completion_tokens: 7, total_tokens: 18 }, + }; + if (!body.stream) return Response.json(completion); + return new Response( + `data: ${JSON.stringify({ + ...completion, + object: "chat.completion.chunk", + choices: [ + { + index: 0, + delta: { role: "assistant", content: REPLY }, + finish_reason: "stop", + }, + ], + })}\n\ndata: [DONE]\n\n`, + { headers: { "content-type": "text/event-stream" } } + ); + } + if (path === "/models/session/intent") { + return Response.json({ error: "Intent unavailable in this fixture" }, { status: 404 }); + } + if (path === "/models/session") { + return Response.json({ + available_models: [MODEL], + selected_model: MODEL, + session_token: "jwt.fake.sdk.max.routing", + expires_at: 0, + }); + } + throw new Error(`Unexpected model request: ${request.method} ${path}`); + } +} + +describe.skipIf(isByokBackend).each([false, true])( + "HydraFusion Max plan-v2 gate: %s", + async (enabled) => { + const handler = new FusionRequestHandler(); + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + gitHubToken: DEFAULT_GITHUB_TOKEN, + requestHandler: handler, + env: { + HYDRAFUSION: "true", + HYDRAFUSION_ROLLOUT: "true", + HYDRAFUSION_PLAN_V2: String(enabled), + COPILOT_EXP_COPILOT_CLI_HYDRAFUSION_PLAN_V2: String(enabled), + }, + }, + }); + + if (!enabled) { + it.each([false, true])( + "rejects public SDK creation before inference (provider=%s)", + async (providerPresent) => { + await expect( + client.createSession({ + model: "hydrafusion-max", + enableExperimentalMode: true, + workingDirectory: workDir, + onPermissionRequest: approveAll, + ...(providerPresent + ? { + provider: { + baseUrl: "https://provider.example.invalid", + apiKey: "fixture-key", + }, + } + : {}), + }) + ).rejects.toThrow('Model "hydrafusion-max" is not available.'); + expect(handler.plans).toEqual([]); + expect(handler.inference).toEqual([]); + } + ); + + describe.skipIf(isByokBackend).each(["native", "legacy"] as const)( + "HydraFusion Max session flags (%s)", + async (transport) => { + const handler = new FusionRequestHandler(); + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio({ + path: + transport === "legacy" + ? await getLegacyCliPathForTests() + : process.env.COPILOT_CLI_PATH, + }), + gitHubToken: DEFAULT_GITHUB_TOKEN, + requestHandler: handler, + env: { + HYDRAFUSION: "true", + HYDRAFUSION_ROLLOUT: "true", + HYDRAFUSION_PLAN_V2: "false", + COPILOT_EXP_COPILOT_CLI_HYDRAFUSION_PLAN_V2: "false", + }, + }, + }); + + it("enables Max for one session without enabling it for the host", async () => { + const options = { + model: "hydrafusion-max", + enableExperimentalMode: true, + workingDirectory: workDir, + onPermissionRequest: approveAll, + }; + const session = await client.createSession({ + ...options, + featureFlags: { + HYDRAFUSION: true, + HYDRAFUSION_ROLLOUT: true, + HYDRAFUSION_PLAN_V2: true, + }, + }); + const events: SessionEvent[] = []; + const unsubscribe = session.on((event) => events.push(event)); + try { + const reply = await session.sendAndWait({ + prompt: "What is the chemical symbol for gold? Do not use tools.", + }); + expect(reply?.data.content).toBe(REPLY); + expect(handler.plans).toEqual([ + expect.objectContaining({ + fusion_mode: "hydrafusion-max", + plan_version: "2", + }), + ]); + expect(events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "session.fusion_resolved", + data: expect.objectContaining({ + syntheticModel: "hydrafusion-max", + policy: "max", + planVersion: "2", + }), + }), + ]) + ); + await expect(client.createSession(options)).rejects.toThrow( + 'Model "hydrafusion-max" is not available.' + ); + } finally { + unsubscribe(); + await session.disconnect(); + } + }); + } + ); + } else { + it("creates Max through the public SDK and executes a concrete v2 plan", async () => { + const session = await client.createSession({ + model: "hydrafusion-max", + enableExperimentalMode: true, + workingDirectory: workDir, + onPermissionRequest: approveAll, + }); + const events: SessionEvent[] = []; + const unsubscribe = session.on((event) => events.push(event)); + try { + const reply = await session.sendAndWait({ + prompt: "What is the chemical symbol for gold? Do not use tools.", + }); + expect(reply?.data.content).toBe(REPLY); + expect(handler.plans).toEqual([ + expect.objectContaining({ + fusion_mode: "hydrafusion-max", + plan_version: "2", + }), + ]); + expect(handler.inference.length).toBeGreaterThan(0); + expect(events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "session.fusion_resolved", + data: expect.objectContaining({ + syntheticModel: "hydrafusion-max", + policy: "max", + planVersion: "2", + }), + }), + ]) + ); + } finally { + unsubscribe(); + await session.disconnect(); + } + }); + } + } +); diff --git a/nodejs/test/e2e/managed_permissions_limit_to.e2e.test.ts b/nodejs/test/e2e/managed_permissions_limit_to.e2e.test.ts new file mode 100644 index 0000000000..1d02686699 --- /dev/null +++ b/nodejs/test/e2e/managed_permissions_limit_to.e2e.test.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { writeFile } from "node:fs/promises"; +import { createServer, type Server } from "node:http"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +function closeServer(server: Server): Promise { + server.closeAllConnections(); + return new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); +} + +describe("Managed permissions limitTo", async () => { + const { copilotClient: client, workDir } = await createSdkTestContext(); + + it.skipIf(process.platform !== "darwin")( + "admits only the configured dynamic loopback destination", + async () => { + const requestedHosts: string[] = []; + const server = createServer((request, response) => { + requestedHosts.push(request.headers.host?.split(":")[0] ?? ""); + response.end("LIMIT_TO_SERVER_OK"); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "0.0.0.0", () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + expect(address && typeof address !== "string").toBe(true); + const port = typeof address === "object" && address ? address.port : 0; + + await writeFile(join(workDir, "limit-to-blocked-marker.txt"), "LIMIT_TO_BLOCKED_OK"); + try { + const session = await client.createSession({ + managedSettings: { + permissions: { + limitTo: ["Domain(127.0.0.1)"], + }, + }, + onPermissionRequest: () => ({ kind: "approve-once" }), + }); + try { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + failIfUnavailable: true, + allowBypass: false, + allowDevToolAccess: false, + learningMode: "deny", + sandboxMcpServers: true, + sandboxLspServers: true, + userPolicy: { + network: { + allowLocalNetwork: true, + allowedHosts: ["127.0.0.1"], + blockedHosts: [], + }, + }, + }, + }); + expect(update.success).toBe(true); + await session.rpc.tools.initializeAndValidate(); + const admitted = await session.rpc.tools.execute({ + name: "bash", + arguments: { + command: + `host=127.0.0.1; curl --disable --silent --show-error --fail --max-time 5 ` + + `--proxy "$HTTP_PROXY" "http://$host:${port}/probe"`, + description: "Probe the admitted dynamic loopback destination", + }, + toolCallId: "limit-to-admitted", + }); + expect(admitted).toMatchObject({ + resultType: "success", + textResultForLlm: expect.stringContaining("LIMIT_TO_SERVER_OK"), + }); + + const blocked = await session.rpc.tools.execute({ + name: "bash", + arguments: { + command: + `host=127.0.0.2; curl --disable --silent --show-error --fail --max-time 5 ` + + `--noproxy "*" "http://$host:${port}/probe" || ` + + "cat limit-to-blocked-marker.txt", + description: "Probe a blocked dynamic loopback destination", + }, + toolCallId: "limit-to-blocked", + }); + expect(blocked).toMatchObject({ + resultType: "success", + textResultForLlm: expect.stringContaining("LIMIT_TO_BLOCKED_OK"), + }); + expect(requestedHosts).toEqual(["127.0.0.1"]); + } finally { + await session.disconnect(); + } + } finally { + await closeServer(server); + } + } + ); +}); 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.e2e.test.ts b/nodejs/test/e2e/rpc.e2e.test.ts index 4e93fdbe44..cd9887f002 100644 --- a/nodejs/test/e2e/rpc.e2e.test.ts +++ b/nodejs/test/e2e/rpc.e2e.test.ts @@ -88,7 +88,6 @@ describe("Session RPC", async () => { model: "claude-sonnet-5", }); - // Get initial model const before = await session.rpc.model.getCurrent(); expect(before.modelId).toBeDefined(); @@ -143,7 +142,6 @@ describe("Session RPC", async () => { expect(afterUpdate.exists).toBe(true); expect(afterUpdate.content).toBe(planContent); - // Delete plan await session.rpc.plan.delete(); // Verify plan is deleted 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..5f3f4f89e4 --- /dev/null +++ b/nodejs/test/e2e/rpc_extensions_loaded.e2e.test.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * 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"); + 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_server_connectors.e2e.test.ts b/nodejs/test/e2e/rpc_server_connectors.e2e.test.ts new file mode 100644 index 0000000000..5cf8fc465f --- /dev/null +++ b/nodejs/test/e2e/rpc_server_connectors.e2e.test.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { mkdirSync, rmSync } from "node:fs"; +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { CopilotClient, RuntimeConnection } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const CATALOG_PATH = "/copilot-connectors/api/v1/plugins"; + +interface CatalogRequest { + token: string; + ifNoneMatch?: string; + statusCode: number; +} + +/** + * Serves the GitHub API routes client-level Connector discovery needs: the + * Copilot-user lookup that resolves the SDK credential's login, and a Connector + * catalog that honors `If-None-Match` so conditional revalidation is observable. + */ +async function startGitHubApi(login: string, capiUrl: string, etag: string) { + const catalogRequests: CatalogRequest[] = []; + const server: Server = createServer((req, res) => { + const url = req.url ?? "/"; + if (req.method === "GET" && url.startsWith("/copilot_internal/user")) { + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify({ + login, + copilot_plan: "individual_pro", + endpoints: { api: capiUrl, telemetry: "https://localhost:1/telemetry" }, + analytics_tracking_id: "connector-discovery-tracking-id", + }) + ); + return; + } + if (req.method === "GET" && url.startsWith(CATALOG_PATH)) { + const token = String(req.headers.authorization ?? "").replace( + /^(?:Bearer|token)\s+/i, + "" + ); + const ifNoneMatch = + typeof req.headers["if-none-match"] === "string" + ? req.headers["if-none-match"] + : undefined; + const statusCode = ifNoneMatch === etag ? 304 : 200; + catalogRequests.push({ + token, + ...(ifNoneMatch === undefined ? {} : { ifNoneMatch }), + statusCode, + }); + if (statusCode === 304) { + res.writeHead(304, { etag }); + res.end(); + return; + } + res.writeHead(200, { "content-type": "application/json", etag }); + res.end( + JSON.stringify({ + plugins: [ + { + name: "mail", + connection: { status: "not_connected" }, + metadata: { displayName: "Mail Connector" }, + mcpServers: { + mcpServers: { + mail: { type: "http", url: "https://connector.invalid/mcp" }, + }, + }, + }, + ], + }) + ); + return; + } + res.writeHead(404, { "content-type": "application/json" }); + res.end(JSON.stringify({ message: "Not Found" })); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => resolve()); + }); + const { port } = server.address() as AddressInfo; + return { + url: `http://127.0.0.1:${port}`, + catalogRequests, + close: () => + new Promise((resolve) => { + server.closeAllConnections(); + server.close(() => resolve()); + }), + }; +} + +describe("Client-level Connector discovery", async () => { + const { env, workDir } = await createSdkTestContext(); + + it( + "should discover and revalidate the Connector catalog without a session", + { timeout: 120_000 }, + async () => { + const login = `connector-discovery-${randomUUID().replaceAll("-", "")}`; + const token = `connector-discovery-token-${randomUUID().replaceAll("-", "")}`; + const etag = 'W/"connector-discovery-v1"'; + const gitHubApi = await startGitHubApi(login, env.COPILOT_API_URL, etag); + const home = join(workDir, `copilot-e2e-connectors-home-${randomUUID()}`); + mkdirSync(home, { recursive: true }); + const client = new CopilotClient({ + workingDirectory: workDir, + env: { + ...env, + COPILOT_HOME: home, + GH_CONFIG_DIR: home, + XDG_CONFIG_HOME: home, + XDG_STATE_HOME: home, + GH_TOKEN: "", + GITHUB_TOKEN: "", + COPILOT_DEBUG_GITHUB_API_URL: gitHubApi.url, + COPILOT_EXP_COPILOT_CLI_MANAGED_MCP_SERVERS: "true", + }, + logLevel: "error", + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: token, + }); + try { + await client.start(); + expect(await client.listSessions()).toEqual([]); + + const capabilities = await client.rpc.connectors.getCapabilities(); + expect(capabilities).toEqual({ + apiVersion: 1, + availability: "enabled", + conditionalCache: true, + opaqueAccountSelection: true, + }); + + const accounts = await client.rpc.connectors.getAccounts(); + expect(accounts.availability).toBe("enabled"); + const account = accounts.accounts.find( + (candidate) => candidate.authInfo.login === login + ); + expect(account).toBeDefined(); + expect(account!.authInfo.host).toBe("https://github.com"); + expect(Object.keys(account!).sort()).toEqual(["accountId", "authInfo"]); + expect(Object.keys(account!.authInfo).sort()).toEqual(["host", "login", "type"]); + expect(JSON.stringify(accounts)).not.toContain(token); + const accountId = account!.accountId; + + await expect( + client.rpc.connectors.list({ accountId: "unknown-discovery-account" }) + ).rejects.toThrow(/account selection/i); + + const catalog = await client.rpc.connectors.refresh({ accountId }); + expect(catalog.accountId).toBe(accountId); + expect(catalog.revision).toBeGreaterThan(0); + expect(catalog.connectors).toEqual([ + { name: "mail", displayName: "Mail Connector", status: "not_connected" }, + ]); + expect(JSON.stringify(catalog)).not.toContain("connector.invalid"); + + const revalidated = await client.rpc.connectors.refresh({ accountId }); + expect(revalidated).toEqual(catalog); + expect(gitHubApi.catalogRequests).toEqual([ + { token, statusCode: 200 }, + { token, ifNoneMatch: etag, statusCode: 304 }, + ]); + + const cached = await client.rpc.connectors.list({ accountId }); + expect(cached).toEqual(catalog); + expect(gitHubApi.catalogRequests).toHaveLength(2); + expect(await client.listSessions()).toEqual([]); + } finally { + try { + await client.stop(); + } catch { + // Best-effort cleanup. + } + await gitHubApi.close(); + rmSync(home, { recursive: true, force: true }); + } + } + ); +}); diff --git a/nodejs/test/e2e/rpc_server_misc.e2e.test.ts b/nodejs/test/e2e/rpc_server_misc.e2e.test.ts index 4f12e507a5..1ada908850 100644 --- a/nodejs/test/e2e/rpc_server_misc.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server_misc.e2e.test.ts @@ -88,12 +88,6 @@ describe("Miscellaneous server-scoped RPC", async () => { } } - it("should reload user settings", { timeout: 120_000 }, async () => { - await client.start(); - - await client.rpc.user.settings.reload(); - }); - it("should get set and clear user settings", { timeout: 120_000 }, async () => { const { client: isolatedClient, home } = await createIsolatedStartedClient(); try { @@ -111,12 +105,10 @@ describe("Miscellaneous server-scoped RPC", async () => { const [settingKey, setting] = entry!; const toggledValue = setting.value !== true; - const set = await isolatedClient.rpc.user.settings.set({ + await isolatedClient.rpc.user.settings.set({ settings: { [settingKey]: toggledValue }, }); - expect(set.shadowedKeys).not.toContain(settingKey); - await isolatedClient.rpc.user.settings.reload(); const afterSet = await isolatedClient.rpc.user.settings.get(); expect(afterSet.settings[settingKey].isDefault).toBe(false); expect(afterSet.settings[settingKey].value).toBe(toggledValue); @@ -124,7 +116,6 @@ describe("Miscellaneous server-scoped RPC", async () => { await isolatedClient.rpc.user.settings.set({ settings: { [settingKey]: null }, }); - await isolatedClient.rpc.user.settings.reload(); const afterClear = await isolatedClient.rpc.user.settings.get(); expect(afterClear.settings[settingKey].isDefault).toBe(true); } finally { @@ -213,14 +204,14 @@ describe("Miscellaneous server-scoped RPC", async () => { const dedicatedClient = createClient({}, DEFAULT_GITHUB_TOKEN); try { await dedicatedClient.start(); - await dedicatedClient.rpc.user.settings.reload(); + await dedicatedClient.rpc.user.settings.get(); await dedicatedClient.rpc.runtime.shutdown(); await waitForCondition( async () => { try { - await dedicatedClient.rpc.user.settings.reload(); + await dedicatedClient.rpc.user.settings.get(); return false; } catch { return true; diff --git a/nodejs/test/e2e/rpc_session_state.e2e.test.ts b/nodejs/test/e2e/rpc_session_state.e2e.test.ts index aab08b3bc5..a599c3e554 100644 --- a/nodejs/test/e2e/rpc_session_state.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state.e2e.test.ts @@ -725,7 +725,16 @@ describe("Session-scoped RPC", async () => { }); it("should compact session history after messages", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + modelCapabilities: { + limits: { + max_prompt_tokens: 128_000, + max_context_window_tokens: 128_000, + max_output_tokens: 32_000, + }, + }, + }); expect((await session.rpc.metadata.isProcessing()).processing).toBe(false); await session.sendAndWait({ prompt: "What is 2+2?" }); @@ -739,10 +748,8 @@ describe("Session-scoped RPC", async () => { expect(contextInfo.contextInfo).not.toBeNull(); if (contextInfo.contextInfo) { expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-5"); - expect(contextInfo.contextInfo.promptTokenLimit).toBe(128_000); - expect(contextInfo.contextInfo.limit).toBeGreaterThanOrEqual( - contextInfo.contextInfo.promptTokenLimit - ); + expect(contextInfo.contextInfo.promptTokenLimit).toBe(128_000 - 4_096); + expect(contextInfo.contextInfo.limit).toBe(128_000); expect(contextInfo.contextInfo.totalTokens).toBeGreaterThan(0); expect(contextInfo.contextInfo.systemTokens).toBeGreaterThan(0); expect(contextInfo.contextInfo.conversationTokens).toBeGreaterThan(0); @@ -763,10 +770,18 @@ describe("Session-scoped RPC", async () => { recomputed.systemTokenCount + recomputed.messagesTokenCount ); + const effectiveContext = await session.rpc.metadata.contextInfo({ + promptTokenLimit: 0, + outputTokenLimit: 0, + }); + expect(effectiveContext.contextInfo).not.toBeNull(); const result = await session.rpc.history.compact(); expect(result.success).toBe(true); expect(result.messagesRemoved).toBeGreaterThanOrEqual(0); if (result.contextWindow) { + expect(result.contextWindow.tokenLimit).toBe( + effectiveContext.contextInfo?.promptTokenLimit + ); expect(result.contextWindow.messagesLength).toBeGreaterThanOrEqual(0); expect(result.contextWindow.currentTokens).toBeGreaterThanOrEqual(0); if (result.contextWindow.conversationTokens != null) { 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 3f017f07cc..adcf067ede 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,40 @@ describe("Generated RPC surface coverage", () => { ...collectRuntimeFunctions(session.rpc, "session"), ]); - expect(inventory).toHaveLength(355); + expect(inventory).toHaveLength(398); + expect(inventory.some((method) => method.wireMethod === "user.settings.reload")).toBe( + false + ); + 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", + "mcp.planUninstall", + "mcp.applyUninstall", + "mcp.installations.list", + "mcp.installations.recover", + "mcp.installations.status", + "mcp.installations.cancel", + ]; + for (const wireMethod of boundInstallationMethods) { + expect( + inventory.filter((method) => method.wireMethod === wireMethod), + `Missing generated server RPC ${wireMethod}` + ).toEqual([expect.objectContaining({ scope: "server", path: wireMethod })]); + } expect([...runtimeFunctions.keys()].sort()).toEqual( inventory.map((method) => `${method.scope}.${method.path}`) ); @@ -343,6 +390,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: { @@ -386,6 +446,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({ @@ -408,5 +473,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 new file mode 100644 index 0000000000..b1b8a1d9d2 --- /dev/null +++ b/nodejs/test/e2e/runtime_host.e2e.test.ts @@ -0,0 +1,1275 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { ChildProcess } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { mkdir, writeFile } from "node:fs/promises"; +import { createServer, type Socket } from "node:net"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { + ActionType, + ResponsePartKind, + ToolCallConfirmationReason, + ToolCallContributorKind, + ToolResultContentType, + TurnState, + type ChatState, + type SessionState, +} from "@microsoft/agent-host-protocol-v09"; +import { describe, expect, it, vi } from "vitest"; +import { z } from "zod"; +import { + approveAll, + CopilotClient, + defineTool, + RuntimeConnection, + type AhpHostExit, + type AhpSessionCreateRequest, + type AhpSessionResumeRequest, + type CopilotSession, +} from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { + assertHostStopped as assertHostStoppedWithPid, + assertProcessStopped, + assertRuntimeListener, + authenticateAhp, + connectAhp, + createAhpSession, + localHostArtifacts, + streamedTurn, + withDeadline, +} from "./harness/runtimeHost.js"; + +const enabled = process.env.COPILOT_RUNTIME_HOST_E2E === "1"; + +// Linux /proc lets this opt-in source-build suite verify executable identity as +// well as process ancestry. No released CLI or runtime package may substitute. +describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { + if (!enabled) return; + if (process.platform !== "linux") throw new Error("Runtime host topology E2Es require Linux"); + const artifacts = localHostArtifacts(); + const connectionToken = "runtime-host-e2e-runtime-connection"; + const ctx = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forTcp({ + path: artifacts.runtimePath, + connectionToken, + }), + env: { + ...artifacts.env, + COPILOT_ENABLE_SECRET_FILTERING: undefined, + COPILOT_MCP_APPS: "true", + }, + }, + }); + const owner = ctx.copilotClient; + + 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({ + ...config, + onPermissionRequest: approveAll, + mcpServers: { + "app-mcp": { + type: "stdio", + command: process.execPath, + args: [ + fileURLToPath( + new URL( + "../../../test/harness/test-mcp-server.mjs", + import.meta.url + ) + ), + ], + tools: ["*"], + }, + }, + }); + await vi.waitFor( + async () => { + const { servers } = await original!.rpc.mcp.list(); + expect(servers.find((server) => server.name === "app-mcp")?.status).toBe( + "connected" + ); + }, + { timeout: 10_000 } + ); + return original; + }, + }); + const ahp = await connectAhp(host, { mcpApps: {} }); + try { + const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + const server = await vi.waitFor( + async () => { + const { result } = await ahp.client.subscribe(session.sessionUri); + const state = result.snapshot?.state as SessionState; + const server = state.customizations?.find((entry) => entry.id === "app-mcp"); + if (server?.type !== "mcpServer") + throw new Error("Missing app MCP customization"); + return server; + }, + { timeout: 10_000 } + ); + expect(server.mcpApp?.capabilities.serverTools).toEqual({}); + expect(server.channel).toMatch(/^mcp:\/\//); + // MCP side-channel methods are outside AHP's typed CommandMap. + const request = ahp.client.request as ( + method: string, + params: { channel: string } + ) => Promise<{ tools: { name: string }[] }>; + const listed = await request.call(ahp.client, "tools/list", { + channel: server.channel!, + }); + expect(listed.tools.some((tool) => tool.name === "get_env")).toBe(true); + } finally { + await host.dispose(); + await ahp.client.shutdown(); + await original?.disconnect(); + } + }); + + it("hands the same app session and custom tool to AHP and releases its original object", async () => { + if (process.env.GITHUB_ACTIONS !== "true") { + throw new Error("Set GITHUB_ACTIONS=true for read-only canonical inference replay"); + } + await ctx.openAiEndpoint.updateConfig({ + filePath: fileURLToPath( + new URL( + "../../../test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml", + import.meta.url + ) + ), + workDir: ctx.workDir, + }); + const marker = "APPLICATION_OWNED_AHP_PROMPT"; + const tool = vi.fn(({ seed }: { seed: string }) => `MAGIC_${seed}_42`); + 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({ + ...config, + onPermissionRequest: approveAll, + systemMessage: { mode: "append", content: marker }, + tools: [ + defineTool("magic_number", { + description: "Returns a magic number", + parameters: z.object({ seed: z.string().describe("A seed value") }), + handler: tool, + }), + ], + }); + return original; + }, + onSessionReleased: released, + }); + const ahp = await connectAhp(host); + try { + const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + expect(original?.sessionId).toBe(session.sessionId); + await assertRuntimeListener(host, runtimeDetails().pid, artifacts); + const response = await streamedTurn( + ahp.client, + session.chatUri, + session.subscription, + "Use the magic_number tool with seed 'hello' and tell me the result" + ); + expect(tool).toHaveBeenCalledOnce(); + expect(tool.mock.calls[0]?.[0]).toEqual({ seed: "hello" }); + // Validate the final assistant response arrived (guards against truncated captures). + expect(response.text).toContain("MAGIC_hello_42"); + expect(response.deltas).toBeGreaterThan(0); + const exchanges = await ctx.openAiEndpoint.getExchanges(); + expect(exchanges.length).toBeGreaterThan(0); + expect(JSON.stringify(exchanges[0].request.messages)).toContain(marker); + expect( + exchanges[0].request.tools?.some((entry) => entry.function.name === "magic_number") + ).toBe(true); + expect(released).not.toHaveBeenCalled(); + await host.dispose(); + await assertHostStopped(host, ahp); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original), { + timeout: 10_000, + }); + await expect(original!.getEvents()).resolves.toEqual(expect.any(Array)); + await host.dispose(); + expect(released).toHaveBeenCalledOnce(); + } finally { + await ahp.client.shutdown(); + // Disposal is explicitly the application's decision, after AHP released it. + await original?.disconnect(); + } + }); + + it("routes application and AHP client tools to their respective callbacks in one session", async () => { + if (process.env.GITHUB_ACTIONS !== "true") { + throw new Error("Set GITHUB_ACTIONS=true for read-only canonical inference replay"); + } + await ctx.openAiEndpoint.updateConfig({ + filePath: fileURLToPath( + new URL( + "../../../test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml", + import.meta.url + ) + ), + workDir: ctx.workDir, + }); + const appTool = vi.fn(({ city }: { city: string }) => `CITY_${city.toUpperCase()}`); + const clientTool = vi.fn( + ({ country }: { country: string }) => `COUNTRY_${country.toUpperCase()}` + ); + let original: CopilotSession | undefined; + await using host = await owner.startAhpHost({ + localServer: {}, + createSession: async ({ config }) => { + original = await owner.createSession({ + ...config, + onPermissionRequest: approveAll, + tools: [ + defineTool("lookup_city", { + description: "Looks up city information", + parameters: z.object({ city: z.string() }), + handler: appTool, + }), + ], + }); + return original; + }, + }); + const ahp = await connectAhp(host); + try { + const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN, [ + { + name: "lookup_country", + description: "Looks up country information", + inputSchema: { + type: "object", + properties: { country: { type: "string" } }, + required: ["country"], + }, + }, + ]); + expect(original?.sessionId).toBe(session.sessionId); + const clientCalls = new Set(); + const response = await streamedTurn( + ahp.client, + session.chatUri, + session.subscription, + "Use lookup_city with 'Paris' and lookup_country with 'France' at the same time, then combine both results in your reply.", + "claude-sonnet-5", + (action) => { + if ( + action.type === ActionType.ChatToolCallStart && + action.contributor?.kind === ToolCallContributorKind.Client + ) { + expect(action.toolName).toBe("lookup_country"); + expect(action.contributor.clientId).toBe(ahp.clientId); + clientCalls.add(action.toolCallId); + } + if ( + action.type === ActionType.ChatToolCallReady && + clientCalls.has(action.toolCallId) + ) { + expect(action.confirmed).toBe(ToolCallConfirmationReason.NotNeeded); + expect(typeof action.toolInput).toBe("string"); + const args = JSON.parse(action.toolInput as string) as { country: string }; + expect(args).toEqual({ country: "France" }); + const text = clientTool(args); + ahp.client.dispatch(session.chatUri, { + type: ActionType.ChatToolCallComplete, + turnId: action.turnId, + toolCallId: action.toolCallId, + result: { + success: true, + pastTenseMessage: "Looked up country information", + content: [{ type: ToolResultContentType.Text, text }], + }, + }); + } + } + ); + expect(appTool).toHaveBeenCalledOnce(); + expect(appTool.mock.calls[0]?.[0]).toEqual({ city: "Paris" }); + expect(clientTool).toHaveBeenCalledExactlyOnceWith({ country: "France" }); + expect(clientCalls.size).toBe(1); + expect(response.text).toContain("CITY_PARIS"); + expect(response.text).toContain("COUNTRY_FRANCE"); + await host.dispose(); + await assertHostStopped(host, ahp); + await expect(original!.getEvents()).resolves.toEqual(expect.any(Array)); + } finally { + await host.dispose(); + await ahp.client.shutdown(); + await original?.disconnect(); + } + }); + + it("releases the original app session after a genuine AHP attach failure", async () => { + if (process.env.GITHUB_ACTIONS !== "true") { + throw new Error("Set GITHUB_ACTIONS=true for read-only canonical inference replay"); + } + await ctx.openAiEndpoint.updateConfig({ + filePath: fileURLToPath( + new URL( + "../../../test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml", + import.meta.url + ) + ), + workDir: ctx.workDir, + }); + const released = vi.fn(); + 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, + onPermissionRequest: approveAll, + tools: [ + defineTool("magic_number", { + description: "Returns a magic number", + parameters: z.object({ seed: z.string().describe("A seed value") }), + handler: tool, + }), + ], + }); + return original; + }, + onSessionReleased: released, + }); + const ahp = await connectAhp(host); + try { + await authenticateAhp(ahp, ctx.env.GITHUB_TOKEN); + await expect( + ahp.client.request("createSession", { + channel: `ahp-session:/${randomUUID()}`, + provider: "copilot", + workingDirectories: [pathToFileURL(ctx.workDir).href], + activeClient: { + clientId: ahp.clientId, + displayName: "Clashing AHP participant", + tools: [ + { + name: "magic_number", + description: + "A different participant cannot own the application's tool", + inputSchema: { type: "object", properties: {} }, + }, + ], + }, + }) + ).rejects.toThrow("attach failed"); + expect(original).toBeDefined(); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original), { + timeout: 10_000, + }); + const response = await original!.sendAndWait({ + prompt: "Use the magic_number tool with seed 'hello' and tell me the result", + }); + // Validate the final assistant response arrived (guards against truncated captures). + expect(response?.data.content).toContain("MAGIC_hello_42"); + expect(tool).toHaveBeenCalledOnce(); + await host.dispose(); + expect(released).toHaveBeenCalledOnce(); + } finally { + await ahp.client.shutdown(); + await original?.disconnect(); + } + }); + + it("releases the original app session after listener disposal without disconnecting it", async () => { + const released = vi.fn(); + let original: CopilotSession | undefined; + await using host = await owner.startAhpHost({ + localServer: {}, + createSession: async ({ config }) => { + original = await owner.createSession({ + ...config, + onPermissionRequest: approveAll, + }); + await original.rpc.mode.set({ mode: "plan" }); + return original; + }, + onSessionReleased: released, + }); + const ahp = await connectAhp(host); + try { + await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + expect(await original!.rpc.mode.get()).toBe("interactive"); + await host.dispose(); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original), { + timeout: 10_000, + }); + await assertHostStopped(host, ahp); + await expect(original!.getEvents()).resolves.toEqual(expect.any(Array)); + await host.dispose(); + expect(released).toHaveBeenCalledOnce(); + } finally { + await ahp.client.shutdown(); + await original?.disconnect(); + } + }); + + it("resumes a durable app-owned session and composes tools after runtime restart", async () => { + await configureReplay("runtime_host/app_resume_callback_composes_tools_after_history"); + const firstOwner = ctx.createClient(); + const resumedOwner = ctx.createClient(); + // Restore the persisted client's tools, not dynamically register a new tool on resume. + const clientId = randomUUID(); + const appTool = vi.fn(({ seed }: { seed: string }) => `MAGIC_${seed}_42`); + const clientTool = vi.fn(({ text }: { text: string }) => `CLIENT_ECHO_${text}`); + const marker = "RESUMED_APPLICATION_CONFIG"; + const clientEchoTool = { + name: "client_echo", + description: "Echoes text from the AHP client", + inputSchema: { + type: "object" as const, + properties: { text: { type: "string" } }, + required: ["text"], + }, + }; + const released = vi.fn(); + let original: CopilotSession | undefined; + let resumedOriginal: CopilotSession | undefined; + let sessionUri = ""; + let sessionId = ""; + const create = vi.fn(async ({ config }: AhpSessionCreateRequest) => { + original = await firstOwner.createSession({ + ...config, + onPermissionRequest: approveAll, + }); + return original; + }); + const first = await firstOwner.startAhpHost({ + localServer: {}, + createSession: create, + onSessionReleased: released, + }); + const firstAhp = await connectAhp(first, undefined, clientId); + try { + const session = await createAhpSession(firstAhp, ctx.workDir, ctx.env.GITHUB_TOKEN, [ + clientEchoTool, + ]); + sessionUri = session.sessionUri; + sessionId = session.sessionId; + expect(create).toHaveBeenCalledOnce(); + expect(original?.sessionId).toBe(sessionId); + await assertRuntimeListener(first, runtimeDetails(firstOwner).pid, artifacts); + expect( + ( + await streamedTurn( + firstAhp.client, + session.chatUri, + session.subscription, + "What is 2+2?" + ) + ).text + ).toContain("4"); + await first.dispose(); + await assertHostStopped(first, firstAhp, runtimeDetails(firstOwner).pid); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original)); + await expect(original!.getEvents()).resolves.toEqual(expect.any(Array)); + } finally { + await first.dispose(); + await firstAhp.client.shutdown(); + await original?.disconnect(); + const pid = runtimeDetails(firstOwner).pid; + await firstOwner.stop(); + await assertProcessStopped(pid, "first application runtime"); + } + + const createAgain = vi.fn(async (): Promise => { + throw new Error("Durable resume must not invoke createSession"); + }); + const resume = vi.fn( + async ({ sessionId: requestedId, config, signal }: AhpSessionResumeRequest) => { + expect(requestedId).toBe(sessionId); + expect(signal.aborted).toBe(false); + expect(config.continuePendingWork).toBe(false); + expect(config.workingDirectory).toBe(ctx.workDir); + resumedOriginal = await resumedOwner.resumeSession(requestedId, { + ...config, + onPermissionRequest: approveAll, + systemMessage: { mode: "append", content: marker }, + tools: [ + defineTool("magic_number", { + description: "Returns a magic number", + parameters: z.object({ seed: z.string().describe("A seed value") }), + handler: appTool, + }), + ], + }); + return resumedOriginal; + } + ); + const resumedReleased = vi.fn(); + const replacement = await resumedOwner.startAhpHost({ + localServer: {}, + createSession: createAgain, + resumeSession: resume, + onSessionReleased: resumedReleased, + }); + const replacementAhp = await connectAhp(replacement, undefined, clientId); + try { + await assertRuntimeListener(replacement, runtimeDetails(resumedOwner).pid, artifacts); + const history = await resumeAhp(replacementAhp, sessionUri); + expect(history.turns.map((turn) => turn.message.text)).toEqual(["What is 2+2?"]); + expect(resume).toHaveBeenCalledOnce(); + expect(createAgain).not.toHaveBeenCalled(); + expect(resumedOriginal?.sessionId).toBe(sessionId); + expect(resumedOriginal).not.toBe(original); + expect(resumedReleased).not.toHaveBeenCalled(); + const { result } = await replacementAhp.client.subscribe(sessionUri); + const chatUri = (result.snapshot?.state as SessionState).defaultChat!; + const { subscription } = await replacementAhp.client.subscribe(chatUri); + replacementAhp.client.dispatch(sessionUri, { + type: ActionType.SessionActiveClientSet, + activeClient: { + clientId: replacementAhp.clientId, + displayName: "Resumed tool owner", + tools: [clientEchoTool], + }, + }); + await vi.waitFor(async () => { + const { result } = await replacementAhp.client.subscribe(sessionUri); + const activeClients = (result.snapshot?.state as SessionState).activeClients; + expect(activeClients.map((entry) => entry.clientId)).toContain( + replacementAhp.clientId + ); + }); + const response = await streamedTurn( + replacementAhp.client, + chatUri, + subscription, + "Call magic_number with seed 'hello' and client_echo with text 'ping', then report both results", + "claude-sonnet-5", + undefined, + { + clientId: replacementAhp.clientId, + handlers: { client_echo: (input) => clientTool(input as { text: string }) }, + } + ); + expect(appTool).toHaveBeenCalledExactlyOnceWith( + { seed: "hello" }, + expect.objectContaining({ sessionId, toolName: "magic_number" }) + ); + expect(clientTool).toHaveBeenCalledExactlyOnceWith({ text: "ping" }); + expect(response.text).toContain("MAGIC_hello_42"); + expect(response.text).toContain("CLIENT_ECHO_ping"); + const exchanges = await ctx.openAiEndpoint.getExchanges(); + const composed = exchanges.filter((exchange) => + JSON.stringify(exchange.request.messages).includes("Call magic_number") + ); + expect(composed).toHaveLength(2); + for (const { request } of composed) { + const names = + request.tools?.flatMap((tool) => + tool.type === "function" ? [tool.function.name] : [] + ) ?? []; + expect(names.filter((name) => name === "magic_number")).toHaveLength(1); + expect(names.filter((name) => name === "client_echo")).toHaveLength(1); + expect(JSON.stringify(request.messages)).toContain(marker); + expect(JSON.stringify(request.messages)).toContain("What is 2+2?"); + } + expect( + composed + .at(-1)! + .request.messages.filter((message) => message.role === "tool") + .map((message) => message.content) + .sort() + ).toEqual(["CLIENT_ECHO_ping", "MAGIC_hello_42"]); + await replacementAhp.client.subscribe(sessionUri); + expect(resume).toHaveBeenCalledOnce(); + await replacement.dispose(); + await assertHostStopped(replacement, replacementAhp, runtimeDetails(resumedOwner).pid); + await vi.waitFor(() => + expect(resumedReleased).toHaveBeenCalledExactlyOnceWith(resumedOriginal) + ); + await expect(resumedOriginal!.getEvents()).resolves.toEqual(expect.any(Array)); + await replacement.dispose(); + expect(resumedReleased).toHaveBeenCalledOnce(); + } finally { + await replacement.dispose(); + await replacementAhp.client.shutdown(); + await resumedOriginal?.disconnect(); + await resumedOwner.stop(); + } + }); + + it("resumes a durable app-owned session through its callback after listener restart", async () => { + await configureReplay(); + const released = vi.fn(); + let original: CopilotSession | undefined; + let resumedOriginal: CopilotSession | undefined; + let sessionUri = ""; + let sessionId = ""; + await using first = await owner.startAhpHost({ + localServer: {}, + createSession: async ({ config }) => { + original = await owner.createSession({ + ...config, + onPermissionRequest: approveAll, + }); + return original; + }, + onSessionReleased: released, + }); + const firstAhp = await connectAhp(first); + try { + const session = await createAhpSession(firstAhp, ctx.workDir, ctx.env.GITHUB_TOKEN); + sessionUri = session.sessionUri; + sessionId = session.sessionId; + expect(original?.sessionId).toBe(sessionId); + expect( + ( + await streamedTurn( + firstAhp.client, + session.chatUri, + session.subscription, + "What is 2+2?" + ) + ).text + ).toContain("4"); + await first.dispose(); + await assertHostStopped(first, firstAhp); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original)); + } finally { + await firstAhp.client.shutdown(); + await original?.disconnect(); + } + + const createAgain = vi.fn(async (): Promise => { + throw new Error("Durable resume must not invoke createSession"); + }); + const resume = vi.fn( + async ({ sessionId: requestedId, config, signal }: AhpSessionResumeRequest) => { + expect(requestedId).toBe(sessionId); + expect(signal.aborted).toBe(false); + expect(config.continuePendingWork).toBe(false); + resumedOriginal = await owner.resumeSession(requestedId, { + ...config, + onPermissionRequest: approveAll, + }); + return resumedOriginal; + } + ); + const resumedReleased = vi.fn(); + await using replacement = await owner.startAhpHost({ + localServer: {}, + createSession: createAgain, + resumeSession: resume, + onSessionReleased: resumedReleased, + }); + const replacementAhp = await connectAhp(replacement); + try { + await resumeAhp(replacementAhp, sessionUri); + expect(resume).toHaveBeenCalledOnce(); + expect(createAgain).not.toHaveBeenCalled(); + expect(resumedOriginal?.sessionId).toBe(sessionId); + expect(resumedOriginal).not.toBe(original); + expect(resumedReleased).not.toHaveBeenCalled(); + await replacement.dispose(); + await assertHostStopped(replacement, replacementAhp); + await vi.waitFor(() => + expect(resumedReleased).toHaveBeenCalledExactlyOnceWith(resumedOriginal) + ); + await expect(resumedOriginal!.getEvents()).resolves.toEqual(expect.any(Array)); + await replacement.dispose(); + expect(resumedReleased).toHaveBeenCalledOnce(); + const retainedResume = vi.fn(async () => resumedOriginal!); + const retainedReleased = vi.fn(); + await using retainedHost = await owner.startAhpHost({ + localServer: {}, + resumeSession: retainedResume, + onSessionReleased: retainedReleased, + }); + const retainedAhp = await connectAhp(retainedHost); + try { + await resumeAhp(retainedAhp, sessionUri); + expect(retainedResume).toHaveBeenCalledOnce(); + await retainedHost.dispose(); + await assertHostStopped(retainedHost, retainedAhp); + await vi.waitFor(() => + expect(retainedReleased).toHaveBeenCalledExactlyOnceWith(resumedOriginal) + ); + } finally { + await retainedAhp.client.shutdown(); + } + } finally { + await replacementAhp.client.shutdown(); + await resumedOriginal?.disconnect(); + } + }); + + it("publishes resident sessions without invoking factories or replacing application config", async () => { + await configureReplay(); + const marker = "PUBLISHED_APPLICATION_PROMPT"; + await using original = await owner.createSession({ + model: "claude-sonnet-5", + onPermissionRequest: approveAll, + systemMessage: { mode: "append", content: marker }, + excludedTools: ["bash"], + tools: [ + defineTool("published_marker", { + description: "Application-owned publication marker", + parameters: z.object({}), + handler: () => "marker", + }), + ], + }); + await original.rpc.mode.set({ mode: "plan" }); + const factory = vi.fn(async (): Promise => { + throw new Error("Publication must not invoke an application factory"); + }); + const released = vi.fn(); + await using host = await owner.startAhpHost({ + localServer: {}, + createSession: factory, + resumeSession: factory, + onSessionReleased: released, + }); + const ahp = await connectAhp(host); + try { + const published = await host.publishSession(original.sessionId); + await authenticateAhp(ahp, ctx.env.GITHUB_TOKEN); + const { result } = await ahp.client.subscribe(published.sessionUri); + const state = result.snapshot?.state as SessionState; + expect(state.lifecycle).toBe("ready"); + expect(await original.rpc.mode.get()).toBe("plan"); + expect(factory).not.toHaveBeenCalled(); + // The application, not the listener, chooses the mode for this turn. + await original.rpc.mode.set({ mode: "interactive" }); + expect( + (await original.sendAndWait({ prompt: "What is 2+2?" }))?.data.content + ).toContain("4"); + const exchanges = await ctx.openAiEndpoint.getExchanges(); + expect(exchanges.length).toBeGreaterThan(0); + const request = exchanges.at(-1)!.request; + expect(JSON.stringify(request.messages)).toContain(marker); + expect(request.tools?.some((tool) => tool.function.name === "published_marker")).toBe( + true + ); + expect(request.tools?.some((tool) => tool.function.name === "bash")).toBe(false); + await host.dispose(); + await assertHostStopped(host, ahp); + expect(factory).not.toHaveBeenCalled(); + expect(released).not.toHaveBeenCalled(); + await expect(original.getEvents()).resolves.toEqual(expect.any(Array)); + } finally { + await ahp.client.shutdown(); + } + }); + + function exitObserver() { + const exits: AhpHostExit[] = []; + let resolve!: (exit: AhpHostExit) => void; + const notified = new Promise((done) => { + resolve = done; + }); + return { + exits, + notified, + onExit: (exit: AhpHostExit) => { + exits.push(exit); + resolve(exit); + }, + }; + } + + async function configureReplay( + snapshot = "session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message" + ) { + if (process.env.GITHUB_ACTIONS !== "true") { + throw new Error("Set GITHUB_ACTIONS=true for read-only canonical inference replay"); + } + await ctx.openAiEndpoint.updateConfig({ + filePath: fileURLToPath( + new URL(`../../../test/snapshots/${snapshot}.yaml`, import.meta.url) + ), + workDir: ctx.workDir, + }); + } + + async function resumeAhp( + ahp: Awaited>, + sessionUri: string, + excludedSdkSessionId?: string + ) { + await authenticateAhp(ahp, ctx.env.GITHUB_TOKEN); + const listed = await ahp.client.request("listSessions", { channel: "ahp-root://" }); + const resources = listed.items.map((item) => item.resource); + expect(resources).toContain(sessionUri); + if (excludedSdkSessionId) { + expect(resources).not.toContain(`ahp-session:/${excludedSdkSessionId}`); + } + // Subscribing a dormant catalog URI is the standard AHP resume path. + const { result } = await ahp.client.subscribe(sessionUri); + const session = result.snapshot?.state as SessionState | undefined; + expect(session?.lifecycle).toBe("ready"); + expect(session?.defaultChat).toBeTruthy(); + const chat = await ahp.client.subscribe(session!.defaultChat!); + expect(chat.result.snapshot).toBeDefined(); + const state = chat.result.snapshot?.state as ChatState; + const turn = state.turns.find((entry) => entry.message.text === "What is 2+2?"); + expect(turn?.state).toBe(TurnState.Complete); + expect( + turn?.responseParts + .filter((part) => part.kind === ResponsePartKind.Markdown) + .map((part) => part.content) + .join("") + ).toContain("4"); + return state; + } + + function runtimeDetails(client = owner) { + const internals = client as unknown as { + runtimePort: number; + cliProcess: ChildProcess; + }; + const pid = internals.cliProcess.pid; + expect(pid).toBeGreaterThan(0); + return { pid: pid!, port: internals.runtimePort }; + } + + function assertHostStopped( + host: Parameters[0], + ahp: Parameters[1], + runtimePid: number | false = runtimeDetails().pid + ) { + return assertHostStoppedWithPid(host, ahp, runtimePid); + } + + 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({ localServer: {}, onExit: observed.onExit }); + const ahp = await connectAhp(host); + try { + await ahp.client.ping(); + await assertRuntimeListener(host, runtimeDetails().pid, artifacts); + 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 }), + owner.rpc.host.dispose({ hostId: host.hostId }), + ]); + const exit = await withDeadline(observed.notified, "disposed host notification"); + expect(exit.hostId).toBe(host.hostId); + expect(exit.reason).toBe("disposed"); + expect(exit.exitCode).toBeUndefined(); + await assertHostStopped(host, ahp); + await expect(session.getEvents()).resolves.toEqual(expect.any(Array)); + // Empty sessions are not persisted/listed until their first turn. + await using additionalSession = await owner.createSession({ + onPermissionRequest: approveAll, + }); + expect(additionalSession.sessionId).not.toBe(session.sessionId); + await host.dispose(); + await owner.rpc.host.dispose({ hostId: randomUUID() }); + expect(observed.exits).toHaveLength(1); + } finally { + await ahp.client.shutdown(); + } + }); + + it("registers AHP credentials through the host without enabling ordinary SDK registration", 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({ localServer: {} }); + const ahp = await connectAhp(host); + try { + const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + expect(session.sessionUri).toMatch(/^ahp-session:/); + await expect(registerFromOwner()).rejects.toThrow("COPILOT_ENABLE_SECRET_FILTERING"); + } finally { + await ahp.client.shutdown(); + } + }); + + it("recovers from listener startup failure while the owner session remains usable", async () => { + await using session = await owner.createSession({ onPermissionRequest: approveAll }); + const observed = exitObserver(); + const blocker = createServer(); + await new Promise((resolve) => blocker.listen(0, "127.0.0.1", resolve)); + const address = blocker.address(); + if (!address || typeof address === "string") throw new Error("Missing blocker address"); + try { + 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) => + blocker.close((error) => (error ? reject(error) : resolve())) + ); + } + // There is no host process to kill: listener recovery is not process isolation. + await using host = await owner.startAhpHost({ + localServer: { port: address.port }, + onExit: observed.onExit, + }); + const ahp = await connectAhp(host); + try { + await assertRuntimeListener(host, runtimeDetails().pid, artifacts); + await ahp.client.ping(); + await host.dispose(); + const exit = await withDeadline(observed.notified, "recovered listener disposal"); + expect(exit.reason).toBe("disposed"); + expect(exit.exitCode).toBeUndefined(); + await assertHostStopped(host, ahp); + expect(observed.exits).toHaveLength(1); + } finally { + await ahp.client.shutdown(); + } + }); + + it("honors listener endpoints and supplied tokens through framed RPC", async () => { + const token = `ahp-e2e-${randomUUID()}`; + 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); + try { + await ephemeral.dispose(); + await assertHostStopped(ephemeral, initialAhp); + } finally { + await initialAhp.client.shutdown(); + } + + for (const hostname of ["0.0.0.0", "localhost", "::1"]) { + await using host = await owner.startAhpHost({ + localServer: { + hostname, + port: hostname === "0.0.0.0" ? port : 0, + token, + requireConnectionToken: true, + }, + }); + 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"); + expect(Number(bound.port)).toBe(port); + } else if (hostname === "::1") { + expect(bound.hostname).toBe("[::1]"); + } else { + expect(["127.0.0.1", "[::1]"]).toContain(bound.hostname); + } + 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( + connectAhp({ url: reachable.href, token: "wrong-token" }) + ).rejects.toThrow(); + const ahp = await connectAhp({ url: reachable.href, token: host.token }); + try { + await ahp.client.ping(); + await assertRuntimeListener(host, runtimeDetails().pid, artifacts); + await host.dispose(); + await assertHostStopped(host, ahp); + } finally { + await ahp.client.shutdown(); + } + } + }); + + 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 }, + { port: 1.5 }, + { token: "" }, + { token: "supplied", requireConnectionToken: false }, + ]) { + await expect( + owner.rpc.host.start({ hostId: randomUUID(), localServer: options }) + ).rejects.toThrow(); + } + await using host = await owner.startAhpHost({ + localServer: { requireConnectionToken: false }, + }); + expect(host.token).toBeUndefined(); + const ahp = await connectAhp(host); + try { + await ahp.client.ping(); + const file = join(ctx.workDir, "ahp-resource.txt"); + await writeFile(file, "workspace data"); + const resource = { channel: "ahp-root://", uri: pathToFileURL(file).href }; + await expect(ahp.client.request("resourceRead", resource)).rejects.toMatchObject({ + code: -32007, + }); + await expect( + ahp.client.request("createSession", { + channel: `ahp-session:/${randomUUID()}`, + provider: "copilot", + workingDirectories: [pathToFileURL(ctx.workDir).href], + }) + ).rejects.toThrow(); + await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + await expect(ahp.client.request("resourceRead", resource)).resolves.toMatchObject({ + data: "workspace data", + }); + await Promise.all([ + owner.rpc.host.dispose({ hostId: host.hostId }), + owner.rpc.host.dispose({ hostId: host.hostId }), + ]); + await assertHostStopped(host, ahp); + await owner.rpc.host.dispose({ hostId: host.hostId }); + } finally { + await ahp.client.shutdown(); + } + }); + + it("rejects a second same-home host and releases ownership on disconnect without stopping SDK sessions", async () => { + await configureReplay(); + await using survivingSession = await owner.createSession({ + onPermissionRequest: approveAll, + }); + const otherOwner = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${runtimeDetails().port}`, { + connectionToken, + }), + }); + try { + const observed = exitObserver(); + const abandonedHost = await otherOwner.startAhpHost({ + localServer: {}, + onExit: observed.onExit, + }); + const abandonedAhp = await connectAhp(abandonedHost); + try { + const session = await createAhpSession( + abandonedAhp, + ctx.workDir, + ctx.env.GITHUB_TOKEN + ); + const response = await streamedTurn( + abandonedAhp.client, + session.chatUri, + session.subscription, + "What is 2+2?" + ); + expect(response.text).toContain("4"); + await assertRuntimeListener(abandonedHost, runtimeDetails().pid, artifacts); + await expect(owner.startAhpHost({ localServer: {} })).rejects.toThrow( + /catalog (?:is )?already in use/ + ); + await expect( + owner.rpc.host.dispose({ hostId: abandonedHost.hostId }) + ).rejects.toThrow(); + await abandonedAhp.client.ping(); + await expect(survivingSession.getEvents()).resolves.toEqual(expect.any(Array)); + const socket = (otherOwner as unknown as { socket: Socket }).socket; + expect(socket.destroyed).toBe(false); + // Lose only this owner transport, without calling host.dispose or + // client.stop, and without killing the shared runtime process. + socket.destroy(); + const exit = await withDeadline(observed.notified, "owner connection loss"); + expect(exit.reason).toBe("ownerDisconnected"); + await assertHostStopped(abandonedHost, abandonedAhp); + await expect(survivingSession.getEvents()).resolves.toEqual(expect.any(Array)); + expect(observed.exits).toHaveLength(1); + await using replacement = await owner.startAhpHost({ localServer: {} }); + const replacementAhp = await connectAhp(replacement); + try { + await resumeAhp(replacementAhp, session.sessionUri, survivingSession.sessionId); + } finally { + await replacementAhp.client.shutdown(); + } + await using additionalSession = await owner.createSession({ + onPermissionRequest: approveAll, + }); + expect(additionalSession.sessionId).not.toBe(survivingSession.sessionId); + } finally { + await abandonedAhp.client.shutdown(); + } + } finally { + await otherOwner.stop(); + } + }); + + it("streams beside an SDK session and recovers its catalog after repeated listener disposal", async () => { + // Reuse the existing canonical 2+2 conversation through the existing + // matcher. AHP and SDK both send this exact prompt/model; incompatible + // requests still fail in replay-only mode rather than inventing replies. + await configureReplay(); + await using sdkSession = await owner.createSession({ + model: "claude-sonnet-5", + onPermissionRequest: approveAll, + streaming: true, + }); + await using host = await owner.startAhpHost({ localServer: {} }); + const ahp = await connectAhp(host); + try { + const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + await assertRuntimeListener(host, runtimeDetails().pid, artifacts); + const [response, sdkResponse] = await Promise.all([ + streamedTurn(ahp.client, session.chatUri, session.subscription, "What is 2+2?"), + sdkSession.sendAndWait({ prompt: "What is 2+2?" }), + ]); + expect(response.text).toContain("4"); + expect(response.deltas).toBeGreaterThan(0); + expect(sdkResponse?.data.content).toContain("4"); + + const runtimeSessions = (await owner.listSessions()).map((item) => item.sessionId); + expect(runtimeSessions).toContain(sdkSession.sessionId); + expect(runtimeSessions).toContain(session.sessionId); + await using observer = await owner.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + }); + expect( + (await observer.getEvents()).some( + (event) => + event.type === "assistant.message" && event.data.content.includes("4") + ) + ).toBe(true); + await host.dispose(); + await assertHostStopped(host, ahp); + expect( + (await sdkSession.getEvents()).some((event) => event.type === "assistant.message") + ).toBe(true); + + const observed = exitObserver(); + await using replacement = await owner.startAhpHost({ + localServer: {}, + onExit: observed.onExit, + }); + const replacementAhp = await connectAhp(replacement); + try { + const resumed = await resumeAhp( + replacementAhp, + session.sessionUri, + sdkSession.sessionId + ); + expect(resumed.turns.length).toBeGreaterThan(0); + await replacement.dispose(); + expect( + (await withDeadline(observed.notified, "catalog owner disposal")).reason + ).toBe("disposed"); + await assertHostStopped(replacement, replacementAhp); + } finally { + await replacementAhp.client.shutdown(); + } + await using recovered = await owner.startAhpHost({ localServer: {} }); + const recoveredAhp = await connectAhp(recovered); + try { + const resumed = await resumeAhp( + recoveredAhp, + session.sessionUri, + sdkSession.sessionId + ); + expect(resumed.turns.length).toBeGreaterThan(0); + await expect(sdkSession.getEvents()).resolves.toEqual(expect.any(Array)); + } finally { + await recoveredAhp.client.shutdown(); + } + } finally { + await ahp.client.shutdown(); + } + }); + + it("uses SDK baseDirectory for a durable catalog across runtime restart and rejects another runtime's writer", async () => { + await configureReplay(); + const baseDirectory = join(ctx.env.COPILOT_HOME, "explicit-base"); + await mkdir(baseDirectory, { recursive: true }); + const first = ctx.createClient({ baseDirectory }); + const second = ctx.createClient({ baseDirectory }); + let sessionUri: string; + try { + await using host = await first.startAhpHost({ localServer: {} }); + const ahp = await connectAhp(host); + try { + const runtime = (first as unknown as { cliProcess: ChildProcess }).cliProcess; + await assertRuntimeListener(host, runtime.pid!, artifacts); + const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + sessionUri = session.sessionUri; + const response = await streamedTurn( + ahp.client, + session.chatUri, + session.subscription, + "What is 2+2?" + ); + expect(response.text).toContain("4"); + // Independent ordinary SDK runtimes may use this home; only a + // second AHP server is excluded from the catalog. + await using ordinary = await second.createSession({ + onPermissionRequest: approveAll, + }); + await expect(second.startAhpHost({ localServer: {} })).rejects.toThrow( + /catalog (?:is )?already in use/ + ); + await expect(ordinary.getEvents()).resolves.toEqual(expect.any(Array)); + await ahp.client.ping(); + await host.dispose(); + await assertHostStopped(host, ahp, runtimeDetails(first).pid); + } finally { + await ahp.client.shutdown(); + } + } finally { + await first.stop(); + await second.stop(); + } + + const restarted = ctx.createClient({ baseDirectory }); + try { + await using host = await restarted.startAhpHost({ localServer: {} }); + const ahp = await connectAhp(host); + try { + const resumed = await resumeAhp(ahp, sessionUri!); + expect(resumed.turns.length).toBeGreaterThan(0); + } finally { + await ahp.client.shutdown(); + } + } finally { + await restarted.stop(); + } + }); + + it("gracefully shuts down the runtime with an attached AHP session", async () => { + const observed = exitObserver(); + const host = await owner.startAhpHost({ localServer: {}, onExit: observed.onExit }); + const ahp = await connectAhp(host); + const runtimePid = runtimeDetails().pid; + try { + await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); + await assertRuntimeListener(host, runtimePid, artifacts); + + // Prove the actual shutdown RPC succeeds before allowing SDK stop + // to reap its process. Eventual forced cleanup is not success. + await withDeadline(owner.rpc.runtime.shutdown(), "runtime shutdown response"); + const exit = await withDeadline( + observed.notified, + "runtime shutdown host notification" + ); + expect(exit.hostId).toBe(host.hostId); + expect(exit.reason).toBe("runtimeShutdown"); + expect(exit.error).toBeUndefined(); + expect(exit.exitCode).toBeUndefined(); + await assertHostStopped(host, ahp, false); + + await owner.stop(); + await assertProcessStopped(runtimePid, "SDK-owned runtime"); + } finally { + await ahp.client.shutdown(); + } + }); +}); diff --git a/nodejs/test/e2e/runtime_host_artifacts.test.ts b/nodejs/test/e2e/runtime_host_artifacts.test.ts new file mode 100644 index 0000000000..b10260e082 --- /dev/null +++ b/nodejs/test/e2e/runtime_host_artifacts.test.ts @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { + localCandidateSources, + hostingSource, + validateCandidateSources, + type CandidateSources, +} from "../../../samples/runtime-host/candidate-sources.js"; +import { localHostArtifacts } from "./harness/runtimeHost.js"; +import { candidateHostArtifacts } from "./harness/runtimeHostCandidate.js"; + +afterEach(() => vi.unstubAllEnvs()); + +describe("Runtime host monorepo source identity", () => { + let work: string; + let runtime: string; + let otherSdkCheckout: string; + let sdk: string; + let sources: CandidateSources; + const hostingCommit = "a".repeat(40); + const lockfile = ( + source = `git+https://github.com/github/copilot-host?rev=${hostingCommit}#${hostingCommit}` + ) => + `version = 4\n\n[[package]]\nname = "copilotd-hosting"\nversion = "0.1.0"\nsource = "${source}"\n`; + + beforeAll(() => { + work = mkdtempSync(join(process.cwd(), ".runtime-host-sources-")); + runtime = join(work, "runtime"); + otherSdkCheckout = join(work, "other-sdk"); + sdk = join(runtime, "src/sdk"); + for (const [checkout, directory] of [ + [runtime, sdk], + [otherSdkCheckout, join(otherSdkCheckout, "nested")], + ]) { + mkdirSync(directory, { recursive: true }); + writeFileSync(join(directory, "tracked"), "source\n"); + if (checkout === runtime) writeFileSync(join(runtime, "Cargo.lock"), lockfile()); + for (const args of [ + ["init", "--quiet"], + ["add", "."], + [ + "-c", + "user.name=SDK test", + "-c", + "user.email=sdk-test@example.invalid", + "-c", + "commit.gpgsign=false", + "-c", + "core.hooksPath=/dev/null", + "commit", + "--quiet", + "-m", + "fixture", + ], + ]) { + execFileSync("git", ["-C", checkout, ...args], { stdio: "pipe" }); + } + } + sources = localCandidateSources(runtime, sdk); + }); + + afterAll(() => { + if (work) rmSync(work, { recursive: true, force: true }); + }); + + it("attests the runtime, its SDK subtree, and the locked hosting library", () => { + expect(sources.sdk).toEqual({ ...sources.runtime, path: "src/sdk" }); + expect(sources.hosting).toEqual({ + repository: "github/copilot-host", + commit: hostingCommit, + }); + expect(validateCandidateSources(sources, sdk)).toEqual({ runtime, sdk }); + }); + + it.each([ + `git+https://example.invalid/host?rev=${hostingCommit}#${hostingCommit}`, + `git+https://github.com/github/copilot-host?branch=main#${hostingCommit}`, + `git+https://github.com/github/copilot-host?rev=${"b".repeat(40)}#${hostingCommit}`, + "registry+https://github.com/rust-lang/crates.io-index", + ])("rejects an unrelated, floating, or mismatched library source: %s", (source) => { + try { + writeFileSync(join(runtime, "Cargo.lock"), lockfile(source)); + expect(() => hostingSource(runtime)).toThrow(); + } finally { + writeFileSync(join(runtime, "Cargo.lock"), lockfile()); + } + }); + + it("rejects missing and ambiguous hosting library entries", () => { + try { + for (const value of ["version = 4\n", lockfile() + lockfile()]) { + writeFileSync(join(runtime, "Cargo.lock"), value); + expect(() => hostingSource(runtime)).toThrow("one copilotd-hosting library"); + } + } finally { + writeFileSync(join(runtime, "Cargo.lock"), lockfile()); + } + }); + + it("rejects a manifest with a different linked hosting revision", () => { + const changed = structuredClone(sources); + changed.hosting.commit = "b".repeat(40); + expect(() => validateCandidateSources(changed, sdk)).toThrow("runtime's locked library"); + }); + + it("rejects a separate SDK repository identity", () => { + const changed = structuredClone(sources); + changed.sdk.repository = "github/copilot-sdk"; + expect(() => validateCandidateSources(changed, sdk)).toThrow(); + }); + + it("rejects an SDK commit different from the runtime commit", () => { + const changed = structuredClone(sources); + changed.sdk.commit = "0".repeat(40); + expect(() => validateCandidateSources(changed, sdk)).toThrow( + "SDK must use the runtime commit" + ); + }); + + it("rejects an SDK recorded under another checkout", () => { + const changed = structuredClone(sources); + changed.sdk.checkout = otherSdkCheckout; + expect(() => validateCandidateSources(changed, sdk)).toThrow( + "SDK must belong to the runtime checkout" + ); + }); + + it("rejects an incorrect SDK subtree", () => { + const changed = structuredClone(sources); + Object.assign(changed.sdk, { path: "." }); + expect(() => validateCandidateSources(changed, sdk)).toThrow( + "SDK must identify the src/sdk subtree" + ); + }); + + it("rejects a candidate used by another SDK checkout", () => { + expect(() => validateCandidateSources(sources, otherSdkCheckout)).toThrow( + "Candidate must use this local SDK checkout" + ); + }); + + it("rejects a separate Git repository nested at the SDK path", () => { + try { + execFileSync("git", ["-C", sdk, "init", "--quiet"], { stdio: "pipe" }); + expect(() => validateCandidateSources(sources, sdk)).toThrow( + "SDK must be part of the runtime repository" + ); + } finally { + rmSync(join(sdk, ".git"), { recursive: true, force: true }); + } + }); + + it("requires the runtime repository root", () => { + const changed = structuredClone(sources); + changed.runtime.checkout = sdk; + expect(() => validateCandidateSources(changed, sdk)).toThrow( + "runtime checkout must be a repository root" + ); + }); + + it("rejects stale runtime source revisions", () => { + const changed = structuredClone(sources); + changed.runtime.commit = "0".repeat(40); + expect(() => validateCandidateSources(changed, sdk)).toThrow( + "runtime candidate source must match the local checkout" + ); + }); + + it("rejects tracked SDK edits before staging", () => { + const tracked = join(sdk, "tracked"); + try { + writeFileSync(tracked, "changed\n"); + expect(() => localCandidateSources(runtime, sdk)).toThrow( + "Commit tracked changes in runtime before attesting a source revision" + ); + } finally { + writeFileSync(tracked, "source\n"); + } + }); + + it.each([1, 2])("requires restaging legacy version %s manifests", (schemaVersion) => { + const manifest = join(work, "legacy.json"); + writeFileSync(manifest, JSON.stringify({ schemaVersion })); + expect(() => candidateHostArtifacts(manifest)).toThrow( + "Restage candidates with linked-library source provenance" + ); + }); +}); + +describe("Runtime host artifact selection", () => { + it("requires only the runtime and provider, not a companion executable", () => { + vi.stubEnv("COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST", undefined); + // Artifact-selector unit fixture only; live E2Es independently attest /proc topology. + vi.stubEnv("COPILOT_CLI_PATH", process.execPath); + vi.stubEnv("COPILOT_RUNTIME_PROVIDER_LIB", process.execPath); + expect(localHostArtifacts()).toMatchObject({ + runtimePath: process.execPath, + providerPath: process.execPath, + }); + }); + + it("requires an absolute candidate provenance manifest", () => { + expect(() => candidateHostArtifacts("candidate.json")).toThrow( + "Candidate manifest path must be absolute" + ); + }); + + it("fails closed instead of falling back when the selected candidate is missing", () => { + const missing = fileURLToPath(new URL(`./missing-${randomUUID()}.json`, import.meta.url)); + vi.stubEnv("COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST", missing); + vi.stubEnv("COPILOT_CLI_PATH", "/unrelated-development-runtime"); + vi.stubEnv("COPILOT_RUNTIME_PROVIDER_LIB", "/unrelated-development-provider"); + expect(() => localHostArtifacts()).toThrow(missing); + }); + + it.each(["COPILOT_CLI_PATH", "COPILOT_RUNTIME_PROVIDER_LIB"])( + "does not accept an npm artifact as an unattested %s", + (name) => { + vi.stubEnv("COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST", undefined); + vi.stubEnv("COPILOT_CLI_PATH", process.execPath); + vi.stubEnv("COPILOT_RUNTIME_PROVIDER_LIB", process.execPath); + vi.stubEnv( + name, + fileURLToPath(new URL("../../node_modules/tsx/dist/cli.mjs", import.meta.url)) + ); + expect(() => localHostArtifacts()).toThrow( + `${name} must not use a released runtime package` + ); + } + ); +}); diff --git a/nodejs/test/e2e/runtime_host_topology.test.ts b/nodejs/test/e2e/runtime_host_topology.test.ts new file mode 100644 index 0000000000..05611ef1c3 --- /dev/null +++ b/nodejs/test/e2e/runtime_host_topology.test.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { readlink } from "node:fs/promises"; +import { createServer } from "node:net"; +import { describe, expect, it } from "vitest"; +import { assertListenerClosed, assertRuntimeListener } from "./harness/runtimeHostTopology.js"; + +describe.skipIf(process.platform !== "linux")("Runtime host /proc evidence", () => { + it.each(["copilotd", "copilotd-test", "copilot-runtime"])( + "rejects a %s descendant independently of the advertised PID", + async (argv0) => { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const child = spawn( + process.execPath, + ["-e", "process.send('ready'); setInterval(() => {}, 1000)"], + { argv0, stdio: ["ignore", "ignore", "ignore", "ipc"] } + ); + try { + await once(child, "message"); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("No test listener"); + const runtimePath = await readlink(`/proc/${process.pid}/exe`); + await expect( + assertRuntimeListener({ url: `ws://127.0.0.1:${address.port}` }, process.pid, { + runtimePath, + providerPath: runtimePath, + embedded: true, + }) + ).rejects.toThrow("No host or second runtime child"); + } finally { + const exited = once(child, "exit"); + child.kill(); + await exited; + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + } + } + ); + + it("requires the advertised listener to be an open socket owned by the identified process", async () => { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("No test listener"); + const host = { url: `ws://127.0.0.1:${address.port}` }; + // Unit-test the OS probe with Node's own executable mapping, not a runtime substitute. + const runtimePath = await readlink(`/proc/${process.pid}/exe`); + const artifacts = { runtimePath, providerPath: runtimePath }; + try { + await expect( + assertRuntimeListener(host, process.pid, artifacts) + ).resolves.toBeUndefined(); + await expect( + assertRuntimeListener(host, process.pid, { + ...artifacts, + providerPath: "/missing-source-provider", + }) + ).rejects.toThrow("source-built provider"); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + } + await assertListenerClosed(host, process.pid); + await expect(assertRuntimeListener(host, process.pid, artifacts)).rejects.toThrow( + "own the listening TCP socket" + ); + }); + + it("rejects a different process even when it maps the expected executable and provider", async () => { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const child = spawn( + process.execPath, + ["-e", "process.send('ready'); setInterval(() => {}, 1000)"], + { stdio: ["ignore", "ignore", "ignore", "ipc"] } + ); + try { + await once(child, "message"); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("No test listener"); + const runtimePath = await readlink(`/proc/${child.pid}/exe`); + await expect( + assertRuntimeListener({ url: `ws://127.0.0.1:${address.port}` }, child.pid!, { + runtimePath, + providerPath: runtimePath, + }) + ).rejects.toThrow("own the listening TCP socket"); + } finally { + const exited = once(child, "exit"); + child.kill(); + await exited; + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + } + }); +}); 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/skill_provider.e2e.test.ts b/nodejs/test/e2e/skill_provider.e2e.test.ts new file mode 100644 index 0000000000..82d584809c --- /dev/null +++ b/nodejs/test/e2e/skill_provider.e2e.test.ts @@ -0,0 +1,450 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; +import { describe, expect, it } from "vitest"; +import { + approveAll, + RuntimeConnection, + type SessionEvent, + type SkillProvider, + type SkillProviderDescriptor, +} from "../../src/index.js"; +import { createSdkTestContext, getLegacyCliPathForTests } from "./harness/sdkTestContext.js"; +import { retry } from "./harness/sdkTestHelper.js"; + +interface ProvidedSkill { + descriptor: SkillProviderDescriptor; + read: () => string | null; +} + +/** An in-memory provider that records every callback the runtime makes. */ +class TestSkillProvider implements SkillProvider { + readonly calls: string[] = []; + + constructor(private readonly skills: ProvidedSkill[]) {} + + get reads(): string[] { + return this.calls + .filter((call) => call.startsWith("read:")) + .map((call) => call.slice("read:".length)); + } + + listSkills(): SkillProviderDescriptor[] { + this.calls.push("list"); + return this.skills.map((skill) => skill.descriptor); + } + + readSkill(name: string): string | null { + this.calls.push(`read:${name}`); + return this.skills.find((skill) => skill.descriptor.name === name)?.read() ?? null; + } +} + +function skill(name: string, description: string, markdown: string): ProvidedSkill { + return { descriptor: { name, description }, read: () => markdown }; +} + +describe("Skill providers", async () => { + const { copilotClient: client, workDir } = await createSdkTestContext(); + + it("should load provider skill lazily through skill tool", async () => { + // Body-only content: the catalog descriptor supplies all of the metadata. + const provider = new TestSkillProvider([ + skill( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n" + ), + ]); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: provider, + }); + + try { + const { skills } = await session.rpc.skills.list(); + const listed = skills.find((s) => s.name === "provider-lookup"); + expect(listed).toMatchObject({ source: "sdk", enabled: true }); + expect(listed?.path ?? "").toBe(""); + expect(provider.reads).toEqual([]); + + const message = await session.sendAndWait({ + prompt: "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + }); + + expect(provider.reads).toEqual(["provider-lookup"]); + // Validate the final assistant response arrived (guards against truncated captures) + expect(message?.data.content).toContain("TANGERINE_QUARTZ_19"); + } finally { + await session.disconnect(); + } + }); + + it("should load provider and file based skills together", async () => { + const skillsDir = path.join(workDir, "file-skills"); + fs.mkdirSync(path.join(skillsDir, "file-notes"), { recursive: true }); + fs.writeFileSync( + path.join(skillsDir, "file-notes", "SKILL.md"), + "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n" + ); + // Frontmatter may restate catalog metadata and is the only source of allowed-tools. + const provider = new TestSkillProvider([ + skill( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n" + ), + ]); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillDirectories: [skillsDir], + skillProvider: provider, + }); + + try { + const { skills } = await session.rpc.skills.list(); + const fileSkill = skills.find((s) => s.name === "file-notes"); + const providerSkill = skills.find((s) => s.name === "provider-audit"); + expect(fileSkill?.source).not.toBe("sdk"); + expect(fileSkill?.path).toBeTruthy(); + expect(providerSkill?.source).toBe("sdk"); + + const message = await session.sendAndWait({ + prompt: "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + }); + + expect(provider.reads).toEqual(["provider-audit"]); + expect(message?.data.content).toContain("MAPLE_FALCON_27"); + // Validate the final assistant response arrived (guards against truncated captures) + expect(message?.data.content).toContain("COBALT_HERON_58"); + } finally { + await session.disconnect(); + } + }); + + it("should rebind skill provider on resume", async () => { + const original = new TestSkillProvider([ + skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n" + ), + ]); + const replacement = new TestSkillProvider([ + skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n" + ), + ]); + const first = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: original, + }); + const sessionId = first.sessionId; + // A completed turn persists the session so that it can be resumed after disconnecting. + await first.sendAndWait({ + prompt: "Without using any tools or skills, reply with exactly REBIND_READY.", + }); + await first.disconnect(); + expect(original.reads).toEqual([]); + const originalCallsBeforeResume = original.calls.length; + + const session = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, + skillProvider: replacement, + }); + + try { + const message = await session.sendAndWait({ + prompt: "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + }); + + expect(replacement.reads).toEqual(["rebind-check"]); + expect(original.calls.length).toBe(originalCallsBeforeResume); + // Validate the final assistant response arrived (guards against truncated captures) + expect(message?.data.content).toContain("BRONZE_BETA_22"); + expect(message?.data.content).not.toContain("AMBER_ALPHA_11"); + } finally { + await session.disconnect(); + } + }); + + it("should report provider read failure without leaking details", async () => { + const secret = "PROVIDER_SECRET_7F3A9C"; + const provider = new TestSkillProvider([ + { + descriptor: { + name: "broken-lookup", + description: "Reports the broken lookup verification word.", + }, + read: () => { + throw new Error(`database unavailable: ${secret}`); + }, + }, + ]); + const events: SessionEvent[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: provider, + onEvent: (event) => events.push(event), + }); + + try { + const message = await session.sendAndWait({ + prompt: "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }); + + expect(provider.reads).toContain("broken-lookup"); + const failures = events.filter( + (event) => event.type === "tool.execution_complete" && !event.data.success + ); + expect(failures).toHaveLength(1); + expect(JSON.stringify(events)).not.toContain(secret); + // Validate the final assistant response arrived (guards against truncated captures) + expect(message?.data.content).toContain("LOAD_FAILED"); + } finally { + await session.disconnect(); + } + }); + + it("should report missing provider skill as not found", async () => { + const provider = new TestSkillProvider([ + { + descriptor: { + name: "vanished-lookup", + description: "Reports the vanished lookup verification word.", + }, + read: () => null, + }, + ]); + const events: SessionEvent[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: provider, + onEvent: (event) => events.push(event), + }); + + try { + const message = await session.sendAndWait({ + prompt: "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }); + + expect(provider.reads).toContain("vanished-lookup"); + const failures = events.filter( + (event) => event.type === "tool.execution_complete" && !event.data.success + ); + expect(failures).toHaveLength(1); + expect(JSON.stringify(failures[0])).toMatch(/not found/i); + // Validate the final assistant response arrived (guards against truncated captures) + expect(message?.data.content).toContain("LOAD_FAILED"); + } finally { + await session.disconnect(); + } + }); + + it("should keep provider dormant when skills disabled", async () => { + const provider = new TestSkillProvider([ + skill("dormant-lookup", "Never listed.", "Never read.\n"), + ]); + const session = await client.createSession({ + onPermissionRequest: approveAll, + enableSkills: false, + skillProvider: provider, + }); + + try { + await session.rpc.skills.ensureLoaded(); + const { skills } = await session.rpc.skills.list(); + + expect(skills.filter((s) => s.source === "sdk")).toEqual([]); + expect(provider.calls).toEqual([]); + } finally { + await session.disconnect(); + } + }); + + it("should unbind provider when resumed without one", async () => { + const provider = new TestSkillProvider([ + skill("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n"), + ]); + const first = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: provider, + }); + const before = await first.rpc.skills.list(); + expect(before.skills.some((s) => s.name === "unbound-lookup")).toBe(true); + const callsBeforeResume = provider.calls.length; + + // Resume while the provider is still bound so the unbind is observable. + const session = await client.resumeSession(first.sessionId, { + onPermissionRequest: approveAll, + }); + + try { + await session.rpc.skills.reload(); + const { skills } = await session.rpc.skills.list(); + + expect(skills.filter((s) => s.source === "sdk")).toEqual([]); + expect(provider.calls.length).toBe(callsBeforeResume); + } finally { + await session.disconnect(); + } + }); + + it("should cancel a blocked provider call when the session disconnects", async () => { + let entered!: () => void; + const started = new Promise((resolve) => (entered = resolve)); + let cancelled!: () => void; + const aborted = new Promise((resolve) => (cancelled = resolve)); + const provider: SkillProvider = { + listSkills: ({ signal }) => + new Promise((_resolve, reject) => { + signal.addEventListener( + "abort", + () => { + cancelled(); + reject(signal.reason); + }, + { once: true } + ); + entered(); + }), + readSkill: () => null, + }; + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: provider, + }); + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + void session.rpc.skills.list().catch(() => {}); + await started; + await session.disconnect(); + + let timer: NodeJS.Timeout | undefined; + await Promise.race([ + aborted, + new Promise((_resolve, reject) => { + timer = setTimeout( + () => reject(new Error("provider signal was not aborted after disconnect")), + 10_000 + ); + }), + ]).finally(() => clearTimeout(timer)); + }); + + it("should reject skill provider for cloud sessions", async () => { + const provider = new TestSkillProvider([ + skill("cloud-lookup", "Never listed.", "Never read.\n"), + ]); + + await expect( + client.createSession({ + onPermissionRequest: approveAll, + cloud: {}, + skillProvider: provider, + }) + ).rejects.toThrow("Skill providers are not supported for cloud sessions."); + + expect(provider.calls).toEqual([]); + }); +}); + +describe("Skill providers with extensions", async () => { + const cliDistDirectory = process.env.COPILOT_EXTENSION_SDK_PATH + ? path.dirname(process.env.COPILOT_EXTENSION_SDK_PATH) + : path.dirname(await getLegacyCliPathForTests()); + const sdkDistDirectory = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", + "..", + "dist" + ); + // Runs on the default runtime, which is the native runtime in CI. + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio(), + env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS" }, + extensionLaunchProvider: { + resolve: async (request) => ({ + launch: { + executable: "node", + args: [path.join(cliDistDirectory, "preloads", "extension_bootstrap.mjs")], + env: { + COPILOT_CLI_DIST_DIR: cliDistDirectory, + EXTENSION_PATH: request.modulePath, + }, + }, + }), + }, + }, + }); + + it( + "should keep provider skills when an extension joins the session", + { timeout: 120_000 }, + async () => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir }); + const provider = new TestSkillProvider([ + skill("extension-safe-lookup", "Reports the extension-safe word.", "Safe.\n"), + ]); + const session = await client.createSession({ + onPermissionRequest: approveAll, + requestExtensions: true, + extensionSdkPath: sdkDistDirectory, + skillProvider: provider, + }); + + try { + const before = await session.rpc.skills.list(); + expect(before.skills.filter((s) => s.source === "sdk").map((s) => s.name)).toEqual([ + "extension-safe-lookup", + ]); + + const extensionDir = path.join( + workDir, + ".github", + "extensions", + "provider-observer" + ); + const joinedFile = path.join(extensionDir, "joined"); + fs.mkdirSync(extensionDir, { recursive: true }); + fs.writeFileSync( + path.join(extensionDir, "extension.mjs"), + `import { writeFileSync } from "node:fs"; +import { joinSession } from "@github/copilot-sdk/extension"; +await joinSession({}); +writeFileSync(new URL("./joined", import.meta.url), "joined"); +setInterval(() => {}, 60_000); +` + ); + await session.rpc.extensions.reload(); + // An extension joins by resuming the session, which must not + // unbind the owner's provider. + await retry( + "wait for the extension to join the session", + async () => { + expect(fs.existsSync(joinedFile)).toBe(true); + }, + 300, + 100 + ); + await session.rpc.skills.reload(); + const { skills } = await session.rpc.skills.list(); + + expect(skills.filter((s) => s.source === "sdk").map((s) => s.name)).toEqual([ + "extension-safe-lookup", + ]); + } finally { + await session.disconnect(); + } + } + ); +}); diff --git a/nodejs/test/e2e/skills.e2e.test.ts b/nodejs/test/e2e/skills.e2e.test.ts index 850a8444df..422e681368 100644 --- a/nodejs/test/e2e/skills.e2e.test.ts +++ b/nodejs/test/e2e/skills.e2e.test.ts @@ -43,7 +43,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY } describe("Skill Behavior", () => { - it("discovers a new skill on the next turn and makes it available to a subagent", async () => { + it("loads a new skill after explicit reload and makes it available to a subagent", async () => { const lateSkillsDir = path.join(workDir, ".late_skills"); const skillResults: { sessionId: string; text: string }[] = []; const session = await client.createSession({ @@ -77,6 +77,19 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY "---\nname: late-skill\ndescription: Reports a unique verification word.\n---\n\nThe verification word is ORCHID_SAPPHIRE_73.\n" ); + await session.rpc.skills.ensureLoaded(); + const beforeReload = await session.rpc.skills.list(); + expect(beforeReload.skills.some((skill) => skill.name === "late-skill")).toBe( + false + ); + + const reload = await session.rpc.skills.reload(); + expect(reload.errors).toEqual([]); + const afterReload = await session.rpc.skills.list(); + expect( + afterReload.skills.find((skill) => skill.name === "late-skill")?.enabled + ).toBe(true); + const second = await session.sendAndWait({ prompt: "Use the skill tool to load late-skill, then use the task tool to ask a task agent to load late-skill with the skill tool and report its verification word. Report the agent's word.", }); diff --git a/nodejs/test/e2e/sse_terminals.e2e.test.ts b/nodejs/test/e2e/sse_terminals.e2e.test.ts new file mode 100644 index 0000000000..77793b7608 --- /dev/null +++ b/nodejs/test/e2e/sse_terminals.e2e.test.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import http from "node:http"; +import { afterAll, describe, expect, it } from "vitest"; +import { approveAll, type SessionEvent } from "../../src/index.js"; +import { withTestCleanup } from "../helpers/withTestCleanup.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { createCompletedResponse } from "./harness/webSocketResponsesServer.js"; + +const FINAL_OUTPUT = "SSE_INCOMPLETE_TERMINAL_DONE"; +const pollOptions = { timeout: 10_000 }; + +describe("SSE incomplete terminals", async () => { + const release = Promise.withResolvers(); + const responses: http.ServerResponse[] = []; + const errors: unknown[] = []; + // Native responses_transport tests own the envelope/output matrix. This case + // proves the held-open HTTP stream crosses the subprocess SDK boundary. + const server = http.createServer((req, res) => { + void (async () => { + if (req.method !== "POST" || req.url !== "/v1/responses") { + throw new Error(`Unexpected provider request: ${req.method} ${req.url}`); + } + req.setEncoding("utf8"); + let body = ""; + for await (const chunk of req) body += chunk; + const request: unknown = JSON.parse(body); + if (typeof request !== "object" || request === null) { + throw new Error("Expected a Responses request object."); + } + if (!("stream" in request) || request.stream !== true) { + res.writeHead(200, { + "content-type": "application/json", + "x-content-type-options": "nosniff", + }); + res.end(JSON.stringify(createCompletedResponse("Test session", "resp-title"))); + return; + } + responses.push(res); + res.on("error", (error) => errors.push(error)); + res.writeHead(200, { "content-type": "text/event-stream" }); + res.write( + `data: ${JSON.stringify({ + type: "response.created", + response: { id: "resp-incomplete", status: "in_progress", output: [] }, + })}\n\n` + ); + await release.promise; + res.write( + `data: ${JSON.stringify({ + type: "response.completed", + response: { + ...createCompletedResponse(FINAL_OUTPUT, "resp-incomplete"), + status: "incomplete", + // Isolate draining from the existing length-continuation policy. + incomplete_details: { reason: "content_filter" }, + }, + })}\n\n` + ); + // Neither [DONE] nor res.end(): only the runtime can retire this stream. + })().catch((error: unknown) => { + errors.push(error); + res.destroy(error instanceof Error ? error : new Error(String(error))); + }); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + afterAll(async () => { + release.resolve(); + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + if (errors.length > 0) throw new AggregateError(errors, "SSE provider fixture failed."); + }); + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Expected a bound TCP address for the SSE provider."); + } + const { copilotClient: client } = await createSdkTestContext({ + useStdio: true, + copilotClientOptions: { gitHubToken: DEFAULT_GITHUB_TOKEN }, + }); + + it("finishes a completed-envelope incomplete response without waiting for peer EOF", async () => { + const session = await client.createSession({ + model: "gpt-5-responses", + streaming: true, + provider: { + type: "openai", + baseUrl: `http://127.0.0.1:${address.port}/v1`, + apiKey: "test-provider-key", + wireApi: "responses", + transport: "http", + }, + onPermissionRequest: approveAll, + }); + const events: SessionEvent[] = []; + const unsubscribe = session.on((event) => events.push(event)); + await withTestCleanup( + async () => { + await session.send({ prompt: "Return the controlled partial answer." }); + await expect + .poll( + () => ({ + requests: responses.length, + errors: events + .filter((event) => event.type === "session.error") + .map((event) => event.data.message), + }), + pollOptions + ) + .toEqual({ requests: 1, errors: [] }); + expect(responses[0].writableEnded).toBe(false); + expect(events.filter((event) => event.type === "assistant.message")).toEqual([]); + await session.rpc.mode.get(); + release.resolve(); + + await expect + .poll( + () => + events.filter((event) => event.type === "assistant.message").at(-1) + ?.data.content, + pollOptions + ) + .toBe(FINAL_OUTPUT); + await expect + .poll(() => { + const finalMessage = events.findLastIndex( + (event) => + event.type === "assistant.message" && + event.data.content === FINAL_OUTPUT + ); + return ( + finalMessage !== -1 && + events + .slice(finalMessage + 1) + .some((event) => event.type === "session.idle") + ); + }, pollOptions) + .toBe(true); + expect(responses).toHaveLength(1); + expect(responses[0].writableEnded).toBe(false); + await expect.poll(() => responses[0].destroyed, pollOptions).toBe(true); + expect(events.filter((event) => event.type === "session.error")).toEqual([]); + }, + () => release.resolve(), + unsubscribe, + () => session.disconnect(), + async () => { + const errors = await client.stop(); + if (errors.length > 0) { + throw new AggregateError(errors, "SSE client cleanup failed"); + } + } + ); + }); +}); 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..ae08a9d16b 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() @@ -201,6 +207,12 @@ describe("Structured output", async () => { const session = await client.createSession({ model, provider, + ...(model === "hydrafusion" + ? { + enableExperimentalMode: true, + featureFlags: { HYDRAFUSION: true, HYDRAFUSION_ROLLOUT: true }, + } + : {}), onPermissionRequest: approveAll, availableTools: [], }); @@ -247,7 +259,7 @@ describe("Structured output", async () => { releaseHook = resolve; }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -319,7 +331,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 +359,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 +374,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 +406,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 +424,7 @@ describe("Structured output", async () => { releaseTool = resolve; }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -469,7 +481,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 9fb89fc0d7..49d7e66a26 100644 --- a/nodejs/test/e2e/telemetry.e2e.test.ts +++ b/nodejs/test/e2e/telemetry.e2e.test.ts @@ -2,12 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { readFile } from "fs/promises"; +import { readFile, writeFile } from "fs/promises"; 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; @@ -55,8 +55,13 @@ describe("Telemetry export", async () => { const telemetryFileName = `telemetry-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`; - const { copilotClient: client, workDir } = await createSdkTestContext({ + const { + copilotClient: client, + createClient, + 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 @@ -73,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: [ @@ -84,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"); @@ -113,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; @@ -131,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( @@ -152,4 +185,105 @@ describe("Telemetry export", async () => { ); expect(getStringAttribute(toolSpan!, "gen_ai.tool.call.result")).toBe(marker); }); + + it("should export per request subagent chat spans", { timeout: 90_000 }, async () => { + const sourceName = "ts-sdk-subagent-telemetry-e2e"; + const telemetryFileName = "subagent-telemetry.jsonl"; + const prompt = + "Use the task tool in sync mode to ask a task agent to read subagent-otel.txt with the view tool. Then reply with SUBAGENT_OTEL_DONE."; + await writeFile(join(workDir, "subagent-otel.txt"), "SUBAGENT_OTEL_FILE_CONTENT"); + + const subagentClient = createClient({ + connection: RuntimeConnection.forStdio(), + gitHubToken: DEFAULT_GITHUB_TOKEN, + telemetry: { + filePath: telemetryFileName, + exporterType: "file", + sourceName, + captureContent: true, + }, + }); + 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 { + await session.disconnect(); + } + } finally { + await subagentClient.stop(); + } + + const spans = (await readTelemetryEntries(join(workDir, telemetryFileName))).filter( + (entry) => entry.type === "span" + ); + expect(spans.every((span) => span.instrumentationScope?.name === sourceName)).toBe(true); + expect(spans.every((span) => span.status?.code !== 2)).toBe(true); + const invocationSpans = spans.filter( + (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]!; + expect(getStringAttribute(root, "gen_ai.conversation.id")).toBe(sessionId!); + expect(root.spanId).toBeTruthy(); + expect(root.traceId).toBeTruthy(); + + const taskTools = spans.filter( + (span) => + getStringAttribute(span, "gen_ai.operation.name") === "execute_tool" && + getStringAttribute(span, "gen_ai.tool.name") === "task" + ); + expect(taskTools).toHaveLength(1); + const taskTool = taskTools[0]!; + expect(taskTool.parentSpanId).toBe(root.spanId); + const subagents = invocationSpans.filter((span) => span.parentSpanId === taskTool.spanId); + expect(subagents).toHaveLength(1); + const subagent = subagents[0]!; + expect(taskTool.traceId).toBe(root.traceId); + expect(subagent.traceId).toBe(root.traceId); + + const chats = spans.filter( + (span) => getStringAttribute(span, "gen_ai.operation.name") === "chat" + ); + expect(chats).toHaveLength(4); + const parentChats = chats.filter((span) => span.parentSpanId === root.spanId); + expect(parentChats).toHaveLength(2); + for (const chat of parentChats) { + expect(chat.traceId).toBe(root.traceId); + } + const childChats = chats.filter((span) => span.parentSpanId === subagent.spanId); + expect(childChats).toHaveLength(2); + for (const chat of childChats) { + expect(chat.traceId).toBe(root.traceId); + expect(getStringAttribute(chat, "github.copilot.initiator")).toBe("sub-agent"); + } + const requestingChats = childChats.filter((span) => + (getStringAttribute(span, "gen_ai.output.messages") ?? "").includes('"view"') + ); + expect(requestingChats).toHaveLength(1); + expect( + getStringAttribute(requestingChats[0]!, "gen_ai.input.messages") ?? "" + ).not.toContain("SUBAGENT_OTEL_FILE_CONTENT"); + const finalChats = childChats.filter((span) => + (getStringAttribute(span, "gen_ai.output.messages") ?? "").includes( + "SUBAGENT_OTEL_CHILD_DONE" + ) + ); + expect(finalChats).toHaveLength(1); + expect(getStringAttribute(finalChats[0]!, "gen_ai.input.messages")).toContain( + "SUBAGENT_OTEL_FILE_CONTENT" + ); + }); }); 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/websocket_terminals.e2e.test.ts b/nodejs/test/e2e/websocket_terminals.e2e.test.ts new file mode 100644 index 0000000000..f94052c5d4 --- /dev/null +++ b/nodejs/test/e2e/websocket_terminals.e2e.test.ts @@ -0,0 +1,262 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { afterAll, describe, expect, it } from "vitest"; +import { approveAll, type SessionEvent } from "../../src/index.js"; +import { withTestCleanup } from "../helpers/withTestCleanup.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { + createCompletedResponse, + startWebSocketResponsesServer, + type WebSocketEnvelope, +} from "./harness/webSocketResponsesServer.js"; + +const WARMUP = "WS_TERMINAL_WARMUP_DONE"; +const RECOVERED = "WS_TERMINAL_RECOVERY_DONE"; +const QUEUED = "WS_TERMINAL_QUEUED_WORK_DONE"; +const FAILURE = "Controlled terminal provider failure"; +const REJECTED_OUTPUT = "THIS_FAILED_RESPONSE_IS_NOT_SUCCESS"; +const shellToolName = process.platform === "win32" ? "powershell" : "bash"; +const pollOptions = { timeout: 10_000 }; + +describe.each(["failed", "incomplete"] as const)( + "WebSocket response.%s terminals", + async (terminalType) => { + const upstream = await startWebSocketResponsesServer(); + // Register first so the SDK context stops its client and proxy before this provider. + afterAll(() => upstream.stop()); + const { copilotClient: client, openAiEndpoint } = await createSdkTestContext({ + // This regression exercises the supported subprocess SDK boundary. + useStdio: true, + copilotClientOptions: { + gitHubToken: DEFAULT_GITHUB_TOKEN, + env: { + COPILOT_API_URL: upstream.baseUrl, + COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES: "false", + WEBSOCKET_RESPONSES_PERSISTENT: "true", + COPILOT_EXP_COPILOT_CLI_WEBSOCKET_RESPONSES_PERSISTENT: "true", + }, + }, + }); + upstream.forwardHttpTo(openAiEndpoint.url); + + it("retires a reused socket without peer EOF and resumes subsequent or queued work", async () => { + const { state } = upstream; + const terminal = Promise.withResolvers(); + state.responses.push( + { + type: "response.completed", + response: createCompletedResponse(WARMUP, "resp-warmup"), + }, + () => terminal.promise, + { + type: "response.completed", + response: createCompletedResponse(RECOVERED, "resp-recovered"), + } + ); + if (terminalType === "incomplete") { + state.responses.push({ + type: "response.completed", + response: createCompletedResponse(QUEUED, "resp-queued"), + }); + } + const session = await client.createSession({ + model: "gpt-5-responses", + onPermissionRequest: approveAll, + }); + const events: SessionEvent[] = []; + const unsubscribe = session.on((event) => events.push(event)); + await withTestCleanup( + async () => { + const warmup = await session.sendAndWait({ + prompt: "Return the warmup response.", + }); + expect(warmup?.data.content).toBe(WARMUP); + await expect + .poll( + () => events.filter((event) => event.type === "session.idle").length, + pollOptions + ) + .toBe(1); + const interruptedMessageId = await session.send({ + prompt: "Exercise the controlled terminal response.", + }); + let recoveryMessageId = interruptedMessageId; + let queuedMessageId: string | undefined; + const queuedPrompt = "Run the queued follow-up."; + await expect.poll(() => state.messages.length, pollOptions).toBe(2); + expect(state.messageConnectionIds).toEqual([1, 1]); + expect(state.messages[1].previous_response_id).toBe("resp-warmup"); + expect(state.openConnectionIds.has(1)).toBe(true); + await session.rpc.mode.get(); + if (terminalType === "incomplete") { + queuedMessageId = await session.send({ + prompt: queuedPrompt, + mode: "enqueue", + }); + expect(state.messages).toHaveLength(2); + } + + // Release only the terminal envelope; the peer stays open until the runtime retires it. + terminal.resolve( + terminalType === "failed" + ? { + type: "response.failed", + response: { + ...createCompletedResponse(REJECTED_OUTPUT, "resp-failed"), + status: "failed", + error: FAILURE, + code: "invalid_prompt", + param: "input", + request_id: "terminal-request", + }, + } + : { + type: "response.incomplete", + response: { + ...createCompletedResponse("", "resp-incomplete"), + status: "incomplete", + incomplete_details: { reason: "max_output_tokens" }, + output: [ + { + type: "function_call", + id: "fc-partial", + call_id: "call-partial", + name: shellToolName, + arguments: JSON.stringify({ + command: "echo WS_PARTIAL_TOOL_MUST_NOT_EXECUTE", + description: "Print a controlled test marker", + }), + status: "incomplete", + }, + ], + }, + } + ); + + if (terminalType === "failed") { + await expect + .poll( + () => + events.some( + (event) => + event.type === "session.error" && + event.data.message.includes(FAILURE) + ), + pollOptions + ) + .toBe(true); + await expect + .poll( + () => + events.filter((event) => event.type === "session.idle").length, + pollOptions + ) + .toBe(2); + expect(state.messages).toHaveLength(2); + recoveryMessageId = await session.send({ + prompt: "Recover after the provider error.", + }); + } + // Validate the final assistant response arrived (guards against truncated captures) + if (terminalType === "incomplete") { + await expect + .poll( + () => + events + .filter((event) => event.type === "assistant.message") + .at(-1)?.data.content, + pollOptions + ) + .toBe(QUEUED); + } + await expect + .poll( + () => + events.some( + (event) => + event.type === "assistant.message" && + event.data.content === RECOVERED && + event.data.originatingMessageId === recoveryMessageId + ), + pollOptions + ) + .toBe(true); + await expect + .poll(() => { + const finalMessage = events.findLastIndex( + (event) => + event.type === "assistant.message" && + event.data.content === + (terminalType === "failed" ? RECOVERED : QUEUED) + ); + return ( + finalMessage !== -1 && + events + .slice(finalMessage + 1) + .some((event) => event.type === "session.idle") + ); + }, pollOptions) + .toBe(true); + expect(state.messageConnectionIds).toEqual( + terminalType === "failed" ? [1, 1, 2] : [1, 1, 2, 2] + ); + await expect.poll(() => state.closedConnectionCount, pollOptions).toBe(1); + expect(state.openConnectionIds.has(1)).toBe(false); + expect(state.messages[2].previous_response_id).toBeUndefined(); + expect(state.responses).toHaveLength(0); + expect(events.filter((event) => event.type === "tool.execution_start")).toEqual( + [] + ); + expect( + events.some( + (event) => + event.type === "assistant.message" && + event.data.content === REJECTED_OUTPUT + ) + ).toBe(false); + expect(JSON.stringify(state.messages[2].input)).not.toContain(REJECTED_OUTPUT); + const recovered = events.find( + (event) => + event.type === "assistant.message" && event.data.content === RECOVERED + ); + expect(recovered).toMatchObject({ + data: { originatingMessageId: recoveryMessageId }, + }); + if (terminalType === "failed") { + expect( + events.filter((event) => event.type === "model.call_failure") + ).toHaveLength(1); + } else { + expect(events.filter((event) => event.type === "session.error")).toEqual( + [] + ); + expect(JSON.stringify(state.messages[2].input)).not.toContain( + "call-partial" + ); + expect(JSON.stringify(state.messages[2].input)).not.toContain(queuedPrompt); + expect( + JSON.stringify(state.messages[3].input).split(queuedPrompt) + ).toHaveLength(2); + expect( + events.find( + (event) => + event.type === "assistant.message" && + event.data.content === QUEUED + ) + ).toMatchObject({ data: { originatingMessageId: queuedMessageId } }); + } + }, + unsubscribe, + () => session.disconnect(), + async () => { + const errors = await client.stop(); + if (errors.length > 0) { + throw new AggregateError(errors, "WebSocket client cleanup failed"); + } + } + ); + }); + } +); diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/workflow.e2e.test.ts similarity index 51% rename from nodejs/test/e2e/factory.e2e.test.ts rename to nodejs/test/e2e/workflow.e2e.test.ts index 5b5796e4e3..66718c35c2 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/workflow.e2e.test.ts @@ -22,11 +22,13 @@ const cliPath = process.env.COPILOT_CLI_PATH ?? (await getLegacyCliPathForTests( const cliDistDirectory = process.env.COPILOT_EXTENSION_SDK_PATH ? dirname(process.env.COPILOT_EXTENSION_SDK_PATH) : dirname(cliPath); -const factoryTestContext = await createSdkTestContext({ +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) => ({ @@ -43,20 +45,20 @@ const factoryTestContext = await createSdkTestContext({ }, }); -async function setupFactoryExtension(workDir: string, onPermissionRequest = approveAll) { - const { copilotClient, openAiEndpoint } = factoryTestContext; - const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); +async function setupWorkflowExtension(workDir: string, onPermissionRequest = approveAll) { + const { copilotClient, openAiEndpoint } = workflowTestContext; + const extensionDir = join(workDir, ".github", "extensions", "workflow-smoke"); const readyFile = join(extensionDir, "ready"); await rm(join(workDir, ".github"), { recursive: true, force: true }); await mkdir(extensionDir, { recursive: true }); await copyFile( - join(__dirname, "fixtures", "factory-extension.mjs"), + join(__dirname, "fixtures", "workflow-extension.mjs"), join(extensionDir, "extension.mjs") ); execFileSync("git", ["init", "--quiet"], { cwd: workDir }); await openAiEndpoint.setCopilotUserByToken(DEFAULT_GITHUB_TOKEN, { - login: "factory-e2e-user", + login: "workflow-e2e-user", copilot_plan: "individual_pro", token_based_billing: true, is_mcp_enabled: true, @@ -64,7 +66,7 @@ async function setupFactoryExtension(workDir: string, onPermissionRequest = appr api: openAiEndpoint.url, telemetry: "https://localhost:1/telemetry", }, - analytics_tracking_id: "e2e-test-tracking-id", + analytics_tracking_id: "workflow-e2e-tracking-id", }); const session = await copilotClient.createSession({ @@ -77,48 +79,6 @@ async function setupFactoryExtension(workDir: string, onPermissionRequest = appr }), }); - await retry( - "wait for the factory extension to join the session", - async () => { - expect(existsSync(readyFile)).toBe(true); - }, - 300, - 100 - ); - - return session; -} - -async function setupWorkflowExtension(workDir: string) { - const { copilotClient, openAiEndpoint } = factoryTestContext; - const extensionDir = join(workDir, ".github", "extensions", "workflow-smoke"); - const readyFile = join(extensionDir, "ready"); - await rm(join(workDir, ".github"), { recursive: true, force: true }); - await mkdir(extensionDir, { recursive: true }); - await copyFile( - join(__dirname, "fixtures", "workflow-extension.mjs"), - join(extensionDir, "extension.mjs") - ); - execFileSync("git", ["init", "--quiet"], { cwd: workDir }); - - await openAiEndpoint.setCopilotUserByToken(DEFAULT_GITHUB_TOKEN, { - login: "workflow-e2e-user", - copilot_plan: "individual_pro", - token_based_billing: true, - is_mcp_enabled: true, - endpoints: { - api: openAiEndpoint.url, - telemetry: "https://localhost:1/telemetry", - }, - analytics_tracking_id: "workflow-e2e-tracking-id", - }); - - const session = await copilotClient.createSession({ - requestExtensions: true, - extensionSdkPath: resolve(__dirname, "..", "..", "dist"), - onPermissionRequest: approveAll, - }); - try { await retry( "wait for the workflow extension to join the session", @@ -135,23 +95,8 @@ async function setupWorkflowExtension(workDir: string) { } } -it("runs an extension-authored factory across the SDK process boundary", async () => { - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("argument-echo", { - args: { source: "sdk-e2e", count: 11 }, - notifyOnComplete: false, - }); - - expect(result).toMatchObject({ - status: "completed", - result: { source: "sdk-e2e", count: 11 }, - }); -}); - it("runs an extension-authored workflow across the SDK process boundary", async () => { - const { workDir } = factoryTestContext; + const { workDir } = workflowTestContext; await using session = await setupWorkflowExtension(workDir); const result = await session.workflow.run("argument-echo", { @@ -164,21 +109,13 @@ it("runs an extension-authored workflow across the SDK process boundary", async status: "completed", }); expect(result.result).toEqual({ source: "sdk-workflow-e2e", count: 12 }); -}, 45_000); +}, 60_000); -// TODO(cli-1.0.81-2): the subagent request is rejected downstream under CLI 1.0.81-2, so the -// fixture reports didThrow: true. Re-enable once the runtime fix ships. -// -// The timeout is generous because the factory abandons its subagent once the runtime has -// accepted the request, so the run settles only after the runtime drains that work. it.skip("forwards every declared subagent option to the runtime", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const result = await session.factory.run("forwards-subagent-options"); + const result = await session.workflow.run("forwards-subagent-options"); expect(result).toMatchObject({ status: "completed", @@ -186,44 +123,38 @@ it.skip("forwards every declared subagent option to the runtime", async () => { }); }, 60_000); -it("throws FactoryResumeError with not_found for an unknown run", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("throws WorkflowResumeError with not_found for an unknown run", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const error = await session.factory + const error = await session.workflow .resume("00000000-0000-0000-0000-000000000000") .catch((caught: unknown) => caught); expect(error).toMatchObject({ - name: "FactoryResumeError", + name: "WorkflowResumeError", code: "not_found", }); }); -it("throws FactoryResumeError with non_resumable for a completed run", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("throws WorkflowResumeError with non_resumable for a completed run", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const run = await session.factory.run("argument-echo", { notifyOnComplete: false }); - const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught); + const run = await session.workflow.run("argument-echo", { notifyOnComplete: false }); + const error = await session.workflow.resume(run.runId).catch((caught: unknown) => caught); expect(error).toMatchObject({ - name: "FactoryResumeError", + name: "WorkflowResumeError", code: "non_resumable", }); }); -it("forwards factory runtime controls across the SDK process boundary", async () => { - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("forwards workflow runtime controls across the SDK process boundary", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const suppressed = await session.factory.run("phased", { + const suppressed = await session.workflow.run("phased", { notifyOnComplete: false, logPhaseNames: false, }); @@ -232,7 +163,7 @@ it("forwards factory runtime controls across the SDK process boundary", async () status: "completed", result: "finished", }); - const progress = await session.factory.getRunProgress(suppressed.runId); + const progress = await session.workflow.getRunProgress(suppressed.runId); expect(progress.records).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: "phase", text: "Collect" }), @@ -247,35 +178,35 @@ it("forwards factory runtime controls across the SDK process boundary", async () events.some( (event) => event.type === "system.notification" && - event.data.kind.type === "factory_completed" && + event.data.kind.type === "workflow_completed" && event.data.kind.runId === suppressed.runId ) ).toBe(false); expect( events.filter( - (event) => event.type === "session.info" && event.data.infoType === "factory_phase" + (event) => event.type === "session.info" && event.data.infoType === "workflow_phase" ) ).toEqual([]); }); -it("pages factory runs and returns cursor metadata", async () => { - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("pages workflow runs and returns cursor metadata", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const first = await session.factory.run("argument-echo", { + const first = await session.workflow.run("argument-echo", { args: { ordinal: 1 }, notifyOnComplete: false, }); - const second = await session.factory.run("argument-echo", { + const second = await session.workflow.run("argument-echo", { args: { ordinal: 2 }, notifyOnComplete: false, }); - const third = await session.factory.run("argument-echo", { + const third = await session.workflow.run("argument-echo", { args: { ordinal: 3 }, notifyOnComplete: false, }); - const newest = await session.factory.listRuns({ limit: 1 }); + const newest = await session.workflow.listRuns({ limit: 1 }); expect(newest).toMatchObject({ runs: [expect.objectContaining({ runId: third.runId })], hasMoreNewer: false, @@ -284,7 +215,7 @@ it("pages factory runs and returns cursor metadata", async () => { expect(newest.oldestSeq).toBe(newest.newestSeq); expect(newest.oldestSeq).not.toBeNull(); - const older = await session.factory.listRuns({ + const older = await session.workflow.listRuns({ beforeSeq: newest.oldestSeq!, limit: 1, }); @@ -294,7 +225,7 @@ it("pages factory runs and returns cursor metadata", async () => { omittedOlder: 1, }); - const oldest = await session.factory.listRuns({ + const oldest = await session.workflow.listRuns({ beforeSeq: older.oldestSeq!, limit: 1, }); @@ -305,37 +236,31 @@ it("pages factory runs and returns cursor metadata", async () => { }); }); -it("runs a factory when its session denies every permission request", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; +it("runs a workflow when its session denies every permission request", async () => { + const { workDir } = workflowTestContext; const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); - await using session = await setupFactoryExtension(workDir, denyPermissions); + await using session = await setupWorkflowExtension(workDir, denyPermissions); await expect( - session.factory.run("argument-echo", { notifyOnComplete: false }) + session.workflow.run("argument-echo", { notifyOnComplete: false }) ).resolves.toMatchObject({ status: "completed", }); expect(denyPermissions).not.toHaveBeenCalled(); }); -it("resumes a failed factory when its session denies every permission request", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; +it("resumes a failed workflow when its session denies every permission request", async () => { + const { workDir } = workflowTestContext; const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); - await using session = await setupFactoryExtension(workDir, denyPermissions); + await using session = await setupWorkflowExtension(workDir, denyPermissions); - const failedRun = await session.factory.run("fails-once", { notifyOnComplete: false }); + const failedRun = await session.workflow.run("fails-once", { notifyOnComplete: false }); expect(failedRun).toMatchObject({ status: "error", }); await expect( - session.factory.resume(failedRun.runId, { + session.workflow.resume(failedRun.runId, { notifyOnComplete: false, logPhaseNames: false, }) @@ -346,19 +271,16 @@ it("resumes a failed factory when its session denies every permission request", expect(denyPermissions).not.toHaveBeenCalled(); }); -it("pauses a running factory through the session API", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); - await using session = await setupFactoryExtension(workDir); +it("pauses a running workflow through the session API", async () => { + const { workDir } = workflowTestContext; + const extensionDir = join(workDir, ".github", "extensions", "workflow-smoke"); + await using session = await setupWorkflowExtension(workDir); - const execution = session.factory.run("externally-paused", { + const execution = session.workflow.run("externally-paused", { notifyOnComplete: false, }); await retry( - "wait for the externally paused factory to enter its body", + "wait for the externally paused workflow to enter its body", async () => { expect(existsSync(join(extensionDir, "external-pause-entered"))).toBe(true); }, @@ -368,10 +290,10 @@ it("pauses a running factory through the session API", async () => { let runId: string | undefined; await retry( - "find the running factory before pausing it", + "find the running workflow before pausing it", async () => { - const running = (await session.factory.listRuns()).find( - (run) => run.factoryName === "externally-paused" && run.status === "running" + const running = (await session.workflow.listRuns()).find( + (run) => run.workflowName === "externally-paused" && run.status === "running" ); expect(running).toBeDefined(); runId = running?.runId; @@ -380,10 +302,10 @@ it("pauses a running factory through the session API", async () => { 100 ); if (!runId) { - throw new Error("Running factory did not expose a run ID"); + throw new Error("Running workflow did not expose a run ID"); } - await expect(session.factory.pause(runId)).resolves.toMatchObject({ + await expect(session.workflow.pause(runId)).resolves.toMatchObject({ runId, status: "paused", }); @@ -394,21 +316,18 @@ it("pauses a running factory through the session API", async () => { }); it("pauses once at a durable checkpoint and continues after resume", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); - await using session = await setupFactoryExtension(workDir); + const { workDir } = workflowTestContext; + const extensionDir = join(workDir, ".github", "extensions", "workflow-smoke"); + await using session = await setupWorkflowExtension(workDir); - const paused = await session.factory.run("durable-pause-checkpoint", { + const paused = await session.workflow.run("durable-pause-checkpoint", { notifyOnComplete: false, }); expect(paused).toMatchObject({ status: "paused" }); expect(readFileSync(join(extensionDir, "checkpoint-attempts"))).toHaveLength(1); expect(readFileSync(join(extensionDir, "checkpoint-preparations"))).toHaveLength(1); - const resumed = await session.factory.resume(paused.runId, { + const resumed = await session.workflow.resume(paused.runId, { notifyOnComplete: false, }); expect(resumed).toMatchObject({ @@ -420,53 +339,44 @@ it("pauses once at a durable checkpoint and continues after resume", async () => expect(readFileSync(join(extensionDir, "checkpoint-preparations"))).toHaveLength(1); }); -it("refuses a factory started through the context session from a factory body", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("refuses a workflow started through the context session from a workflow body", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const result = await session.factory.run("starts-from-context-session", { + const result = await session.workflow.run("starts-from-context-session", { notifyOnComplete: false, }); expect(result).toMatchObject({ status: "completed", - result: expect.stringContaining("factory.run, factory.resume, and factory.pause"), + result: expect.stringContaining("workflow.run, workflow.resume, and workflow.pause"), }); - expect((result as { result: string }).result).toContain("factory body"); + expect((result as { result: string }).result).toContain("workflow body"); }); -it("refuses a factory started through the module session from a factory body", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("refuses a workflow started through the module session from a workflow body", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const result = await session.factory.run("starts-from-module-session", { + const result = await session.workflow.run("starts-from-module-session", { notifyOnComplete: false, }); expect(result).toMatchObject({ status: "completed", - result: expect.stringContaining("factory.run, factory.resume, and factory.pause"), + result: expect.stringContaining("workflow.run, workflow.resume, and workflow.pause"), }); - expect((result as { result: string }).result).toContain("factory body"); + expect((result as { result: string }).result).toContain("workflow body"); }); -it("allows a module-level extension watcher to start a factory while another body is parked", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); - await using session = await setupFactoryExtension(workDir); +it("allows a module-level extension watcher to start a workflow while another body is parked", async () => { + const { workDir } = workflowTestContext; + const extensionDir = join(workDir, ".github", "extensions", "workflow-smoke"); + await using session = await setupWorkflowExtension(workDir); - const parked = session.factory.run("parked", { notifyOnComplete: false }); + const parked = session.workflow.run("parked", { notifyOnComplete: false }); await retry( - "wait for the parked factory to enter its body", + "wait for the parked workflow to enter its body", async () => { expect(existsSync(join(extensionDir, "entered"))).toBe(true); }, @@ -477,7 +387,7 @@ it("allows a module-level extension watcher to start a factory while another bod writeFileSync(join(extensionDir, "start-b"), "start"); const bResultFile = join(extensionDir, "b-result"); await retry( - "wait for the module-level watcher factory run to succeed", + "wait for the module-level watcher workflow run to succeed", async () => { expect(existsSync(bResultFile)).toBe(true); expect(JSON.parse(readFileSync(bResultFile, "utf8"))).toMatchObject({ @@ -499,14 +409,11 @@ it("allows a module-level extension watcher to start a factory while another bod }); }, 60_000); -it("returns an array result from an extension-authored factory", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("returns an array result from an extension-authored workflow", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); - const result = await session.factory.run("array-result", { notifyOnComplete: false }); + const result = await session.workflow.run("array-result", { notifyOnComplete: false }); expect(result).toMatchObject({ status: "completed", @@ -514,15 +421,12 @@ it("returns an array result from an extension-authored factory", async () => { }); }); -it("passes array factory arguments across the SDK process boundary", async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); +it("passes array workflow arguments across the SDK process boundary", async () => { + const { workDir } = workflowTestContext; + await using session = await setupWorkflowExtension(workDir); const args = [1, "two", false]; - const result = await session.factory.run("argument-echo", { + const result = await session.workflow.run("argument-echo", { args, notifyOnComplete: false, }); diff --git a/nodejs/test/e2e/workiq_discovery.e2e.test.ts b/nodejs/test/e2e/workiq_discovery.e2e.test.ts new file mode 100644 index 0000000000..249d982770 --- /dev/null +++ b/nodejs/test/e2e/workiq_discovery.e2e.test.ts @@ -0,0 +1,443 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import { join } from "node:path"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { approveAll, RuntimeConnection, CopilotClient } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; +import { ConnectProxy } from "../../../test/harness/connectProxy.js"; +import { createE2eRequestHandler } from "../../../test/harness/mockHandlers.js"; + +describe("account-derived WorkIQ", async () => { + const { env, workDir, openAiEndpoint } = await createSdkTestContext(); + + it("discovers disabled WorkIQ without requesting authentication or persisting a server", async () => { + const profile = join(workDir, "workiq-profile"); + await mkdir(profile, { recursive: true }); + await writeFile( + join(profile, "config.json"), + JSON.stringify({ + loggedInUsers: [ + { + host: "https://substrate.office.com", + login: "workiq-test-account", + kind: "loki", + derivedFrom: "entra:workiq-account-a", + }, + ], + }) + ); + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + workingDirectory: workDir, + env: { ...env, COPILOT_HOME: profile, COPILOT_DISABLE_KEYTAR: "1" }, + }); + onTestFinished(() => client.stop()); + let authenticationRequests = 0; + const session = await client.createSession({ + configDirectory: profile, + onPermissionRequest: approveAll, + onMcpAuthRequest: async () => { + authenticationRequests++; + return { kind: "cancelled" }; + }, + }); + const accounts = await session.rpc.accounts.enumerate({ query: { kind: "accounts" } }); + expect(accounts.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "entra", selectionId: "entra:workiq-account-a" }), + ]) + ); + const discovered = await session.rpc.mcp.list(); + expect(discovered.servers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "WorkIQ", + source: "account", + status: "disabled", + url: "https://workiq.svc.cloud.microsoft/mcp", + }), + ]) + ); + expect(discovered).not.toHaveProperty("accountServerConfigs"); + await session.rpc.mcp.reload(); + const reloaded = await session.rpc.mcp.list(); + expect(reloaded.servers.find((server) => server.name === "WorkIQ")?.status).toBe( + "disabled" + ); + expect(authenticationRequests).toBe(0); + await expect(readFile(join(profile, "mcp-config.json"))).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("preserves an explicitly configured WorkIQ alias instead of adding another server", async () => { + const profile = join(workDir, "workiq-override-profile"); + await mkdir(profile, { recursive: true }); + await writeFile( + join(profile, "config.json"), + JSON.stringify({ + loggedInUsers: [ + { + host: "https://substrate.office.com", + login: "workiq-test-account", + kind: "loki", + derivedFrom: "entra:workiq-account-a", + }, + ], + }) + ); + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + workingDirectory: workDir, + env: { ...env, COPILOT_HOME: profile, COPILOT_DISABLE_KEYTAR: "1" }, + }); + onTestFinished(() => client.stop()); + const session = await client.createSession({ + configDirectory: profile, + onPermissionRequest: approveAll, + mcpServers: { + "my-workiq": { + type: "http", + url: "https://workiq.svc.cloud.microsoft/mcp", + tools: ["chosen-tool"], + }, + "my-sse": { + type: "sse", + url: "https://example.test/events", + tools: ["*"], + }, + "my-local": { + type: "stdio", + command: "unused-while-disabled", + tools: ["*"], + }, + }, + disabledMcpServers: ["my-workiq", "my-sse", "my-local"], + }); + const result = await session.rpc.mcp.list(); + expect(result.servers.some((server) => server.name === "WorkIQ")).toBe(false); + expect(result.servers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "my-workiq", + status: "disabled", + url: "https://workiq.svc.cloud.microsoft/mcp", + }), + expect.objectContaining({ + name: "my-sse", + status: "disabled", + url: "https://example.test/events", + }), + expect.objectContaining({ name: "my-local", status: "disabled" }), + ]) + ); + expect(result.servers.find((server) => server.name === "my-local")).not.toHaveProperty( + "url" + ); + expect(result).not.toHaveProperty("accountServerConfigs"); + }); + + it.each(["silent", "browser"] as const)( + "authenticates through %s, persists renewal, and disconnects on parent logout", + async (authentication) => { + const profile = join(workDir, `workiq-${authentication}-profile`); + await mkdir(profile, { recursive: true }); + const objectId = "workiq-object"; + const tenantId = randomUUID(); + const parent = `entra:${objectId}@${tenantId}`; + await writeFile( + join(profile, "config.json"), + JSON.stringify({ + loggedInUsers: [ + { + host: "https://substrate.office.com", + login: "workiq-test-account", + kind: "loki", + derivedFrom: parent, + }, + ], + authTokens: { + [`entra:${parent}:entra`]: { + token: JSON.stringify({ web: { refresh_token: "parent-refresh" } }), + }, + }, + }) + ); + const resource = "fdcc1f02-fc51-4226-8753-f668596af7f7"; + const scope = `${resource}/access`; + const requests: Array<{ host: string; path: string }> = []; + const grants: URLSearchParams[] = []; + let browserScopes: string[] = []; + let initializeRequests = 0; + const fallback = createE2eRequestHandler({ capiProxyUrl: openAiEndpoint.url }); + const proxy = new ConnectProxy( + async (req, res, host) => { + if ( + host !== "workiq.svc.cloud.microsoft" && + host !== "login.microsoftonline.com" + ) { + return fallback(req, res, host); + } + const path = req.url ?? "/"; + requests.push({ host, path }); + const reply = (status: number, value: unknown) => { + res.writeHead(status, { "content-type": "application/json" }); + res.end(JSON.stringify(value)); + return true; + }; + if (path.includes(".well-known/oauth-protected-resource")) { + return reply(200, { + resource: "https://workiq.svc.cloud.microsoft/mcp", + authorization_servers: [ + "https://login.microsoftonline.com/organizations/v2.0", + ], + scopes_supported: [scope], + }); + } + if (host === "login.microsoftonline.com" && path.includes(".well-known")) { + return reply(200, { + issuer: "https://login.microsoftonline.com/organizations/v2.0", + authorization_endpoint: + "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize", + token_endpoint: + "https://login.microsoftonline.com/organizations/oauth2/v2.0/token", + response_types_supported: ["code"], + code_challenge_methods_supported: ["S256"], + }); + } + let body = ""; + for await (const chunk of req) body += chunk.toString(); + if (host === "login.microsoftonline.com" && path.endsWith("/token")) { + const grant = new URLSearchParams(body); + grants.push(grant); + if ( + authentication === "browser" && + grant.get("refresh_token") === "parent-refresh" + ) { + return reply(400, { + error: "invalid_grant", + error_description: "Interaction required", + }); + } + // Entra emits oid only when the browser request includes profile. + const claims = { + sub: "workiq-client-subject", + tid: tenantId, + ...(browserScopes.includes("profile") ? { oid: objectId } : {}), + }; + return reply(200, { + access_token: "workiq-service-token", + token_type: "Bearer", + expires_in: 3600, + scope, + ...(grant.get("grant_type") === "authorization_code" + ? { + id_token: `e30.${Buffer.from(JSON.stringify(claims)).toString("base64url")}.fixture`, + ...(browserScopes.includes("offline_access") + ? { refresh_token: "workiq-browser-refresh" } + : {}), + } + : {}), + }); + } + if (host === "workiq.svc.cloud.microsoft") { + if (req.headers.authorization !== "Bearer workiq-service-token") { + res.setHeader( + "www-authenticate", + `Bearer resource_metadata="https://workiq.svc.cloud.microsoft/.well-known/oauth-protected-resource", scope="${scope}"` + ); + return reply(401, { error: "invalid_token" }); + } + if (req.method !== "POST") return reply(405, {}); + const message = JSON.parse(body); + if (message.method === "initialize") initializeRequests++; + if (message.id === undefined) { + res.writeHead(202); + res.end(); + return true; + } + const result = + message.method === "initialize" + ? { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "workiq-fixture", version: "1" }, + } + : message.method === "tools/list" + ? { + tools: [ + { + name: "whoami", + description: "Return the linked fixture identity", + inputSchema: { type: "object", properties: {} }, + }, + ], + } + : message.method === "tools/call" + ? { content: [{ type: "text", text: parent }] } + : {}; + return reply(200, { jsonrpc: "2.0", id: message.id, result }); + } + return reply(404, {}); + }, + { + interceptDomains: [ + "workiq.svc.cloud.microsoft", + "login.microsoftonline.com", + "api.githubcopilot.com", + "api.github.com", + "github.com", + "api.mcp.github.com", + ], + } + ); + let client: CopilotClient | undefined; + const starting = proxy.start(); + onTestFinished(async () => { + try { + await client?.stop(); + } finally { + await starting; + await proxy.stop(); + } + }); + await starting; + const clientOptions = { + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + workingDirectory: workDir, + env: { + ...env, + COPILOT_HOME: profile, + COPILOT_DISABLE_KEYTAR: "1", + HTTPS_PROXY: proxy.proxyUrl, + HTTP_PROXY: proxy.proxyUrl, + https_proxy: proxy.proxyUrl, + http_proxy: proxy.proxyUrl, + NODE_EXTRA_CA_CERTS: proxy.caFilePath, + SSL_CERT_FILE: proxy.caFilePath, + }, + }; + client = new CopilotClient(clientOptions); + let session = await client.createSession({ + configDirectory: profile, + mcpOAuthTokenStorage: "persistent", + onPermissionRequest: approveAll, + }); + expect( + (await session.rpc.mcp.list()).servers.find((server) => server.name === "WorkIQ") + ?.status + ).toBe("disabled"); + expect(requests).toHaveLength(0); + await session.rpc.mcp.enable({ serverName: "WorkIQ" }); + if (authentication === "browser") { + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === "WorkIQ" && server.status === "needs-auth" + ), + { timeoutMessage: "WorkIQ did not request browser authentication" } + ); + const redirectUri = "https://example.test/workiq-callback"; + const login = await session.rpc.mcp.oauth.login({ + serverName: "WorkIQ", + forceReauth: true, + redirectUri, + }); + expect(login.authorizationUrl).toBeDefined(); + expect(login.authorizationId).toBeDefined(); + const authorizationUrl = new URL(login.authorizationUrl!); + browserScopes = authorizationUrl.searchParams.get("scope")?.split(" ") ?? []; + 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(), + }); + expect(browserScopes).toEqual( + expect.arrayContaining([scope, "openid", "profile", "offline_access"]) + ); + } + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === "WorkIQ" && server.status === "connected" + ), + { timeoutMessage: "WorkIQ did not connect after enablement" } + ); + const initialGrantCount = authentication === "browser" ? 2 : 1; + expect(grants).toHaveLength(initialGrantCount); + expect(grants[0].get("client_id")).toBe("ba081686-5d24-4bc6-a0d6-d034ecffed87"); + expect(grants[0].get("refresh_token")).toBe("parent-refresh"); + expect(grants[0].get("scope")).toContain(scope); + expect( + (await session.rpc.mcp.listTools({ serverName: "WorkIQ" })).tools.map( + (tool) => tool.name + ) + ).toContain("whoami"); + + const connectedInitializeRequests = initializeRequests; + expect(connectedInitializeRequests).toBeGreaterThan(0); + for (let attempt = 0; attempt < 2; attempt++) { + await session.rpc.mcp.enable({ serverName: "WorkIQ" }); + expect(initializeRequests).toBe(connectedInitializeRequests); + expect( + (await session.rpc.mcp.list()).servers.find( + (server) => server.name === "WorkIQ" + )?.status + ).toBe("connected"); + } + const preferences = JSON.parse(await readFile(join(profile, "config.json"), "utf8")); + expect(preferences.accountMcpEnablement[JSON.stringify([parent, "WorkIQ"])]).toBe(true); + expect(grants).toHaveLength(initialGrantCount); + + await session.rpc.mcp.oauth.authenticationStateChanged({ + serverName: "WorkIQ", + refreshSessionToken: true, + }); + expect(grants).toHaveLength(initialGrantCount + 1); + expect(grants.at(-1)?.get("refresh_token")).toBe( + authentication === "browser" ? "workiq-browser-refresh" : "parent-refresh" + ); + const tokenFiles = (await readdir(join(profile, "mcp-oauth-config"))).filter((name) => + name.endsWith(".tokens.json") + ); + const saved = await Promise.all( + tokenFiles.map(async (name) => + JSON.parse(await readFile(join(profile, "mcp-oauth-config", name), "utf8")) + ) + ); + expect(saved.length).toBeGreaterThan(0); + expect(saved.every((record) => record.accountId === parent)).toBe(true); + expect(saved.every((record) => record.parentAccountId === undefined)).toBe(true); + expect(saved.every((record) => record.parentAccount === undefined)).toBe(true); + expect(saved.every((record) => record.brokerAccountId === undefined)).toBe(true); + await client.stop(); + client = new CopilotClient(clientOptions); + session = await client.createSession({ + configDirectory: profile, + mcpOAuthTokenStorage: "persistent", + onPermissionRequest: approveAll, + }); + await session.rpc.mcp.reload(); + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === "WorkIQ" && server.status === "connected" + ), + { timeoutMessage: "Persisted WorkIQ authentication did not reconnect" } + ); + expect(grants).toHaveLength(initialGrantCount + 1); + await session.rpc.accounts.set({ command: { kind: "logout", selectionId: parent } }); + expect( + (await session.rpc.mcp.list()).servers.some((server) => server.name === "WorkIQ") + ).toBe(false); + await expect(session.rpc.mcp.listTools({ serverName: "WorkIQ" })).rejects.toThrow(); + } + ); +}); diff --git a/nodejs/test/extension.test.ts b/nodejs/test/extension.test.ts index 91d9de1567..9bb6b11ae9 100644 --- a/nodejs/test/extension.test.ts +++ b/nodejs/test/extension.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { CopilotClient } from "../src/client.js"; import { approveAll } from "../src/index.js"; -import { createCanvas, defineFactory, defineWorkflow, joinSession } from "../src/extension.js"; +import { createCanvas, defineWorkflow, joinSession } from "../src/extension.js"; import { defaultJoinSessionPermissionHandler } from "../src/types.js"; describe("joinSession", () => { @@ -80,6 +80,20 @@ describe("joinSession", () => { expect(resumeForExtension.mock.calls[1]![3]).toBeUndefined(); }); + it("strips a skill provider, which only the session's owning client may supply", async () => { + process.env.SESSION_ID = "session-123"; + const resumeForExtension = vi + .spyOn(CopilotClient.prototype, "resumeSessionForExtension") + .mockResolvedValue({} as any); + const skillProvider = { listSkills: () => [], readSkill: () => null }; + + // Untyped callers can still pass the field that JoinSessionConfig omits. + await joinSession({ tools: [], skillProvider } as Parameters[0]); + + const [, config] = resumeForExtension.mock.calls[0]!; + expect(config).not.toHaveProperty("skillProvider"); + }); + it("forwards workflow contributions independently", async () => { process.env.SESSION_ID = "session-123"; const resumeForExtension = vi @@ -93,31 +107,10 @@ describe("joinSession", () => { await joinSession({ workflows: [workflow] }); expect(resumeForExtension.mock.calls[0]![2]).toEqual({ - factories: undefined, workflows: [workflow], }); }); - it("rejects mixed factory and workflow contributions", async () => { - process.env.SESSION_ID = "session-123"; - const factory = defineFactory({ - meta: { name: "legacy", description: "Legacy", phases: [] }, - run: async () => null, - }); - const workflow = defineWorkflow({ - meta: { name: "review", description: "Review", phases: [] }, - run: async () => null, - }); - - await expect( - joinSession({ - // @ts-expect-error Contribution generations are mutually exclusive. - factories: [factory], - workflows: [workflow], - }) - ).rejects.toThrow("cannot register both factories and workflows"); - }); - it("exports the canvas helper from the extension surface", () => { const canvas = createCanvas({ id: "counter", 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/factory.test.ts b/nodejs/test/factory.test.ts deleted file mode 100644 index 679cbf9131..0000000000 --- a/nodejs/test/factory.test.ts +++ /dev/null @@ -1,2772 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -import { readFileSync } from "node:fs"; -import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest"; -import { ResponseError } from "vscode-jsonrpc/node.js"; -import { CopilotClient } from "../src/client.js"; -import { joinSession } from "../src/extension.js"; -import { CopilotSession } from "../src/session.js"; -import { - defineFactory, - FactoryResumeError, - isFactoryRunTerminal, - type FactoryAgentOptions, - type FactoryContext, - type FactoryDefinition, - type FactoryJsonSchema, - type JsonValue, -} from "../src/factory.js"; - -/** Builds a `factory.run_updated` invalidation event for a run. */ -function runUpdatedEvent(runId: string, revision: number): Record { - return { - type: "factory.run_updated", - id: `event-${runId}-${revision}`, - parentId: null, - timestamp: new Date().toISOString(), - ephemeral: true, - data: { runId, revision }, - }; -} - -async function stopClient(client: CopilotClient): Promise { - await client.stop(); -} - -describe("factories", () => { - const originalSessionId = process.env.SESSION_ID; - - afterEach(() => { - if (originalSessionId === undefined) { - delete process.env.SESSION_ID; - } else { - process.env.SESSION_ID = originalSessionId; - } - vi.restoreAllMocks(); - }); - - it("defines a stable handle and accepts omitted limits", async () => { - const meta = { - name: "no-limits", - description: "A factory without resource limits", - phases: [], - }; - const run = vi.fn(async ({ args }: { args: unknown }) => args); - const handle = defineFactory({ meta, run }); - - expect(handle.meta).toEqual(meta); - expect(handle.meta).not.toBe(meta); - expect(Object.isFrozen(handle)).toBe(true); - expect(Object.isFrozen(handle.meta)).toBe(true); - - // The handle holds a snapshot, so mutating the caller's object after - // registration cannot desynchronize the advertised metadata. - meta.name = "mutated"; - (meta.phases as string[]).push("late"); - expect(handle.meta.name).toBe("no-limits"); - expect(handle.meta.phases).toEqual([]); - meta.name = "no-limits"; - meta.phases.length = 0; - - // The stored metadata is deep-frozen, so the handle's view of it must be - // readonly all the way down. Assert both halves: the mutation is a type - // error, and it also throws at runtime. - expect(() => { - // @ts-expect-error handle.meta is deeply readonly. - handle.meta.name = "mutated"; - }).toThrow(TypeError); - expect(() => { - // @ts-expect-error handle.meta.phases is a readonly array. - handle.meta.phases.push({ title: "late" }); - }).toThrow(TypeError); - - const session = new CopilotSession("session-1", {} as never); - session.registerFactories([handle]); - const result = await session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: meta.name, - runId: "run-1", - executionToken: "execution-token", - args: { value: 42 }, - }); - - expect(run).toHaveBeenCalledOnce(); - expect(result).toEqual({ result: { value: 42 } }); - }); - - it.each([ - [[{ title: "" }], "must not be empty"], - [[{ title: "Inspect" }, { title: "Inspect" }], "declared more than once"], - ])("rejects invalid declared phase titles", (phases, message) => { - expect(() => - defineFactory({ - meta: { - name: "invalid-phases", - description: "Invalid phase metadata", - phases, - }, - run: async () => {}, - }) - ).toThrow(message); - }); - - it("returns an absent execute result for a void factory", async () => { - const factory = defineFactory({ - meta: { - name: "void-result", - description: "Returns no result", - phases: [], - }, - run: async () => {}, - }); - const session = new CopilotSession("session-void-result", {} as never); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "void-result", - runId: "run-void-result", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({}); - }); - - it.each([42, "factory-result", [1, "two", false]])( - "returns non-object JSON factory result %j", - async (factoryResult) => { - const factory = defineFactory({ - meta: { - name: "json-result", - description: "Returns any JSON value", - phases: [], - }, - run: async () => factoryResult, - }); - const session = new CopilotSession("session-json-result", {} as never); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "json-result", - runId: "run-json-result", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: factoryResult }); - } - ); - - it.each([ - ["function", { nested: () => undefined }, "$.nested"], - ["symbol", [Symbol("invalid")], "$[0]"], - ["BigInt", { nested: 1n }, "$.nested"], - ])("rejects a %s anywhere in a factory result", async (_label, factoryResult, expectedPath) => { - const factory = defineFactory({ - meta: { - name: "unsupported-result", - description: "Returns an unsupported value", - phases: [], - }, - run: async () => factoryResult as never, - }); - const session = new CopilotSession("session-unsupported-result", {} as never); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "unsupported-result", - runId: "run-unsupported-result", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toMatchObject({ - message: `Factory result contains a function, symbol, or BigInt at ${expectedPath}`, - data: { - code: "factory_result_not_json", - category: "unsupported_type", - }, - }); - }); - - it.each([ - ["NaN", Number.NaN], - ["Infinity", Number.POSITIVE_INFINITY], - ])("rejects the non-finite number %s in a factory result", async (_label, value) => { - const factory = defineFactory({ - meta: { - name: "non-finite-result", - description: "Returns a non-finite number", - phases: [], - }, - run: async () => ({ value }) as never, - }); - const session = new CopilotSession("session-non-finite-result", {} as never); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "non-finite-result", - runId: "run-non-finite-result", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toMatchObject({ - message: "Factory result contains a non-finite number at $.value", - data: { - code: "factory_result_not_json", - category: "non_finite_number", - }, - }); - }); - - it("rejects a cyclic factory result", async () => { - const factoryResult: Record = {}; - factoryResult.self = factoryResult; - const factory = defineFactory({ - meta: { - name: "cyclic-result", - description: "Returns a cycle", - phases: [], - }, - run: async () => factoryResult as never, - }); - const session = new CopilotSession("session-cyclic-result", {} as never); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "cyclic-result", - runId: "run-cyclic-result", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toMatchObject({ - message: "Factory result contains a cyclic reference at $.self", - data: { - code: "factory_result_not_json", - category: "cyclic_value", - }, - }); - }); - - it.each([ - ["object", { nested: undefined }, "$.nested"], - ["array", [undefined], "$[0]"], - ])( - "rejects nested undefined in a factory result %s", - async (_label, factoryResult, expectedPath) => { - const factory = defineFactory({ - meta: { - name: "nested-undefined-result", - description: "Returns nested undefined", - phases: [], - }, - run: async () => factoryResult as never, - }); - const session = new CopilotSession("session-nested-undefined-result", {} as never); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "nested-undefined-result", - runId: "run-nested-undefined-result", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toMatchObject({ - message: `Factory result contains nested undefined at ${expectedPath}`, - data: { - code: "factory_result_not_json", - category: "nested_undefined", - }, - }); - } - ); - - it("rejects duplicate factory names within a single registration", () => { - const run = async () => null; - const first = defineFactory({ - meta: { name: "dup", description: "first", phases: [] }, - run, - }); - const second = defineFactory({ - meta: { name: "dup", description: "second", phases: [] }, - run, - }); - - const session = new CopilotSession("session-dup", {} as never); - expect(() => session.registerFactories([first, second])).toThrow( - /Duplicate factory name "dup"/ - ); - }); - - it.each([ - ["maxConcurrentSubagents", 0], - ["maxConcurrentSubagents", 1.5], - ["maxTotalSubagents", -1], - ["maxTotalSubagents", Number.POSITIVE_INFINITY], - ["timeoutSeconds", 0], - ["timeoutSeconds", Number.NaN], - ["timeoutSeconds", Number.POSITIVE_INFINITY], - ["maxAiCredits", 0], - ["maxAiCredits", Number.NaN], - ["maxAiCredits", Number.POSITIVE_INFINITY], - ["maxAiCredits", 0.000_000_000_4], - ["maxAiCredits", (Number.MAX_SAFE_INTEGER + 2) / 1_000_000_000], - ] as const)("rejects invalid %s limit %s", (field, value) => { - const definition = { - meta: { - name: `invalid-${field}-${String(value)}`, - description: "Invalid factory", - phases: [], - limits: { [field]: value }, - }, - run: async () => null, - } as FactoryDefinition; - - expect(() => defineFactory(definition)).toThrow(/must be a positive/); - }); - - it("accepts positive fractional timeoutSeconds through the Node timer ceiling", () => { - for (const timeoutSeconds of [0.001, 1.5, 2_147_483.647]) { - expect(() => - defineFactory({ - meta: { - name: `accepted-timeout-${timeoutSeconds}`, - description: "Factory with an accepted active-execution timeout", - phases: [], - limits: { timeoutSeconds }, - }, - run: async () => null, - }) - ).not.toThrow(); - } - }); - - it("accepts AI-credit ceilings that round to a positive safe nano-AIU integer", () => { - for (const maxAiCredits of [ - 0.000_000_000_5, - 1.25, - Number.MAX_SAFE_INTEGER / 1_000_000_000, - ]) { - expect(() => - defineFactory({ - meta: { - name: `accepted-credits-${maxAiCredits}`, - description: "Factory with an accepted AI-credit ceiling", - phases: [], - limits: { maxAiCredits }, - }, - run: async () => null, - }) - ).not.toThrow(); - } - }); - - it("rejects timeoutSeconds above the Node setTimeout ceiling", () => { - const definition = { - meta: { - name: "oversized-timeout", - description: "Factory with an out-of-range timeout", - phases: [], - limits: { timeoutSeconds: 2_147_483.648 }, - }, - run: async () => null, - } as FactoryDefinition; - - expect(() => defineFactory(definition)).toThrow( - 'Factory limit "timeoutSeconds" must not exceed 2147483.647 seconds' - ); - }); - - it("documents timeoutSeconds as accumulated active-execution time in public and generated types", () => { - const publicTypes = readFileSync(new URL("../src/types.ts", import.meta.url), "utf8"); - const generatedRpc = readFileSync( - new URL("../src/generated/rpc.ts", import.meta.url), - "utf8" - ); - - expect(publicTypes).toContain("Maximum accumulated active-execution time, in seconds."); - expect(publicTypes).toContain("subprocess waits, queued-agent waits, and sleeps"); - expect(publicTypes).toContain("timeoutSeconds?: number;"); - expect(generatedRpc).toContain("Maximum accumulated active-execution time in seconds."); - expect(generatedRpc).toContain("subprocess waits, queued-agent waits, and sleeps"); - expect(generatedRpc).toContain("timeoutSeconds?: number;"); - }); - - // A guessed ceiling does not make a run safer: it stops a healthy run partway - // with `factory_limit_reached`, after that run has already taken the user's - // approval and spent credits. Both documents are handed to the model verbatim - // by the `factories_manage` guide, so neither may read as an invitation to - // invent one. - it("documents limits as opt-in rather than inviting an invented ceiling", () => { - const guide = readFileSync(new URL("../docs/factories.md", import.meta.url), "utf8"); - const patterns = readFileSync( - new URL("../docs/factory-patterns.md", import.meta.url), - "utf8" - ); - - expect(guide).toContain( - "Set a ceiling only from real knowledge of what the factory costs, or because the user named one" - ); - expect(guide).toContain("no basis for estimating a number"); - - // The opening `defineFactory` sample is the shape an author copies. Filling - // all four ceilings in there taught the numbers as much as the syntax. - const openingSample = guide.slice(0, guide.indexOf("## Declaring an argument shape")); - expect(openingSample).not.toContain("limits: {"); - - // The Scaling section used to answer "there is no built-in concurrency cap" - // with "so declare one before fanning out widely". - expect(patterns).not.toContain("declare one before fanning out widely"); - expect(patterns).toContain("bound a wide fan-out with the factory's own counters"); - }); - - it("documents factory invocation and list paging behavior accurately", () => { - const guide = readFileSync(new URL("../docs/factories.md", import.meta.url), "utf8"); - const publicApi = readFileSync(new URL("../src/factory.ts", import.meta.url), "utf8"); - const listRunsPagingWording = "newest default page of this session's durable factory runs"; - const listRunsMetadata = ["oldestSeq", "newestSeq", "hasMoreNewer", "omittedOlder"]; - const resumeCodes = [ - "not_found", - "non_resumable", - "already_active", - "factory_already_running", - "factory_limits_invalid", - "factory_session_disposed", - "factory_storage_unavailable", - "factory_storage_corrupt", - ]; - const normalizeJSDoc = (document: string) => - document.replace(/\r?\n\s*\* ?/g, " ").replace(/\s+/g, " "); - const normalizedGuide = normalizeJSDoc(guide); - const normalizedPublicApi = normalizeJSDoc(publicApi); - - for (const document of [guide, publicApi]) { - expect(document).not.toContain("reapproval_declined"); - expect(document).not.toContain("no_approval_provider"); - expect(document).not.toMatch(/declined fresh run[\s\S]*terminal `cancelled` envelope/i); - } - - for (const document of [normalizedGuide, normalizedPublicApi]) { - expect(document).toContain(listRunsPagingWording); - for (const field of listRunsMetadata) { - expect(document).toContain(field); - } - } - - expect(normalizedGuide).toContain( - "SDK-initiated `run` and `resume` do not request permission" - ); - expect(normalizedGuide).toContain( - "`run_factory` tool requests permission before the durable row exists" - ); - expect(normalizedGuide).toContain("declining it creates no run row"); - expect(normalizedGuide).toContain("its maximum number of active top-level runs"); - for (const code of resumeCodes) { - expect(guide).toContain(`\`${code}\``); - } - expect(guide).toContain( - "Options are exactly `label`, `schema`, `model`, `agent`, `reasoningEffort`, and `contextTier`" - ); - expect(normalizedGuide).toContain( - "session returned by `joinSession`. It refuses calls that start, resume, or pause a factory run" - ); - expect(normalizedGuide).toContain( - "A `paused` envelope means that the current attempt settled, not that the durable run is permanently finished" - ); - expect(normalizedGuide).toContain('await ctx.pause("review-ready")'); - expect(normalizedGuide).toContain("session.factory.pause(runId)"); - - expect(normalizedPublicApi).toContain("SDK-initiated runs do not request permission"); - expect(normalizedPublicApi).toContain("declining it creates no run row"); - expect(normalizedPublicApi).toContain( - "while the session is at its active top-level run limit" - ); - expect(normalizedPublicApi).toContain("SDK-initiated resumes do not request permission"); - expect(normalizedPublicApi).toContain("with a documented resume code rejects with"); - expect(normalizedPublicApi).toContain( - "session instance returned by `joinSession`. It refuses calls that start, resume, or pause a factory run" - ); - expect(normalizedPublicApi).toContain( - "`paused` settles the current attempt, but the same durable run can later resume" - ); - }); - - it("carries a declared argsSchema through defineFactory into the registration payload", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => stopClient(client)); - - const argsSchema = { - type: "object", - required: ["repoPath"], - properties: { - repoPath: { type: "string" }, - depth: { type: ["integer", "null"] }, - mode: { enum: ["fast", "thorough"] }, - }, - } satisfies FactoryJsonSchema; - const meta = { - name: "declares-args", - description: "Declares the argument shape it expects", - phases: [], - argsSchema, - }; - const factory = defineFactory({ meta, run: async () => ({ ok: true }) }); - - // The declaration is snapshotted and deep-frozen like the rest of the - // metadata, so it cannot be mutated after registration. - expect(factory.meta.argsSchema).toEqual(argsSchema); - expect(factory.meta.argsSchema).not.toBe(argsSchema); - expect(Object.isFrozen(factory.meta.argsSchema)).toBe(true); - expect(() => { - // @ts-expect-error handle.meta.argsSchema is deeply readonly. - factory.meta.argsSchema!.type = "array"; - }).toThrow(TypeError); - - const omitted = defineFactory({ - meta: { name: "omits-args", description: "Declares nothing", phases: [] }, - run: async () => ({ ok: true }), - }); - expect(omitted.meta.argsSchema).toBeUndefined(); - expect("argsSchema" in omitted.meta).toBe(false); - - const sendRequest = vi - .spyOn( - (client as never as { connection: { sendRequest: Function } }).connection, - "sendRequest" - ) - .mockImplementation(async (method: string, params: Record) => { - if (method === "session.resume") { - return { sessionId: params.sessionId }; - } - throw new Error(`Unexpected method: ${method}`); - }); - - await client.resumeSessionForExtension( - "session-args-schema", - { onPermissionRequest: () => ({ kind: "approved" }) }, - [factory, omitted] - ); - - const payload = sendRequest.mock.calls.find( - ([method]) => method === "session.resume" - )![1] as { factories: Array> }; - // The schema has to survive JSON serialization to reach the runtime, which - // validates `args` against it before a run row exists. - expect(JSON.parse(JSON.stringify(payload.factories))[0].argsSchema).toEqual(argsSchema); - expect(payload.factories[1]).not.toHaveProperty("argsSchema"); - }); - - it("documents argsSchema consistently with the runtime's enforced subset", () => { - const publicTypes = readFileSync(new URL("../src/types.ts", import.meta.url), "utf8"); - const publicApi = readFileSync(new URL("../src/factory.ts", import.meta.url), "utf8"); - const guide = readFileSync(new URL("../docs/factories.md", import.meta.url), "utf8"); - const normalizeJSDoc = (document: string) => - document.replace(/\r?\n\s*\* ?/g, " ").replace(/\s+/g, " "); - - expect(publicTypes).toContain("argsSchema?: FactoryJsonSchema;"); - - // The `run_factory` tool tells the model exactly this. The two surfaces - // have to agree about what a declaration does and does not enforce. - for (const document of [normalizeJSDoc(publicTypes), guide]) { - expect(document).toContain("types, required properties, and enum"); - expect(document).toMatch( - /`minLength`, `pattern`,? (?:and|or) `additionalProperties` are recorded/ - ); - } - expect(normalizeJSDoc(publicTypes)).toContain("before** the run starts"); - // Enforcement is tool-path only: `toolRunFactoryValidateArgs` is called from - // the runtime's runFactoryTool, and never from `session.factory.run`. Both - // surfaces must keep saying so, or authors will assume their own SDK-initiated - // runs are checked. - expect(normalizeJSDoc(publicTypes)).toContain( - "`session.factory.run(...)` is not validated against the declaration" - ); - expect(guide).toContain("Validation covers the model's `run_factory` path only"); - expect(normalizeJSDoc(publicApi)).toContain( - "`null`, `boolean`, `integer`, `number`, `string`, `array`, or `object`" - ); - expect(guide).toContain("no run row, permission prompt, or credit spend happens"); - }); - - it("serializes only factory metadata in the extension resume payload", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => stopClient(client)); - - const run = vi.fn(async () => ({ ok: true })); - const factory = defineFactory({ - meta: { - name: "registered", - description: "Registration test", - phases: [{ title: "Run" }], - limits: { maxTotalSubagents: 2 }, - }, - run, - }); - const sendRequest = vi - .spyOn( - (client as never as { connection: { sendRequest: Function } }).connection, - "sendRequest" - ) - .mockImplementation(async (method: string, params: Record) => { - if (method === "session.resume") { - const sessions = (client as never as { sessions: Map }) - .sessions; - expect( - sessions.get(params.sessionId as string)?.clientSessionApis.factory - ).toBeDefined(); - return { sessionId: params.sessionId }; - } - throw new Error(`Unexpected method: ${method}`); - }); - - await client.resumeSessionForExtension( - "session-registration", - { onPermissionRequest: () => ({ kind: "approved" }) }, - [factory] - ); - - const payload = sendRequest.mock.calls.find( - ([method]) => method === "session.resume" - )![1] as { - factories: unknown[]; - }; - expect(payload.factories).toEqual([factory.meta]); - expect(payload.factories[0]).not.toHaveProperty("run"); - expect(JSON.stringify(payload.factories)).not.toContain("async"); - }); - - it("passes factories only through the extension join path", async () => { - process.env.SESSION_ID = "session-extension"; - const factory = defineFactory({ - meta: { - name: "extension-only", - description: "Extension-only registration", - phases: [], - }, - run: async () => ({ ok: true }), - }); - const resumeSessionForExtension = vi - .spyOn(CopilotClient.prototype, "resumeSessionForExtension") - .mockResolvedValue({} as CopilotSession); - - await joinSession({ factories: [factory] }); - - expect(resumeSessionForExtension).toHaveBeenCalledWith( - "session-extension", - expect.objectContaining({ suppressResumeEvent: true }), - { factories: [factory], workflows: undefined }, - undefined - ); - }); - - it("builds the factory context with the unrestricted joined session identity", async () => { - process.env.SESSION_ID = "session-context"; - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.log") { - return {}; - } - if (method === "session.tasks.list") { - return { tasks: [] }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const joinedSession = new CopilotSession("session-context", { sendRequest } as never); - const contextSeen = Promise.withResolvers<{ - runId: string; - args: unknown; - session: CopilotSession; - signal: AbortSignal; - }>(); - const factory = defineFactory({ - meta: { - name: "context", - description: "Context test", - phases: [], - }, - run: async (context) => { - contextSeen.resolve(context); - context.phase("A"); - context.log("hi"); - const tasks = await context.session.rpc.tasks.list(); - return { ok: true, taskCount: tasks.tasks.length }; - }, - }); - vi.spyOn(CopilotClient.prototype, "resumeSessionForExtension").mockImplementation( - async (_sessionId, _config, contributions) => { - if (Array.isArray(contributions)) { - throw new Error("joinSession should use the contribution object"); - } - joinedSession.registerFactories(contributions.factories); - return joinedSession; - } - ); - - const joinSessionResult = await joinSession({ factories: [factory] }); - const executeResult = await joinSessionResult.clientSessionApis.factory!.execute({ - sessionId: joinSessionResult.sessionId, - name: "context", - runId: "run-context", - executionToken: "execution-token", - args: { value: 42 }, - }); - const context = await contextSeen.promise; - - expect(context.runId).toBe("run-context"); - expect(context.args).toEqual({ value: 42 }); - expect(context.session).toBe(joinSessionResult); - expect(context.session.rpc).toBe(joinSessionResult.rpc); - expect(context.signal).toBeInstanceOf(AbortSignal); - expect(executeResult).toEqual({ result: { ok: true, taskCount: 0 } }); - expect(sendRequest).toHaveBeenCalledWith("session.tasks.list", { - sessionId: joinSessionResult.sessionId, - }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.log", { - sessionId: joinSessionResult.sessionId, - runId: "run-context", - executionToken: "execution-token", - lines: [ - { seq: 0, kind: "phase", text: "A" }, - { seq: 1, kind: "log", text: "hi" }, - ], - }); - }); - - it("rejects nested factories without forwarding a runNested request", async () => { - const sendRequest = vi.fn(async () => { - throw new Error("Unexpected forward request"); - }); - const session = new CopilotSession("session-no-nesting", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "no-nesting", - description: "Nested factory rejection test", - phases: [], - }, - run: async (context) => context.factory("nested", { value: 42 }), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "no-nesting", - runId: "run-no-nesting", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toThrow("nested factories are not supported"); - expect(sendRequest).not.toHaveBeenCalled(); - }); - - it("keeps factory reads and cancellation available inside a factory body", async () => { - const sendRequest = vi.fn(async (method: string) => { - switch (method) { - case "session.factory.getRun": - return { runId: "other-run", status: "completed" }; - case "session.factory.listRuns": - return { runs: [] }; - case "session.factory.cancel": - return {}; - default: - throw new Error(`Unexpected method: ${method}`); - } - }); - const session = new CopilotSession("session-factory-reads", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "factory-reads", - description: "Read factory state from a factory body", - phases: [], - }, - run: async ({ session: contextSession }) => { - const [run, runs] = await Promise.all([ - contextSession.factory.getRun("other-run"), - contextSession.factory.listRuns(), - contextSession.factory.cancel("other-run"), - ]); - return { runId: run.runId, runCount: runs.length }; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "factory-reads", - runId: "run-factory-reads", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: { runId: "other-run", runCount: 0 } }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.getRun", { - sessionId: session.sessionId, - runId: "other-run", - }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.listRuns", { - sessionId: session.sessionId, - }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.cancel", { - sessionId: session.sessionId, - runId: "other-run", - }); - }); - - it("allows factory.run after a factory body returns", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.run") { - return { runId: "run-after-body", status: "completed", result: "started" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-after-body", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "returns", - description: "Return before a separate factory run", - phases: [], - }, - run: async () => "finished", - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "returns", - runId: "run-returns", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "finished" }); - await expect(session.factory.run("after-body")).resolves.toMatchObject({ - status: "completed", - result: "started", - }); - }); - - it("allows a factory-body timer to start a factory after the body settles", async () => { - const delayedRun = Promise.withResolvers(); - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.run") { - return { runId: "run-from-timer", status: "completed", result: "started" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-timer", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "timer", - description: "Start a factory from an unawaited timer", - phases: [], - }, - run: async () => { - setTimeout(() => { - void session.factory - .run("from-timer") - .then(delayedRun.resolve, delayedRun.reject); - }, 0); - return "finished"; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "timer", - runId: "run-timer", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "finished" }); - await expect(delayedRun.promise).resolves.toMatchObject({ - status: "completed", - result: "started", - }); - }); - - it("flushes progress incrementally while a factory body is awaiting", async () => { - const sendRequest = vi.fn(async () => ({})); - const session = new CopilotSession("session-live-progress", { sendRequest } as never); - const body = Promise.withResolvers(); - const factory = defineFactory({ - meta: { - name: "live-progress", - description: "Incremental progress test", - phases: [], - }, - run: async ({ log }) => { - log("before await"); - await body.promise; - return "done"; - }, - }); - session.registerFactories([factory]); - - const execution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "live-progress", - runId: "run-live-progress", - executionToken: "execution-token", - args: {}, - }); - await vi.waitFor(() => { - expect(sendRequest).toHaveBeenCalledWith("session.factory.log", { - sessionId: session.sessionId, - runId: "run-live-progress", - executionToken: "execution-token", - lines: [{ seq: 0, kind: "log", text: "before await" }], - }); - }); - - body.resolve(); - await expect(execution).resolves.toEqual({ result: "done" }); - }); - - it("calls factory.agent with the current run id and returns its text", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.agent") { - return { result: "pong" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-agent", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "agent", - description: "Agent context test", - phases: [], - }, - run: async ({ agent }) => - agent("Reply with pong", { - label: "Pong helper", - model: "gpt-test", - schema: { type: "string" }, - effort: "high", - } as FactoryAgentOptions), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "agent", - runId: "run-agent", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "pong" }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.agent", { - sessionId: session.sessionId, - factoryRunId: "run-agent", - executionToken: "execution-token", - prompt: "Reply with pong", - opts: { - label: "Pong helper", - model: "gpt-test", - schema: { type: "string" }, - }, - }); - }); - - it("forwards every declared factory.agent option", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.agent") { - return { result: "pong" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-agent-options", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "agent-options", - description: "Agent option forwarding test", - phases: [], - }, - run: async ({ agent }) => - agent("Reply with pong", { - label: "Pong helper", - model: "gpt-test", - schema: { type: "string" }, - agent: "reviewer", - reasoningEffort: "high", - contextTier: "long_context", - }), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "agent-options", - runId: "run-agent-options", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "pong" }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.agent", { - sessionId: session.sessionId, - factoryRunId: "run-agent-options", - executionToken: "execution-token", - prompt: "Reply with pong", - opts: { - label: "Pong helper", - model: "gpt-test", - schema: { type: "string" }, - agent: "reviewer", - reasoningEffort: "high", - contextTier: "long_context", - }, - }); - }); - - it("sends empty factory.agent options when none are supplied", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.agent") { - return { result: "pong" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-empty-agent-options", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "empty-agent-options", - description: "Empty agent option forwarding test", - phases: [], - }, - run: async ({ agent }) => agent("Reply with pong"), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "empty-agent-options", - runId: "run-empty-agent-options", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "pong" }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.agent", { - sessionId: session.sessionId, - factoryRunId: "run-empty-agent-options", - executionToken: "execution-token", - prompt: "Reply with pong", - opts: {}, - }); - }); - - it("keeps each execution token on callbacks from overlapping contexts with the same run id", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.agent") { - return { result: "agent result" }; - } - if (method === "session.factory.journal.get") { - return { hit: false }; - } - return {}; - }); - const session = new CopilotSession("session-overlapping-attempts", { - sendRequest, - } as never); - const contexts: FactoryContext[] = []; - const bodies = [Promise.withResolvers(), Promise.withResolvers()]; - const contextsReady = Promise.withResolvers(); - const factory = defineFactory({ - meta: { - name: "overlapping-attempts", - description: "Execution token capture test", - phases: [], - }, - run: async (context) => { - const invocation = contexts.length; - contexts.push(context); - if (contexts.length === 2) { - contextsReady.resolve(); - } - await bodies[invocation].promise; - return `attempt ${invocation + 1}`; - }, - }); - session.registerFactories([factory]); - const first = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "overlapping-attempts", - runId: "shared-run", - executionToken: "old-token", - args: {}, - }); - const second = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "overlapping-attempts", - runId: "shared-run", - executionToken: "current-token", - args: {}, - }); - await contextsReady.promise; - - contexts[0].log("stale log"); - await contexts[0].agent("stale agent"); - await contexts[0].step("stale journal", () => "stale result"); - await contexts[1].agent("current agent"); - - expect(sendRequest).toHaveBeenCalledWith( - "session.factory.log", - expect.objectContaining({ executionToken: "old-token" }) - ); - expect(sendRequest).toHaveBeenCalledWith( - "session.factory.agent", - expect.objectContaining({ executionToken: "old-token", prompt: "stale agent" }) - ); - expect(sendRequest).toHaveBeenCalledWith( - "session.factory.journal.get", - expect.objectContaining({ executionToken: "old-token", key: "stale journal" }) - ); - expect(sendRequest).toHaveBeenCalledWith( - "session.factory.journal.put", - expect.objectContaining({ executionToken: "old-token", key: "stale journal" }) - ); - expect(sendRequest).toHaveBeenCalledWith( - "session.factory.agent", - expect.objectContaining({ executionToken: "current-token", prompt: "current agent" }) - ); - - bodies[0].resolve(); - bodies[1].resolve(); - await expect(first).resolves.toEqual({ result: "attempt 1" }); - await expect(second).resolves.toEqual({ result: "attempt 2" }); - }); - - it("runs a durable step once, serves cached null, and does not cache failures", async () => { - const journal = new Map(); - const sendRequest = vi.fn( - async (method: string, params: { key?: string; resultJson?: unknown }) => { - if (method === "session.factory.journal.get") { - return journal.has(params.key!) - ? { hit: true, resultJson: journal.get(params.key!) } - : { hit: false }; - } - if (method === "session.factory.journal.put") { - journal.set(params.key!, params.resultJson); - return {}; - } - throw new Error(`Unexpected method: ${method}`); - } - ); - const session = new CopilotSession("session-step", { sendRequest } as never); - let cachedProducerCalls = 0; - let failingProducerCalls = 0; - const factory = defineFactory({ - meta: { - name: "step", - description: "Durable step context test", - phases: [], - }, - run: async ({ step }) => { - const first = await step("cached-null", async () => { - cachedProducerCalls++; - return null; - }); - const second = await step("cached-null", async () => { - cachedProducerCalls++; - return "wrong"; - }); - const failed = await step("retry", async () => { - failingProducerCalls++; - throw new Error("transient"); - }).catch(() => "failed"); - const retried = await step("retry", async () => { - failingProducerCalls++; - return "recovered"; - }); - return { first, second, failed, retried }; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "step", - runId: "run-step", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ - result: { first: null, second: null, failed: "failed", retried: "recovered" }, - }); - expect(cachedProducerCalls).toBe(1); - expect(failingProducerCalls).toBe(2); - expect( - sendRequest.mock.calls.filter(([method]) => method === "session.factory.journal.put") - ).toHaveLength(2); - }); - - it.each([ - ["undefined", () => undefined], - ["NaN", () => Number.NaN], - ["Infinity", () => Number.POSITIVE_INFINITY], - ["function", () => () => undefined], - ["symbol", () => Symbol("invalid")], - ["BigInt", () => 1n], - [ - "cycle", - () => { - const value: Record = {}; - value.self = value; - return value; - }, - ], - ["non-plain object", () => new Date()], - [ - "accessor property", - () => Object.defineProperty({}, "value", { enumerable: true, get: () => "hidden" }), - ], - [ - "non-enumerable property", - () => Object.defineProperty({}, "value", { enumerable: false, value: "hidden" }), - ], - ["array hole", () => new Array(1)], - [ - "array accessor", - () => Object.defineProperty([], "0", { enumerable: true, get: () => "hidden" }), - ], - ["array extra key", () => Object.assign([1], { extra: "dropped" })], - ])("rejects a journaled step %s result", async (_label, makeValue) => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.journal.get") { - return { hit: false }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-invalid-step", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "invalid-step", - description: "Rejects lossy step values", - phases: [], - }, - run: async ({ step }) => { - await step("invalid", async () => makeValue() as never); - return "must-not-complete"; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "invalid-step", - runId: "run-invalid-step", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toMatchObject({ - data: { - code: "factory_step_not_json", - }, - }); - expect( - sendRequest.mock.calls.filter(([method]) => method === "session.factory.journal.put") - ).toHaveLength(0); - }); - - it("validates a journaled step cache hit before replay", async () => { - const cached = Object.assign([1], { extra: "dropped" }); - const producer = vi.fn(async () => "must-not-run"); - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.journal.get") { - return { hit: true, resultJson: cached }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-invalid-step-cache", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "invalid-step-cache", - description: "Rejects invalid cached values", - phases: [], - }, - run: async ({ step }) => step("cached", producer), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "invalid-step-cache", - runId: "run-invalid-step-cache", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toMatchObject({ - data: { - code: "factory_step_not_json", - category: "unsupported_object", - }, - }); - expect(producer).not.toHaveBeenCalled(); - }); - - it("replays a journaled step value identically on resume", async () => { - const journal = new Map(); - const sendRequest = vi.fn( - async (method: string, params: { key?: string; resultJson?: unknown }) => { - if (method === "session.factory.journal.get") { - return journal.has(params.key!) - ? { hit: true, resultJson: journal.get(params.key!) } - : { hit: false }; - } - if (method === "session.factory.journal.put") { - journal.set(params.key!, params.resultJson); - return {}; - } - throw new Error(`Unexpected method: ${method}`); - } - ); - const producer = vi.fn(async () => ({ nested: [1, null, "same"] })); - const session = new CopilotSession("session-step-replay", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "step-replay", - description: "Replays strict JSON", - phases: [], - }, - run: async ({ step }) => step("same", producer), - }); - session.registerFactories([factory]); - - const first = await session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "step-replay", - runId: "run-step-replay", - executionToken: "execution-token", - args: {}, - }); - const replay = await session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "step-replay", - runId: "run-step-replay", - executionToken: "execution-token", - args: {}, - }); - - expect(replay).toEqual(first); - expect(producer).toHaveBeenCalledOnce(); - }); - - it("bypasses validation and journaling for a volatile step", async () => { - const sendRequest = vi.fn(); - const session = new CopilotSession("session-volatile-step", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "volatile-step", - description: "Allows author-opted-out volatile values", - phases: [], - }, - run: async ({ step }) => { - const value = await step("volatile", async () => (() => "not JSON") as never, { - volatile: true, - }); - expect(typeof value).toBe("function"); - return "completed"; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "volatile-step", - runId: "run-volatile-step", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "completed" }); - expect(sendRequest).not.toHaveBeenCalled(); - }); - - it("does not start a volatile step producer after the run is aborted", async () => { - const sendRequest = vi.fn(); - const session = new CopilotSession("session-volatile-abort", { sendRequest } as never); - let producerRan = false; - const factory = defineFactory({ - meta: { - name: "volatile-abort", - description: "Volatile steps honour cancellation", - phases: [], - }, - run: async ({ step, runId }) => { - // Abort mid-run, then attempt a volatile step. The producer must - // not run: cancellation has to stop new extension work starting, - // exactly as it does on the journaled path. - await session.clientSessionApis.factory!.abort({ - sessionId: session.sessionId, - runId, - executionToken: "execution-token", - }); - await step( - "volatile", - () => { - producerRan = true; - return "should not happen"; - }, - { volatile: true } - ); - return "completed"; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "volatile-abort", - runId: "run-volatile-abort", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toThrow(); - expect(producerRan).toBe(false); - }); - - it("rejects a factory result array with an extra own key", async () => { - const factory = defineFactory({ - meta: { - name: "array-extra-result", - description: "Rejects lossy array keys", - phases: [], - }, - run: async () => Object.assign([1], { extra: 1n }) as never, - }); - const session = new CopilotSession("session-array-extra-result", {} as never); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "array-extra-result", - runId: "run-array-extra-result", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toMatchObject({ - data: { - code: "factory_result_not_json", - category: "unsupported_object", - }, - }); - }); - - it("exposes factory getRun and forwards the run id", async () => { - const envelope = { runId: "run-read", status: "error", error: "failed" }; - const sendRequest = vi.fn(async () => envelope); - const session = new CopilotSession("session-read", { sendRequest } as never); - - await expect(session.factory.getRun("run-read")).resolves.toEqual(envelope); - expect(sendRequest).toHaveBeenCalledWith("session.factory.getRun", { - sessionId: session.sessionId, - runId: "run-read", - }); - }); - - it("exposes factory observability methods and forwards paging options", async () => { - const summary = { - runId: "run-observe", - factoryName: "observe", - description: "Observe", - status: "running" as const, - revision: 4, - createdAt: 1, - startedAt: 2, - updatedAt: 3, - completedAt: null, - currentPhase: { id: "p0", ordinal: 0 }, - declaredPhaseCount: 1, - liveAgentCount: 1, - totalSpawnedAgentCount: 1, - consumed: { activeMs: 10, subagents: 1, nanoAiu: 5 }, - declaredLimits: {}, - approved: {}, - observedAt: 4, - activeSegmentStartedAt: 2, - terminal: null, - }; - const progress = { - records: [], - oldestSeq: null, - newestSeq: null, - hasMoreOlder: false, - hasMoreNewer: false, - revision: 4, - }; - const detail = { ...summary, phases: [], agents: [], progress }; - const runsPage = { - runs: [summary], - oldestSeq: 11, - newestSeq: 12, - hasMoreNewer: true, - omittedOlder: 10, - }; - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.listRuns") return runsPage; - if (method === "session.factory.getRunDetail") return detail; - return progress; - }); - const session = new CopilotSession("session-observe", { sendRequest } as never); - - await expect(session.factory.listRuns()).resolves.toEqual([summary]); - const listedPage = await session.factory.listRuns({ - afterSeq: 10, - beforeSeq: 20, - limit: 50, - }); - expect(listedPage).toEqual(runsPage); - expect(listedPage.oldestSeq).toBe(11); - expect(listedPage.newestSeq).toBe(12); - expect(listedPage.hasMoreNewer).toBe(true); - expect(listedPage.omittedOlder).toBe(10); - await expect(session.factory.getRunDetail("run-observe")).resolves.toEqual(detail); - await expect( - session.factory.getRunProgress("run-observe", { - phaseId: "p0", - afterSeq: 10, - limit: 50, - }) - ).resolves.toEqual(progress); - expect(sendRequest).toHaveBeenNthCalledWith(1, "session.factory.listRuns", { - sessionId: session.sessionId, - }); - expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.listRuns", { - sessionId: session.sessionId, - afterSeq: 10, - beforeSeq: 20, - limit: 50, - }); - expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunDetail", { - sessionId: session.sessionId, - runId: "run-observe", - }); - expect(sendRequest).toHaveBeenNthCalledWith(4, "session.factory.getRunProgress", { - sessionId: session.sessionId, - runId: "run-observe", - phaseId: "p0", - afterSeq: 10, - limit: 50, - }); - }); - - it("exposes factory cancel and forwards the run id", async () => { - const envelope = { runId: "run-cancel", status: "cancelled", reason: "cancelled" }; - const sendRequest = vi.fn(async () => envelope); - const session = new CopilotSession("session-cancel", { sendRequest } as never); - - await expect(session.factory.cancel("run-cancel")).resolves.toEqual(envelope); - expect(sendRequest).toHaveBeenCalledWith("session.factory.cancel", { - sessionId: session.sessionId, - runId: "run-cancel", - }); - }); - - it("exposes guarded public pause and prevents factory bodies from bypassing ctx.pause", async () => { - const paused = { runId: "run-pause", status: "paused" as const }; - const sendRequest = vi.fn(async () => paused); - const session = new CopilotSession("session-pause", { sendRequest } as never); - - await expect(session.factory.pause("run-pause")).resolves.toEqual(paused); - expect(sendRequest).toHaveBeenCalledWith("session.factory.pause", { - sessionId: session.sessionId, - runId: "run-pause", - }); - - const factory = defineFactory({ - meta: { - name: "pause-bypass", - description: "Public pause cannot bypass context restrictions", - phases: [], - }, - run: ({ runId, session: factorySession }) => factorySession.factory.pause(runId), - }); - session.registerFactories([factory]); - sendRequest.mockClear(); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "pause-bypass", - runId: "run-pause-bypass", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toThrow( - "factory.run, factory.resume, and factory.pause are not allowed while a factory body is running on this call path." - ); - expect(sendRequest).not.toHaveBeenCalled(); - }); - - it("rejects an empty pause checkpoint key before RPC", async () => { - const sendRequest = vi.fn(); - const session = new CopilotSession("session-empty-pause-key", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "empty-pause-key", - description: "Empty pause key rejection", - phases: [], - }, - run: ({ pause }) => pause(""), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "empty-pause-key", - runId: "run-empty-pause-key", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toThrow("must not be empty"); - expect(sendRequest).not.toHaveBeenCalled(); - }); - - it("waits for cooperative abort when a pause checkpoint returns pause", async () => { - const checkpointRequested = Promise.withResolvers(); - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.pauseAtCheckpoint") { - checkpointRequested.resolve(); - return { action: "pause" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-checkpoint-pause", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "checkpoint-pause", - description: "Pause checkpoint abort behavior", - phases: [], - }, - run: ({ pause }) => pause("review-ready"), - }); - session.registerFactories([factory]); - - let settled = false; - const execution = session.clientSessionApis - .factory!.execute({ - sessionId: session.sessionId, - name: "checkpoint-pause", - runId: "run-checkpoint-pause", - executionToken: "execution-token", - args: {}, - }) - .finally(() => { - settled = true; - }); - await checkpointRequested.promise; - await Promise.resolve(); - expect(settled).toBe(false); - - await session.clientSessionApis.factory!.abort({ - sessionId: session.sessionId, - runId: "run-checkpoint-pause", - executionToken: "execution-token", - }); - await expect(execution).rejects.toMatchObject({ name: "AbortError" }); - }); - - it("returns void and continues when a pause checkpoint returns continue", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.pauseAtCheckpoint") { - return { action: "continue" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const session = new CopilotSession("session-checkpoint-continue", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "checkpoint-continue", - description: "Continue checkpoint behavior", - phases: [], - }, - run: async ({ pause }) => { - const result = await pause("review-ready"); - return result === undefined ? "continued" : "unexpected"; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "checkpoint-continue", - runId: "run-checkpoint-continue", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "continued" }); - expect(sendRequest).toHaveBeenCalledWith("session.factory.pauseAtCheckpoint", { - sessionId: session.sessionId, - runId: "run-checkpoint-continue", - executionToken: "execution-token", - key: "review-ready", - }); - }); - - it.each(["parallel", "pipeline"] as const)( - "rejects pause checkpoints inside %s helper branches before RPC", - async (helper) => { - const sendRequest = vi.fn(); - const session = new CopilotSession(`session-pause-${helper}`, { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: `pause-${helper}`, - description: "Pause helper-scope rejection", - phases: [], - }, - run: async ({ pause, parallel, pipeline }) => { - const attempt = async () => { - try { - await pause("review-ready"); - return "unexpected"; - } catch (error) { - return (error as Error).message; - } - }; - return helper === "parallel" - ? parallel([attempt]) - : pipeline(["item"], attempt); - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: `pause-${helper}`, - runId: `run-pause-${helper}`, - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ - result: [`Factory pause checkpoints are not allowed inside ${helper}() branches`], - }); - expect(sendRequest).not.toHaveBeenCalled(); - } - ); - - it("runs parallel as a barrier and maps a throwing thunk to null", async () => { - const first = Promise.withResolvers(); - const second = Promise.withResolvers(); - const started: string[] = []; - const session = new CopilotSession("session-parallel", {} as never); - const factory = defineFactory({ - meta: { - name: "parallel", - description: "Parallel combinator test", - phases: [], - }, - run: async ({ parallel }) => - parallel([ - async () => { - started.push("first"); - return first.promise; - }, - async () => { - started.push("second"); - return second.promise; - }, - async () => { - started.push("throwing"); - throw new Error("expected"); - }, - ]), - }); - session.registerFactories([factory]); - - let settled = false; - const execution = session.clientSessionApis - .factory!.execute({ - sessionId: session.sessionId, - name: "parallel", - runId: "run-parallel", - args: {}, - }) - .finally(() => { - settled = true; - }); - await vi.waitFor(() => expect(started).toEqual(["first", "second", "throwing"])); - - second.resolve("second"); - await Promise.resolve(); - expect(settled).toBe(false); - - first.resolve("first"); - await expect(execution).resolves.toEqual({ result: ["first", "second", null] }); - }); - - it("rejects already-invoked promises passed to parallel with a clear diagnostic", async () => { - const session = new CopilotSession("session-parallel-promises", {} as never); - const factory = defineFactory({ - meta: { - name: "parallel-promises", - description: "Parallel misuse diagnostic", - phases: [], - }, - run: async ({ parallel }) => - parallel([Promise.resolve("already running")] as unknown as Array< - () => Promise - >), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "parallel-promises", - runId: "run-parallel-promises", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toThrow( - "parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)" - ); - }); - - it("flows pipeline items independently and drops only the item whose stage throws", async () => { - const releaseFirstItem = Promise.withResolvers(); - const secondStageStarted = Promise.withResolvers(); - const finalStageItems: string[] = []; - const session = new CopilotSession("session-pipeline", {} as never); - const factory = defineFactory({ - meta: { - name: "pipeline", - description: "Pipeline combinator test", - phases: [], - }, - run: async ({ pipeline }) => - pipeline( - ["slow", "fast", "throw"], - async (_previous, item) => { - if (item === "slow") { - await releaseFirstItem.promise; - } - if (item === "throw") { - throw new Error("expected"); - } - return `${item}-stage-1`; - }, - async (previous, item) => { - if (item === "fast") { - secondStageStarted.resolve(); - } - finalStageItems.push(item as string); - return `${previous}-stage-2`; - } - ), - }); - session.registerFactories([factory]); - - const execution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "pipeline", - runId: "run-pipeline", - executionToken: "execution-token", - args: {}, - }); - await secondStageStarted.promise; - expect(finalStageItems).toEqual(["fast"]); - - releaseFirstItem.resolve(); - await expect(execution).resolves.toEqual({ - result: ["slow-stage-1-stage-2", "fast-stage-1-stage-2", null], - }); - expect(finalStageItems).toEqual(["fast", "slow"]); - }); - - it("enforces the 4096-item cap for parallel and pipeline", async () => { - const session = new CopilotSession("session-fanout-cap", {} as never); - const factory = defineFactory({ - meta: { - name: "fanout-cap", - description: "Fan-out cap test", - phases: [], - }, - run: async ({ parallel, pipeline }) => { - const tooManyItems = Array.from({ length: 4097 }, () => null); - const parallelError = await parallel( - tooManyItems.map(() => async () => null) - ).catch((error: unknown) => error); - const pipelineError = await pipeline(tooManyItems).catch((error: unknown) => error); - return { - parallel: (parallelError as Error).message, - pipeline: (pipelineError as Error).message, - }; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "fanout-cap", - runId: "run-fanout-cap", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ - result: { - parallel: "parallel() accepts at most 4096 items; got 4097.", - pipeline: "pipeline() accepts at most 4096 items; got 4097.", - }, - }); - }); - - it("does not deadlock nested combinators when only leaf agents use a one-slot limiter", async () => { - let active = 0; - let maxActive = 0; - let tail = Promise.resolve(); - const sendRequest = vi.fn( - async (method: string, params: { prompt: string }): Promise<{ result: string }> => { - if (method !== "session.factory.agent") { - throw new Error(`Unexpected method: ${method}`); - } - const previous = tail; - const done = Promise.withResolvers(); - tail = done.promise; - await previous; - active++; - maxActive = Math.max(maxActive, active); - await Promise.resolve(); - active--; - done.resolve(); - return { result: params.prompt }; - } - ); - const session = new CopilotSession("session-nested-combinators", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "nested-combinators", - description: "Nested combinator deadlock regression", - phases: [], - }, - run: async ({ agent, parallel, pipeline }) => - parallel([ - () => parallel([() => agent("a"), () => agent("b")]), - () => pipeline(["c"], (_previous, item) => agent(item as string)), - ]), - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "nested-combinators", - runId: "run-nested-combinators", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: [["a", "b"], ["c"]] }); - expect(maxActive).toBe(1); - expect(sendRequest).toHaveBeenCalledTimes(3); - }); - - it("flushes buffered progress in finally when the factory body throws", async () => { - const sendRequest = vi.fn(async () => ({})); - const session = new CopilotSession("session-throw-progress", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "throw-progress", - description: "Throwing progress test", - phases: [], - }, - run: async ({ log }) => { - log("before throw"); - throw new Error("body failed"); - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "throw-progress", - runId: "run-throw-progress", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toThrow("body failed"); - expect(sendRequest).toHaveBeenCalledWith("session.factory.log", { - sessionId: session.sessionId, - runId: "run-throw-progress", - executionToken: "execution-token", - lines: [{ seq: 0, kind: "log", text: "before throw" }], - }); - }); - - it("keeps a completed execution successful when only the final progress flush fails", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.log") { - throw new Error("final transport failure"); - } - return {}; - }); - const warning = vi.spyOn(console, "warn").mockImplementation(() => {}); - const session = new CopilotSession("session-final-flush-failure", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "final-flush-failure", - description: "Final flush failure regression test", - phases: [], - }, - run: async ({ log }) => { - log("final line"); - return "done"; - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "final-flush-failure", - runId: "run-final-flush-failure", - executionToken: "execution-token", - args: {}, - }) - ).resolves.toEqual({ result: "done" }); - expect(warning).toHaveBeenCalledWith( - "Failed to flush final factory progress after the factory body settled", - expect.objectContaining({ message: "final transport failure" }) - ); - }); - - it("keeps a completed execution successful when a background progress flush fails", async () => { - vi.useFakeTimers(); - const release = Promise.withResolvers(); - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.log") { - throw new Error("background transport failure"); - } - return {}; - }); - const warning = vi.spyOn(console, "warn").mockImplementation(() => {}); - const session = new CopilotSession("session-background-flush-failure", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "background-flush-failure", - description: "Background flush failure regression test", - phases: [], - }, - run: async ({ log }) => { - log("background line"); - await release.promise; - return "done"; - }, - }); - session.registerFactories([factory]); - - try { - const execution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "background-flush-failure", - runId: "run-background-flush-failure", - executionToken: "execution-token", - args: {}, - }); - await vi.advanceTimersByTimeAsync(10_000); - await Promise.resolve(); - - release.resolve(); - - await expect(execution).resolves.toEqual({ result: "done" }); - expect(warning).toHaveBeenCalledWith( - "Ignoring a background factory progress flush failure after the factory body settled", - expect.objectContaining({ message: "background transport failure" }) - ); - } finally { - vi.useRealTimers(); - } - }); - - it("keeps a mid-run progress flush failure fatal", async () => { - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.log") { - throw new Error("mid-run transport failure"); - } - if (method === "session.factory.agent") { - return { result: "must not complete" }; - } - return {}; - }); - const session = new CopilotSession("session-mid-run-flush-failure", { - sendRequest, - } as never); - const factory = defineFactory({ - meta: { - name: "mid-run-flush-failure", - description: "Mid-run flush failure regression test", - phases: [], - }, - run: async ({ agent, log }) => { - log("before agent"); - return agent("trigger a flush"); - }, - }); - session.registerFactories([factory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "mid-run-flush-failure", - runId: "run-mid-run-flush-failure", - executionToken: "execution-token", - args: {}, - }) - ).rejects.toThrow("mid-run transport failure"); - expect(sendRequest).not.toHaveBeenCalledWith("session.factory.agent", expect.anything()); - }); - - it("surfaces the per-run abort signal on the factory context", async () => { - const session = new CopilotSession("session-abort-signal", {} as never); - const signalSeen = Promise.withResolvers(); - const factory = defineFactory({ - meta: { - name: "abort-signal", - description: "Abort signal test", - phases: [], - }, - run: async ({ signal }) => { - signalSeen.resolve(signal); - await new Promise((resolve) => - signal.addEventListener("abort", () => resolve(), { once: true }) - ); - return signal.aborted; - }, - }); - session.registerFactories([factory]); - - const execution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "abort-signal", - runId: "run-abort-signal", - executionToken: "execution-token", - args: {}, - }); - const signal = await signalSeen.promise; - expect(signal.aborted).toBe(false); - - await session.clientSessionApis.factory!.abort({ - sessionId: session.sessionId, - runId: "run-abort-signal", - executionToken: "execution-token", - }); - - expect(signal.aborted).toBe(true); - await expect(execution).resolves.toEqual({ result: true }); - }); - - it("rejects an in-flight runtime-backed await when factory.abort trips the signal", async () => { - const agentResponse = Promise.withResolvers<{ result: string }>(); - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.agent") { - return agentResponse.promise; - } - return {}; - }); - const session = new CopilotSession("session-abort-await", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: "abort-await", - description: "Abort an in-flight factory await", - phases: [], - }, - run: async ({ agent }) => agent("wait forever"), - }); - session.registerFactories([factory]); - - const execution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "abort-await", - runId: "run-abort-await", - executionToken: "execution-token", - args: {}, - }); - await vi.waitFor(() => - expect(sendRequest).toHaveBeenCalledWith("session.factory.agent", expect.anything()) - ); - - await session.clientSessionApis.factory!.abort({ - sessionId: session.sessionId, - runId: "run-abort-await", - executionToken: "execution-token", - }); - - await expect(execution).rejects.toMatchObject({ name: "AbortError" }); - agentResponse.resolve({ result: "late" }); - }); - - it("ignores a late abort for an older execution token with the same run id", async () => { - const oldAgent = Promise.withResolvers<{ result: string }>(); - const currentAgent = Promise.withResolvers<{ result: string }>(); - const sendRequest = vi.fn(async (method: string, params: { executionToken?: string }) => { - if (method !== "session.factory.agent") { - return {}; - } - return params.executionToken === "old-token" ? oldAgent.promise : currentAgent.promise; - }); - const session = new CopilotSession("session-token-scoped-abort", { - sendRequest, - } as never); - const signals: AbortSignal[] = []; - const factory = defineFactory({ - meta: { - name: "token-scoped-abort", - description: "Abort only the matching execution attempt", - phases: [], - }, - run: async ({ agent, signal }) => { - signals.push(signal); - return agent("wait"); - }, - }); - session.registerFactories([factory]); - - const oldExecution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "token-scoped-abort", - runId: "shared-run", - executionToken: "old-token", - args: {}, - }); - const currentExecution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "token-scoped-abort", - runId: "shared-run", - executionToken: "current-token", - args: {}, - }); - await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(2)); - - await session.clientSessionApis.factory!.abort({ - sessionId: session.sessionId, - runId: "shared-run", - executionToken: "old-token", - }); - expect(signals[0].aborted).toBe(true); - expect(signals[1].aborted).toBe(false); - await expect(oldExecution).rejects.toMatchObject({ name: "AbortError" }); - - currentAgent.resolve({ result: "current completed" }); - await expect(currentExecution).resolves.toEqual({ result: "current completed" }); - oldAgent.resolve({ result: "late old result" }); - }); - - it.each(["parallel", "pipeline"] as const)( - "propagates cancellation out of %s instead of mapping it to null", - async (combinator) => { - const agentResponse = Promise.withResolvers<{ result: string }>(); - const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.agent") { - return agentResponse.promise; - } - return {}; - }); - const session = new CopilotSession("session-abort-parallel", { sendRequest } as never); - const factory = defineFactory({ - meta: { - name: `abort-${combinator}`, - description: "Cancellation must bubble out of a combinator", - phases: [], - }, - // If the combinator swallowed the AbortError to null, this run would - // resolve successfully with [null] despite the run being cancelled. - run: async ({ agent, parallel, pipeline }) => - combinator === "parallel" - ? parallel([() => agent("wait forever")]) - : pipeline(["wait forever"], (_previous, item) => agent(item as string)), - }); - session.registerFactories([factory]); - - const execution = session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: `abort-${combinator}`, - runId: `run-abort-${combinator}`, - executionToken: "execution-token", - args: {}, - }); - await vi.waitFor(() => - expect(sendRequest).toHaveBeenCalledWith("session.factory.agent", expect.anything()) - ); - - await session.clientSessionApis.factory!.abort({ - sessionId: session.sessionId, - runId: `run-abort-${combinator}`, - executionToken: "execution-token", - }); - - await expect(execution).rejects.toMatchObject({ name: "AbortError" }); - agentResponse.resolve({ result: "late" }); - } - ); - - it("dispatches factory.execute to the registered factory selected by name", async () => { - const firstRun = vi.fn(async () => ({ selected: "first" })); - const secondRun = vi.fn(async ({ args, log }) => { - log("executing"); - return { selected: "second", echoed: args }; - }); - const firstFactory = defineFactory({ - meta: { - name: "first", - description: "First factory", - phases: [], - }, - run: firstRun, - }); - const secondFactory = defineFactory({ - meta: { - name: "second", - description: "Second factory", - phases: [], - }, - run: secondRun, - }); - const session = new CopilotSession("session-execute", { - sendRequest: vi.fn(async () => ({})), - } as never); - session.registerFactories([firstFactory, secondFactory]); - - await expect( - session.clientSessionApis.factory!.execute({ - sessionId: session.sessionId, - name: "second", - runId: "run-echo", - executionToken: "execution-token", - args: { message: "hello" }, - }) - ).resolves.toEqual({ - result: { selected: "second", echoed: { message: "hello" } }, - }); - expect(firstRun).not.toHaveBeenCalled(); - expect(secondRun).toHaveBeenCalledOnce(); - - const error = await session.clientSessionApis - .factory!.execute({ - sessionId: session.sessionId, - name: "missing", - runId: "run-missing", - args: {}, - }) - .catch((caught: unknown) => caught); - expect(error).toBeInstanceOf(ResponseError); - expect((error as ResponseError<{ code: string; name: string }>).data).toEqual({ - code: "factory_not_found", - name: "missing", - }); - }); - - it("runs fresh factories and routes direct and legacy resumes by ID without args", async () => { - const factory = defineFactory({ - meta: { - name: "friendly-run", - description: "Friendly run wrapper", - phases: [], - }, - run: async () => ({ unused: true }), - }); - const sendRequest = vi.fn(async (method: string, params: { name?: string }) => - method === "session.factory.resume" - ? { - factoryName: "stored-name", - run: { - runId: "run-prior", - status: "completed", - result: { name: "stored-name", persistedArgs: true }, - }, - } - : { - runId: "run-foreground", - status: "completed", - result: { name: params.name }, - } - ); - const session = new CopilotSession("session-run", { sendRequest } as never); - - await expect( - session.factory.resume("run-prior", { - limits: { maxTotalSubagents: 7 }, - notifyOnComplete: true, - logPhaseNames: true, - }) - ).resolves.toMatchObject({ - status: "completed", - result: { name: "stored-name", persistedArgs: true }, - }); - await expect( - session.factory.run("by-name", { - args: { value: 1 }, - limits: { maxTotalSubagents: 7 }, - notifyOnComplete: false, - logPhaseNames: true, - resumeFromRunId: "run-prior", - }) - ).resolves.toMatchObject({ - status: "completed", - result: { name: "stored-name", persistedArgs: true }, - }); - await expect( - session.factory.run(factory, { - notifyOnComplete: true, - logPhaseNames: false, - }) - ).resolves.toMatchObject({ - status: "completed", - result: { name: "friendly-run" }, - }); - expect(sendRequest).toHaveBeenNthCalledWith(1, "session.factory.resume", { - sessionId: session.sessionId, - runId: "run-prior", - limits: { maxTotalSubagents: 7 }, - notifyOnComplete: true, - logPhaseNames: true, - }); - expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.resume", { - sessionId: session.sessionId, - runId: "run-prior", - limits: { maxTotalSubagents: 7 }, - notifyOnComplete: false, - logPhaseNames: true, - }); - expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.run", { - sessionId: session.sessionId, - name: "friendly-run", - args: {}, - options: { - limits: undefined, - notifyOnComplete: true, - logPhaseNames: false, - }, - }); - }); - - it("preserves omitted, numeric, and explicit unlimited invocation limit overrides", async () => { - const sendRequest = vi.fn(async (method: string) => - method === "session.factory.resume" - ? { - factoryName: "stored-name", - run: { runId: "run-limits", status: "completed" }, - } - : { runId: "run-limits", status: "completed" } - ); - const session = new CopilotSession("session-limit-overrides", { - sendRequest, - } as never); - - await session.factory.run("omitted"); - await session.factory.run("numeric", { - limits: { maxTotalSubagents: 12 }, - }); - await session.factory.run("unlimited", { - limits: { maxTotalSubagents: null }, - }); - await session.factory.resume("run-limits", { - limits: { timeoutSeconds: null }, - }); - - expect(sendRequest.mock.calls[0][1]).toMatchObject({ - options: { limits: undefined }, - }); - expect(sendRequest.mock.calls[1][1]).toMatchObject({ - options: { limits: { maxTotalSubagents: 12 } }, - }); - expect(sendRequest.mock.calls[2][1]).toMatchObject({ - options: { limits: { maxTotalSubagents: null } }, - }); - expect(sendRequest.mock.calls[3][1]).toMatchObject({ - limits: { timeoutSeconds: null }, - }); - }); - - it("returns the full envelope for a failed foreground run", async () => { - const envelope = { - runId: "run-error", - status: "error" as const, - error: "factory failed", - snapshot: { completed: 1 }, - }; - const session = new CopilotSession("session-error", { - sendRequest: vi.fn(async () => envelope), - } as never); - - // A run that exists resolves with its envelope; only pre-execution - // failures (no run id) reject. - await expect(session.factory.run("failing")).resolves.toEqual(envelope); - }); - - it.each([ - "not_found", - "non_resumable", - "already_active", - "factory_already_running", - "factory_limits_invalid", - "factory_session_disposed", - "factory_storage_unavailable", - "factory_storage_corrupt", - ] as const)( - "throws FactoryResumeError with code %s for pre-execution failures", - async (code) => { - const session = new CopilotSession("session-resume-error", { - sendRequest: vi.fn(async () => { - throw new ResponseError(-32602, `resume failed: ${code}`, { code }); - }), - } as never); - - const error = await session.factory - .resume("run-error") - .catch((caught: unknown) => caught); - expect(error).toBeInstanceOf(FactoryResumeError); - expect((error as FactoryResumeError).code).toBe(code); - } - ); - - it("leaves an unreachable permission_denied response as a raw ResponseError", async () => { - const session = new CopilotSession("session-resume-permission-denied", { - sendRequest: vi.fn(async () => { - throw new ResponseError(-32602, "resume failed: permission_denied", { - code: "permission_denied", - }); - }), - } as never); - - const error = await session.factory.resume("run-error").catch((caught: unknown) => caught); - expect(error).toBeInstanceOf(ResponseError); - expect(error).not.toBeInstanceOf(FactoryResumeError); - expect((error as ResponseError<{ code: string }>).data.code).toBe("permission_denied"); - }); - - it("returns resumed execution failures as envelopes", async () => { - const envelope = { - runId: "run-execution-error", - status: "error" as const, - error: "resumed body failed", - }; - const session = new CopilotSession("session-resumed-run-error", { - sendRequest: vi.fn(async () => ({ factoryName: "stored-name", run: envelope })), - } as never); - - await expect(session.factory.resume("run-execution-error")).resolves.toEqual(envelope); - }); -}); - -describe("factory run settlement", () => { - it.each([ - ["completed", true], - ["error", true], - ["halted", true], - ["paused", true], - ["cancelled", true], - ["pending", false], - ["running", false], - ] as const)("classifies %s as terminal=%s", (status, expected) => { - expect(isFactoryRunTerminal(status)).toBe(expected); - }); - - it("resolves immediately when the run has already settled", async () => { - const envelope = { runId: "run-settled", status: "completed" as const, result: 42 }; - const sendRequest = vi.fn(async () => envelope); - const session = new CopilotSession("session-wait-settled", { sendRequest } as never); - - await expect(session.factory.waitForRun("run-settled")).resolves.toEqual(envelope); - expect(sendRequest).toHaveBeenCalledTimes(1); - expect(sendRequest).toHaveBeenCalledWith("session.factory.getRun", { - sessionId: session.sessionId, - runId: "run-settled", - }); - }); - - it("waits for a running run to reach a terminal status", async () => { - const running = { runId: "run-wait", status: "running" as const }; - const terminal = { runId: "run-wait", status: "completed" as const, result: "done" }; - let current: unknown = running; - const sendRequest = vi.fn(async () => current); - const session = new CopilotSession("session-wait-running", { sendRequest } as never); - - const settled = session.factory.waitForRun("run-wait"); - // The first read observed a running envelope, so the wait is still pending. - await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); - - // An invalidation event for an unrelated run must not trigger a re-read. - (session as never as { _dispatchEvent(event: unknown): void })._dispatchEvent( - runUpdatedEvent("some-other-run", 2) - ); - expect(sendRequest).toHaveBeenCalledTimes(1); - - current = terminal; - (session as never as { _dispatchEvent(event: unknown): void })._dispatchEvent( - runUpdatedEvent("run-wait", 3) - ); - - await expect(settled).resolves.toEqual(terminal); - }); - - it("periodically re-reads when a terminal invalidation is missed", async () => { - vi.useFakeTimers(); - const running = { runId: "run-poll", status: "running" as const }; - const terminal = { runId: "run-poll", status: "completed" as const, result: "polled" }; - let current: unknown = running; - const sendRequest = vi.fn(async () => current); - const session = new CopilotSession("session-wait-poll", { sendRequest } as never); - - try { - const settled = session.factory.waitForRun("run-poll"); - await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); - - current = terminal; - await vi.advanceTimersByTimeAsync(5_000); - - await expect(settled).resolves.toEqual(terminal); - expect(sendRequest).toHaveBeenCalledTimes(2); - } finally { - vi.useRealTimers(); - } - }); - - it("stops watching once the run settles", async () => { - const running = { runId: "run-unsub", status: "running" as const }; - const terminal = { runId: "run-unsub", status: "error" as const, error: "body failed" }; - let current: unknown = running; - const sendRequest = vi.fn(async () => current); - const session = new CopilotSession("session-wait-unsub", { sendRequest } as never); - const handlersFor = (): Set | undefined => - ( - session as never as { - typedEventHandlers: Map>; - } - ).typedEventHandlers.get("factory.run_updated"); - - const settled = session.factory.waitForRun("run-unsub"); - await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); - expect(handlersFor()?.size ?? 0).toBe(1); - - current = terminal; - (session as never as { _dispatchEvent(event: unknown): void })._dispatchEvent( - runUpdatedEvent("run-unsub", 2) - ); - await expect(settled).resolves.toEqual(terminal); - - // The subscription must be released, or every completed wait leaks a - // listener for the lifetime of the session. - expect(handlersFor()?.size ?? 0).toBe(0); - - const callsAtSettlement = sendRequest.mock.calls.length; - // A late event for a settled run must not provoke another read. - (session as never as { _dispatchEvent(event: unknown): void })._dispatchEvent( - runUpdatedEvent("run-unsub", 3) - ); - expect(sendRequest).toHaveBeenCalledTimes(callsAtSettlement); - }); - - it("rejects when the signal is already aborted and never reads", async () => { - const sendRequest = vi.fn(async () => ({ runId: "run-pre", status: "running" })); - const session = new CopilotSession("session-wait-pre-abort", { sendRequest } as never); - - await expect( - session.factory.waitForRun("run-pre", { signal: AbortSignal.abort() }) - ).rejects.toThrow(); - expect(sendRequest).not.toHaveBeenCalled(); - }); - - it("rejects when aborted while waiting, leaving the run untouched", async () => { - const sendRequest = vi.fn(async () => ({ runId: "run-abort", status: "running" })); - const session = new CopilotSession("session-wait-abort", { sendRequest } as never); - const controller = new AbortController(); - - const settled = session.factory.waitForRun("run-abort", { signal: controller.signal }); - await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); - - controller.abort(); - await expect(settled).rejects.toThrow(); - // Aborting the wait must not cancel the run. - expect(sendRequest).not.toHaveBeenCalledWith("session.factory.cancel", expect.anything()); - }); - - it("propagates a read failure", async () => { - const sendRequest = vi.fn(async () => { - throw new Error("factory_storage_unavailable"); - }); - const session = new CopilotSession("session-wait-error", { sendRequest } as never); - - await expect(session.factory.waitForRun("run-broken")).rejects.toThrow( - "factory_storage_unavailable" - ); - }); - - it("collapses a burst of invalidation events into one in-flight read", async () => { - const running = { runId: "run-burst", status: "running" as const }; - const terminal = { runId: "run-burst", status: "completed" as const }; - let release: (() => void) | undefined; - const gate = new Promise((resolve) => (release = resolve)); - let readCount = 0; - const sendRequest = vi.fn(async () => { - readCount += 1; - if (readCount === 2) { - await gate; - } - // Reads 1 and 2 observe a running run; only the coalesced third - // read observes the terminal one. - return readCount >= 3 ? terminal : running; - }); - const session = new CopilotSession("session-wait-burst", { sendRequest } as never); - - const settled = session.factory.waitForRun("run-burst"); - await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); - - const dispatch = (revision: number): void => - (session as never as { _dispatchEvent(event: unknown): void })._dispatchEvent( - runUpdatedEvent("run-burst", revision) - ); - - // Second read is held open while three more events arrive; they must - // collapse into a single follow-up read rather than three. - dispatch(2); - await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(2)); - dispatch(3); - dispatch(4); - dispatch(5); - expect(sendRequest).toHaveBeenCalledTimes(2); - - release?.(); - await expect(settled).resolves.toEqual(terminal); - // One initial read, the held read, and exactly one coalesced re-read - // standing in for all three queued events. - expect(sendRequest).toHaveBeenCalledTimes(3); - }); -}); 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/fixtures/mcp-plan-transport-choice.schema.json b/nodejs/test/fixtures/mcp-plan-transport-choice.schema.json new file mode 100644 index 0000000000..24be6ad517 --- /dev/null +++ b/nodejs/test/fixtures/mcp-plan-transport-choice.schema.json @@ -0,0 +1,169 @@ +{ + "definitions": { + "McpPlanTransportChoice": { + "anyOf": [ + { + "$ref": "#/definitions/McpPlanTransportChoicePackage", + "description": "An eligible local-package transport choice. Package identity is required and a remote endpoint cannot be represented." + }, + { + "$ref": "#/definitions/McpPlanTransportChoiceRemote", + "description": "An eligible remote-endpoint transport choice. The endpoint is required and package identity cannot be represented." + } + ], + "description": "One eligible way to run the server, represented as a tagged package or remote variant so package identity and endpoint states cannot contradict the install method.", + "title": "McpPlanTransportChoice" + }, + "McpPlanTransportChoicePackage": { + "type": "object", + "properties": { + "choiceId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Stable identifier for this choice within the plan, used to select it when the plan is applied." + }, + "transport": { + "$ref": "#/definitions/McpPlanPackageTransport", + "description": "Local process transport this package choice would use." + }, + "installMethod": { + "$ref": "#/definitions/McpPlanPackageInstallMethod", + "const": "package", + "description": "Discriminator: this choice runs a local package" + }, + "packageType": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Packaging ecosystem, for example `oci` or `npm`." + }, + "packageIdentifier": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "Package identifier. Inert untrusted data." + }, + "requiredValues": { + "type": "array", + "items": { + "$ref": "#/definitions/McpPlanRequiredValue", + "description": "One non-secret value a transport choice needs, represented as a scalar or enumerated variant so enum values cannot be missing or attached to another type." + }, + "description": "Typed values this choice requires, excluding secrets." + }, + "secretPlaceholders": { + "type": "array", + "items": { + "$ref": "#/definitions/McpPlanSecretPlaceholder", + "description": "A secret a transport choice needs, referenced by placeholder. No secret value ever appears in a plan, and the placeholder resolves against the keychain only when a plan is applied." + }, + "description": "Secrets this choice requires, referenced by placeholder only." + } + }, + "required": [ + "choiceId", + "transport", + "installMethod", + "packageType", + "packageIdentifier", + "requiredValues", + "secretPlaceholders" + ], + "additionalProperties": false, + "description": "An eligible local-package transport choice. Package identity is required and a remote endpoint cannot be represented.", + "title": "McpPlanTransportChoicePackage" + }, + "McpPlanTransportChoiceRemote": { + "type": "object", + "properties": { + "choiceId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Stable identifier for this choice within the plan, used to select it when the plan is applied." + }, + "transport": { + "$ref": "#/definitions/McpPlanRemoteTransport", + "description": "Endpoint transport this remote choice would use." + }, + "installMethod": { + "$ref": "#/definitions/McpPlanRemoteInstallMethod", + "const": "remote", + "description": "Discriminator: this choice connects to a remote endpoint" + }, + "endpoint": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "Endpoint URL. Inert untrusted data." + }, + "requiredValues": { + "type": "array", + "items": { + "$ref": "#/definitions/McpPlanRequiredValue", + "description": "One non-secret value a transport choice needs, represented as a scalar or enumerated variant so enum values cannot be missing or attached to another type." + }, + "description": "Typed values this choice requires, excluding secrets." + }, + "secretPlaceholders": { + "type": "array", + "items": { + "$ref": "#/definitions/McpPlanSecretPlaceholder", + "description": "A secret a transport choice needs, referenced by placeholder. No secret value ever appears in a plan, and the placeholder resolves against the keychain only when a plan is applied." + }, + "description": "Secrets this choice requires, referenced by placeholder only." + } + }, + "required": [ + "choiceId", + "transport", + "installMethod", + "endpoint", + "requiredValues", + "secretPlaceholders" + ], + "additionalProperties": false, + "description": "An eligible remote-endpoint transport choice. The endpoint is required and package identity cannot be represented.", + "title": "McpPlanTransportChoiceRemote" + }, + "McpPlanPackageInstallMethod": { + "type": "string", + "enum": ["package"], + "description": "Discriminator for a package-backed transport choice", + "title": "McpPlanPackageInstallMethod", + "x-enumDescriptions": { + "package": "Install and run a local package." + } + }, + "McpPlanRemoteInstallMethod": { + "type": "string", + "enum": ["remote"], + "description": "Discriminator for a remote-endpoint transport choice", + "title": "McpPlanRemoteInstallMethod", + "x-enumDescriptions": { + "remote": "Connect to a remote endpoint." + } + }, + "McpPlanPackageTransport": { + "type": "string", + "enum": ["stdio"], + "description": "Transport exposed by a locally launched package", + "title": "McpPlanPackageTransport", + "x-enumDescriptions": { + "stdio": "A locally launched process spoken to over standard input and output." + } + }, + "McpPlanRemoteTransport": { + "type": "string", + "enum": ["http", "streamable-http", "sse"], + "description": "Transport exposed by a remote endpoint", + "title": "McpPlanRemoteTransport", + "x-enumDescriptions": { + "http": "An HTTP endpoint.", + "streamable-http": "A streamable HTTP endpoint.", + "sse": "A server-sent events endpoint." + } + } + } +} diff --git a/nodejs/test/helpers/withTestCleanup.ts b/nodejs/test/helpers/withTestCleanup.ts new file mode 100644 index 0000000000..49e9d312db --- /dev/null +++ b/nodejs/test/helpers/withTestCleanup.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** Attempt every cleanup without hiding the test-body failure or accepting failed teardown. */ +export async function withTestCleanup( + run: () => Promise, + ...cleanups: Array<() => void | Promise> +): Promise { + const outcome = await Promise.resolve() + .then(run) + .then( + (value) => ({ value }), + (error: unknown) => ({ error }) + ); + const cleanupErrors: unknown[] = []; + for (const cleanup of cleanups) { + try { + await cleanup(); + } catch (error) { + cleanupErrors.push(error); + } + } + if ("error" in outcome) { + if (cleanupErrors.length > 0) { + throw new AggregateError( + [outcome.error, ...cleanupErrors], + "Test body and cleanup failed", + { + cause: outcome.error, + } + ); + } + throw outcome.error; + } + if (cleanupErrors.length === 1) { + throw cleanupErrors[0]; + } + if (cleanupErrors.length > 1) { + throw new AggregateError(cleanupErrors, "Test cleanup failed"); + } + return outcome.value; +} diff --git a/nodejs/test/host-client.test.ts b/nodejs/test/host-client.test.ts new file mode 100644 index 0000000000..98d2f48e28 --- /dev/null +++ b/nodejs/test/host-client.test.ts @@ -0,0 +1,1077 @@ +import { once } from "node:events"; +import { createServer, type Socket } from "node:net"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { + createMessageConnection, + type MessageConnection, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node.js"; +import { approveAll, CopilotClient, RuntimeConnection, type AhpHostOptions } from "../src/index.js"; +import type { HostStartRequest } from "../src/generated/rpc.js"; + +// Wire-level SDK unit tests. Real runtime/listener coverage lives in test/e2e. +async function fixture( + configure: (rpc: MessageConnection, socket: Socket, writer: StreamMessageWriter) => void +) { + const sockets = new Set(); + const connections = new Set(); + const server = createServer((socket) => { + sockets.add(socket); + socket.once("close", () => sockets.delete(socket)); + const writer = new StreamMessageWriter(socket); + const rpc = createMessageConnection(new StreamMessageReader(socket), writer); + connections.add(rpc); + rpc.onRequest("connect", () => ({ protocolVersion: 3 })); + rpc.onRequest("ping", () => ({ message: "ok", timestamp: Date.now() })); + configure(rpc, socket, writer); + rpc.listen(); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Missing fixture address"); + const client = new CopilotClient({ + connection: RuntimeConnection.forUri(`127.0.0.1:${address.port}`), + }); + onTestFinished(async () => { + await client.forceStop(); + for (const rpc of connections) rpc.dispose(); + for (const socket of sockets) socket.destroy(); + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + }); + return client; +} + +function info(hostId: string) { + return { hostId, url: "ws://127.0.0.1:54321", token: "test-only-token" }; +} + +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) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("session.create", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + rpc.onRequest("session.resume", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + }); + const released = vi.fn(); + const replacement = vi.fn(); + const options: AhpHostOptions = { + localServer: {}, + createSession: ({ config }) => + client.createSession({ ...config, onPermissionRequest: approveAll }), + resumeSession: ({ sessionId, config }) => + client.resumeSession(sessionId, { ...config, onPermissionRequest: approveAll }), + onSessionReleased: released, + }; + const host = await client.startAhpHost(options); + options.createSession = replacement; + options.resumeSession = undefined; + options.onSessionReleased = replacement; + for (const resume of [false, true]) { + const sessionId = resume ? "resumed" : "created"; + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: sessionId, + resume, + config: { sessionId }, + }) + ).resolves.toEqual({ sessionId }); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: sessionId, + }); + } + await vi.waitFor(() => expect(released).toHaveBeenCalledTimes(2)); + expect(replacement).not.toHaveBeenCalled(); + }); + + it.each([false, true])( + "normalizes the configuration directory before invoking factories (resume=%s)", + async (resume) => { + let rpc!: MessageConnection; + const materialize = vi.fn(({ sessionId }: { sessionId: string }) => ({ sessionId })); + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest(resume ? "session.resume" : "session.create", materialize); + }); + const checkConfig = (config: object) => { + expect(config).toHaveProperty("configDirectory", "/selected-config"); + expect(config).not.toHaveProperty("configDir"); + }; + const host = await client.startAhpHost({ + localServer: {}, + createSession: ({ config }) => { + checkConfig(config); + return client.createSession({ ...config, onPermissionRequest: approveAll }); + }, + resumeSession: ({ sessionId, config }) => { + checkConfig(config); + return client.resumeSession(sessionId, { + ...config, + onPermissionRequest: approveAll, + }); + }, + }); + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "config", + resume, + config: { sessionId: "configured", configDir: "/selected-config" }, + }) + ).resolves.toEqual({ sessionId: "configured" }); + expect(materialize.mock.calls[0][0]).toMatchObject({ + configDir: "/selected-config", + }); + } + ); + + it("reports listener-task exit without a process PID or exit code", async () => { + let rpc!: MessageConnection; + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + }); + const onExit = vi.fn(); + const host = await client.startAhpHost({ localServer: {}, onExit }); + expect(host.pid).toBeUndefined(); + const exit = { hostId: host.hostId, reason: "exited" }; + await rpc.sendNotification("host.exited", exit); + await client.ping(); + expect(onExit).toHaveBeenCalledExactlyOnceWith(exit); + expect(onExit.mock.calls[0][0]).not.toHaveProperty("exitCode"); + }); + + it("resumes with the retained original after its creation snapshot was consumed", async () => { + let rpc!: MessageConnection; + const create = vi.fn(({ sessionId }: { sessionId: string }) => ({ sessionId })); + const resume = vi.fn(); + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("session.create", create); + rpc.onRequest("session.resume", resume); + }); + const released = vi.fn(); + let original: Awaited>; + const host = await client.startAhpHost({ + localServer: {}, + createSession: async ({ config }) => { + original = await client.createSession({ + ...config, + onPermissionRequest: approveAll, + }); + return original; + }, + resumeSession: async () => original, + onSessionReleased: released, + }); + const config = { sessionId: "retained", workingDirectory: "/workspace" }; + await rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "create", + config, + }); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: "create", + }); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original)); + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "resume", + resume: true, + config: { ...config, continuePendingWork: false, suppressResumeEvent: true }, + }) + ).resolves.toEqual({ sessionId: "retained" }); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: "resume", + }); + await vi.waitFor(() => expect(released).toHaveBeenCalledTimes(2)); + expect(released.mock.calls[1][0]).toBe(original!); + expect(create).toHaveBeenCalledOnce(); + expect(resume).not.toHaveBeenCalled(); + }); + + it.each([false, true])( + "does not retain configuration snapshots from a failed factory (resume=%s)", + async (resume) => { + let rpc!: MessageConnection; + const materialize = vi.fn(({ sessionId }: { sessionId: string }) => ({ sessionId })); + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("session.create", materialize); + rpc.onRequest("session.resume", materialize); + }); + let original: Awaited> | undefined; + const host = await client.startAhpHost({ + localServer: {}, + createSession: async ({ config }) => { + original = await client.createSession({ + ...config, + onPermissionRequest: approveAll, + }); + throw new Error("application setup failed"); + }, + resumeSession: async ({ sessionId, config }) => { + if (original) return original; + original = await client.resumeSession(sessionId, { + ...config, + onPermissionRequest: approveAll, + }); + throw new Error("application setup failed"); + }, + }); + const config = { sessionId: "retained", workingDirectory: "/workspace" }; + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "failed", + resume, + config, + }) + ).rejects.toThrow("application setup failed"); + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "resume", + resume: true, + config: { ...config, continuePendingWork: false, suppressResumeEvent: true }, + }) + ).resolves.toEqual({ sessionId: "retained" }); + expect(client["sessions"].get("retained")).toBe(original); + expect(materialize).toHaveBeenCalledOnce(); + } + ); + + it("registers a distinct resume callback before start and releases the original object", async () => { + let rpc!: MessageConnection; + const creates = vi.fn(); + const resumes = vi.fn(({ sessionId }: { sessionId: string }) => ({ sessionId })); + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("session.resume", resumes); + rpc.onRequest("host.start", async (request: HostStartRequest) => { + expect(request.resumeFactory).toBe(true); + await rpc.sendRequest("host.materializeSession", { + hostId: request.hostId, + handoffId: "resume", + resume: true, + config: { + sessionId: "durable", + workingDirectory: "/workspace", + continuePendingWork: false, + suppressResumeEvent: true, + }, + }); + return info(request.hostId); + }); + }); + const released = vi.fn(); + const host = await client.startAhpHost({ + localServer: {}, + createSession: creates, + resumeSession: ({ sessionId, config }) => + client.resumeSession(sessionId, { ...config, onPermissionRequest: approveAll }), + onSessionReleased: released, + }); + const original = client["sessions"].get("durable"); + expect(original).toBeDefined(); + expect(creates).not.toHaveBeenCalled(); + expect(resumes).toHaveBeenCalledOnce(); + expect(resumes.mock.calls[0][0]).toMatchObject({ + sessionId: "durable", + workingDirectory: "/workspace", + continuePendingWork: false, + disableResume: true, + }); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: "resume", + }); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original)); + expect(client["sessions"].get("durable")).toBe(original); + }); + + it("does not fall back to the create callback for an application resume", async () => { + let rpc!: MessageConnection; + const creates = vi.fn(); + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + }); + const host = await client.startAhpHost({ localServer: {}, createSession: creates }); + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "missing-resume", + resume: true, + config: { sessionId: "durable" }, + }) + ).rejects.toThrow("unavailable"); + expect(creates).not.toHaveBeenCalled(); + }); + + it.each([false, true])( + "releases late results without stale captures (resume=%s)", + async (resume) => { + let rpc!: MessageConnection; + let finish!: () => void; + const gate = new Promise((resolve) => { + finish = resolve; + }); + let original: Awaited> | undefined; + let signal: AbortSignal | undefined; + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("session.resume", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + rpc.onRequest("session.create", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + }); + const released = vi.fn(); + const host = await client.startAhpHost({ + localServer: {}, + createSession: async (request) => { + signal = request.signal; + original = await client.createSession({ + ...request.config, + onPermissionRequest: approveAll, + }); + await gate; + return original; + }, + resumeSession: async (request) => { + if (original) return original; + signal = request.signal; + original = await client.resumeSession(request.sessionId, { + ...request.config, + onPermissionRequest: approveAll, + }); + await gate; + return original; + }, + onSessionReleased: released, + }); + const response = rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "late-resume", + resume, + config: { sessionId: "durable" }, + }); + const rejected = expect(response).rejects.toThrow("handoff ended"); + await vi.waitFor(() => expect(original).toBeDefined()); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: "late-resume", + }); + await rejected; + expect(signal?.aborted).toBe(true); + expect(released).not.toHaveBeenCalled(); + finish(); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original)); + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "retained-resume", + resume: true, + config: { sessionId: "durable", continuePendingWork: false }, + }) + ).resolves.toEqual({ sessionId: "durable" }); + } + ); + + it("publishes an existing session using the same owner's host RPC", async () => { + const publish = vi.fn(({ sessionId }: { sessionId: string }) => ({ + sessionId, + sessionUri: `copilot:/${sessionId}`, + })); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("host.publishSession", publish); + }); + const host = await client.startAhpHost({ localServer: {} }); + await expect(host.publishSession("existing")).resolves.toEqual({ + sessionId: "existing", + sessionUri: "copilot:/existing", + }); + expect(publish.mock.calls[0][0]).toEqual({ hostId: host.hostId, sessionId: "existing" }); + }); + it("allows the owner callback to create a session while host.start is still pending", async () => { + const client = await fixture((rpc) => { + rpc.onRequest("session.create", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + rpc.onRequest("host.start", async ({ hostId }: HostStartRequest) => { + await rpc.sendRequest("host.materializeSession", { + hostId, + handoffId: "during-start", + config: { sessionId: "during-start" }, + }); + return info(hostId); + }); + }); + const released = vi.fn(); + const host = await client.startAhpHost({ + localServer: {}, + createSession: ({ config }) => + client.createSession({ + ...config, + onPermissionRequest: approveAll, + }), + onSessionReleased: released, + }); + const original = client["sessions"].get("during-start"); + expect(original).toBeDefined(); + expect(host.hostId).toBeTruthy(); + expect(released).not.toHaveBeenCalled(); + await client.forceStop(); + expect(released).toHaveBeenCalledExactlyOnceWith(original); + expect(client["hostSessionFactories"].size).toBe(0); + expect(client["hostHandoffs"].size).toBe(0); + }); + + it("rejects a different client's object without disconnecting that object", async () => { + let rpc!: MessageConnection; + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + }); + const other = await fixture((connection) => { + connection.onRequest("session.create", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + }); + const original = await other.createSession({ + sessionId: "other-client", + onPermissionRequest: approveAll, + }); + const released = vi.fn(); + const host = await client.startAhpHost({ + localServer: {}, + createSession: async () => original, + onSessionReleased: released, + }); + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "wrong-owner", + config: { sessionId: original.sessionId }, + }) + ).rejects.toThrow("this client"); + expect(released).toHaveBeenCalledExactlyOnceWith(original); + expect(other["sessions"].get(original.sessionId)).toBe(original); + }); + + it("keeps a normal app session and its tools until one release without disconnecting it", async () => { + let rpc!: MessageConnection; + const creates = vi.fn(({ sessionId }: { sessionId: string }) => ({ sessionId })); + const disconnect = vi.fn(() => ({})); + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("session.create", creates); + rpc.onRequest("session.detach", disconnect); + }); + const onSessionReleased = vi.fn(); + const toolHandler = vi.fn(() => "app-result"); + const host = await client.startAhpHost({ + localServer: {}, + createSession: ({ config }) => + client.createSession({ + ...config, + onPermissionRequest: approveAll, + systemMessage: { mode: "append", content: "app prompt" }, + tools: [{ name: "app_tool", parameters: {}, handler: toolHandler }], + }), + onSessionReleased, + }); + const result = await rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "participation", + config: { sessionId: "app-session", workingDirectory: "/workspace", streaming: true }, + }); + expect(result).toEqual({ sessionId: "app-session" }); + const original = client["sessions"].get("app-session"); + expect(original).toBeDefined(); + expect(creates.mock.calls[0]?.[0]).toMatchObject({ + systemMessage: { content: "app prompt" }, + tools: [{ name: "app_tool" }], + }); + expect(onSessionReleased).not.toHaveBeenCalled(); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: "participation", + }); + await vi.waitFor(() => expect(onSessionReleased).toHaveBeenCalledExactlyOnceWith(original)); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: "participation", + }); + await rpc.sendNotification("host.exited", { hostId: host.hostId, reason: "exited" }); + await vi.waitFor(() => expect(client["hostSessionFactories"].size).toBe(0)); + expect(onSessionReleased).toHaveBeenCalledOnce(); + expect(disconnect).not.toHaveBeenCalled(); + expect(client["hostHandoffs"].size).toBe(0); + expect(client["sessions"].get("app-session")).toBe(original); + }); + + it("cancels a pending handoff and releases a late original object once", async () => { + let rpc!: MessageConnection; + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("session.create", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + }); + let finish!: () => void; + const pending = new Promise((resolve) => { + finish = resolve; + }); + let signal: AbortSignal | undefined; + let original: Awaited> | undefined; + const released = vi.fn(); + const host = await client.startAhpHost({ + localServer: {}, + createSession: async (request) => { + signal = request.signal; + await pending; + original = await client.createSession({ + ...request.config, + onPermissionRequest: approveAll, + }); + return original; + }, + onSessionReleased: released, + }); + const creation = rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "pending", + config: { sessionId: "late-session" }, + }); + const failed = expect(creation).rejects.toThrow("handoff ended"); + await vi.waitFor(() => expect(signal).toBeDefined()); + await rpc.sendNotification("host.sessionReleased", { + hostId: host.hostId, + handoffId: "pending", + }); + await failed; + expect(signal?.aborted).toBe(true); + expect(released).not.toHaveBeenCalled(); + finish(); + await vi.waitFor(() => expect(released).toHaveBeenCalledExactlyOnceWith(original)); + expect(client["hostHandoffs"].size).toBe(0); + }); + + it("rejects ignored host settings and releases the returned object", async () => { + let rpc!: MessageConnection; + const client = await fixture((connection) => { + rpc = connection; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("session.create", ({ sessionId }: { sessionId: string }) => ({ + sessionId, + })); + }); + const released = vi.fn(); + const host = await client.startAhpHost({ + localServer: {}, + createSession: ({ config }) => + client.createSession({ + sessionId: config.sessionId, + onPermissionRequest: approveAll, + }), + onSessionReleased: released, + }); + await expect( + rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "wrong-config", + config: { sessionId: "selected-session", enableConfigDiscovery: true }, + }) + ).rejects.toThrow("preserve"); + expect(released).toHaveBeenCalledExactlyOnceWith( + client["sessions"].get("selected-session") + ); + }); + + 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({ localServer: {} }); + expect(start.mock.calls[0]?.[0]).toEqual({ hostId: host.hostId, localServer: {} }); + expect(host.token).toBeUndefined(); + expect(client["hostExitCallbacks"].size).toBe(0); + }); + + it("leaves defaults to the runtime and forwards every disposal without synthesizing exits", async () => { + const start = vi.fn(({ hostId }: HostStartRequest) => info(hostId)); + const dispose = vi.fn((_params: { hostId: string }) => ({})); + const onExit = vi.fn(); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", start); + rpc.onRequest("host.dispose", dispose); + }); + + 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, localServer: {} }); + expect(client).not.toHaveProperty("startHost"); + await Promise.all([host.dispose(), host.dispose()]); + await host.dispose(); + await host[Symbol.asyncDispose](); + expect(dispose).toHaveBeenCalledTimes(4); + expect(dispose.mock.calls.map(([params]) => params)).toEqual( + Array.from({ length: 4 }, () => ({ hostId: host.hostId })) + ); + expect(onExit).not.toHaveBeenCalled(); + }); + + it("forwards only wire options, preserving explicit values and generating its own ID", async () => { + const start = vi.fn(({ hostId }: HostStartRequest) => info(hostId)); + 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 } = { + localServer: { + hostname: "::1", + port: 0, + token: "explicit-test-token", + requireConnectionToken: false, + }, + onExit, + hostId: "caller-cannot-select-id", + workingDirectory: "/not-sent", + }; + + const host = await client.startAhpHost(options); + expect(start.mock.calls[0]?.[0]).toEqual({ + hostId: host.hostId, + localServer: options.localServer, + }); + expect(host.hostId).not.toBe(options.hostId); + expect(onExit).not.toHaveBeenCalled(); + }); + + it("leaves listener validation and startup errors to the runtime", async () => { + const start = vi.fn(() => { + throw new Error("Runtime rejected listener configuration"); + }); + const client = await fixture((rpc) => rpc.onRequest("host.start", start)); + const options = { + localServer: { + hostname: "", + port: -1, + token: "", + requireConnectionToken: true, + }, + }; + + await expect(client.startAhpHost(options)).rejects.toThrow( + "Runtime rejected listener configuration" + ); + expect(start).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining(options), + expect.anything() + ); + }); + + it("does not lose an exit delivered before the start response", async () => { + const onExit = vi.fn(); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", async ({ hostId }: HostStartRequest) => { + await rpc.sendNotification("host.exited", { + hostId, + reason: "exited", + error: "hosting task failed", + }); + return info(hostId); + }); + }); + + const host = await client.startAhpHost({ localServer: {}, onExit }); + expect(onExit).toHaveBeenCalledExactlyOnceWith({ + hostId: host.hostId, + reason: "exited", + error: "hosting task failed", + }); + expect(client["hostExitCallbacks"].size).toBe(0); + }); + + it("propagates startup errors and releases the callback registration", async () => { + const onExit = vi.fn(); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", () => { + throw new Error("Failed to bind AHP listener: 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(); + }); + + it("releases early-exit registrations even if startup subsequently fails", async () => { + const onExit = vi.fn(); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", async ({ hostId }: HostStartRequest) => { + await rpc.sendNotification("host.exited", { hostId, reason: "exited" }); + throw new Error("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(); + expect(onExit).toHaveBeenCalledOnce(); + }); + + it("rejects startup when the owner connection closes before readiness", async () => { + const onExit = vi.fn(); + const client = await fixture((rpc, socket) => { + rpc.onRequest("host.start", () => { + socket.destroy(); + return new Promise(() => {}); + }); + }); + + await expect(client.startAhpHost({ localServer: {}, onExit })).rejects.toThrow(); + expect(onExit).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ reason: "ownerDisconnected" }) + ); + expect(client["hostExitCallbacks"].size).toBe(0); + }); + + it("drains notifications and successful responses received immediately before EOF", async () => { + const client = await fixture((rpc, socket, writer) => { + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("ping", () => new Promise(() => {})); + const write = writer.write.bind(writer); + vi.spyOn(writer, "write").mockImplementation((message) => { + if ( + "result" in message && + typeof message.result === "object" && + message.result !== null && + "hostId" in message.result + ) { + const hostId = message.result.hostId; + const messages = [ + ...Array.from({ length: 16 }, (_, index) => ({ + jsonrpc: "2.0", + method: "host.exited", + params: { + hostId: index === 15 ? hostId : `unrelated-${index}`, + reason: "exited", + exitCode: 0, + }, + })), + message, + ]; + socket.end( + messages + .map((value) => { + const body = JSON.stringify(value); + return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`; + }) + .join("") + ); + return Promise.resolve(); + } + return write(message); + }); + }); + await client.start(); + const unanswered = expect(client.ping()).rejects.toThrow(); + const onExit = vi.fn(); + const host = await client.startAhpHost({ localServer: {}, onExit }); + expect(onExit).toHaveBeenCalledExactlyOnceWith({ + hostId: host.hostId, + reason: "exited", + exitCode: 0, + }); + await unanswered; + }); + + it("suppresses closed-stream response failures when EOF cancels a pending factory", async () => { + let rpc!: MessageConnection; + let socket!: Socket; + const client = await fixture((connection, transport) => { + rpc = connection; + socket = transport; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + }); + let signal: AbortSignal | undefined; + const onExit = vi.fn(); + const host = await client.startAhpHost({ + localServer: {}, + createSession: (request) => { + signal = request.signal; + return new Promise(() => {}); + }, + onExit, + }); + const pending = rpc.sendRequest("host.materializeSession", { + hostId: host.hostId, + handoffId: "pending", + config: { sessionId: "pending" }, + }); + const rejected = expect(pending).rejects.toThrow(); + await vi.waitFor(() => expect(signal).toBeDefined()); + const writer = client["messageWriter"]!; + const write = writer.write.bind(writer); + const failures: unknown[] = []; + const response = vi.spyOn(writer, "write").mockImplementation((message) => { + const result = write(message); + void result.catch((error) => failures.push(error)); + return result; + }); + const closedWrite = vi + .spyOn(StreamMessageWriter.prototype, "write") + .mockRejectedValueOnce( + Object.assign(new Error("stream destroyed"), { code: "ERR_STREAM_DESTROYED" }) + ); + onTestFinished(() => closedWrite.mockRestore()); + socket.end(); + await vi.waitFor(() => expect(response).toHaveBeenCalledOnce()); + rpc.dispose(); + await rejected; + await expect(response.mock.results[0].value).resolves.toBeUndefined(); + expect(failures).toEqual([]); + expect(signal!.aborted).toBe(true); + expect(onExit).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ reason: "ownerDisconnected" }) + ); + }); + + it("keeps concurrent hosts independently disposable", async () => { + const dispose = vi.fn(); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("host.dispose", async ({ hostId }: { hostId: string }) => { + dispose(hostId); + await rpc.sendNotification("host.exited", { hostId, reason: "disposed" }); + return {}; + }); + }); + const firstExit = vi.fn(); + const secondExit = vi.fn(); + const [first, second] = await Promise.all([ + client.startAhpHost({ localServer: {}, onExit: firstExit }), + client.startAhpHost({ localServer: {}, onExit: secondExit }), + ]); + + expect(first.hostId).not.toBe(second.hostId); + await first.dispose(); + expect(firstExit).toHaveBeenCalledOnce(); + expect(secondExit).not.toHaveBeenCalled(); + await second.dispose(); + expect(secondExit).toHaveBeenCalledOnce(); + expect(dispose.mock.calls.map(([hostId]) => hostId)).toEqual([first.hostId, second.hostId]); + expect(client["hostExitCallbacks"].size).toBe(0); + }); + + it("routes exits by ID at most once and still forwards disposal after exit", async () => { + let serverRpc!: MessageConnection; + const dispose = vi.fn(() => ({})); + const onExit = vi.fn(); + const client = await fixture((rpc) => { + serverRpc = rpc; + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("host.dispose", dispose); + }); + const host = await client.startAhpHost({ localServer: {}, onExit }); + await serverRpc.sendNotification("host.exited", { hostId: "unrelated", reason: "exited" }); + await client.ping(); + expect(onExit).not.toHaveBeenCalled(); + const exit = { + hostId: host.hostId, + reason: "runtimeShutdown", + exitCode: null, + error: null, + }; + await serverRpc.sendNotification("host.exited", exit); + await serverRpc.sendNotification("host.exited", exit); + await client.ping(); + expect(onExit).toHaveBeenCalledExactlyOnceWith(exit); + expect(client["hostExitCallbacks"].size).toBe(0); + await Promise.all([host.dispose(), host.dispose()]); + expect(dispose).toHaveBeenCalledTimes(2); + await client.forceStop(); + expect(onExit).toHaveBeenCalledOnce(); + }); + + it("does not retry failed disposal or report a synthetic exit", async () => { + const onExit = vi.fn(); + const dispose = vi.fn(() => { + throw new Error("Runtime cleanup failed"); + }); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("host.dispose", dispose); + }); + const host = await client.startAhpHost({ localServer: {}, onExit }); + await expect(host.dispose()).rejects.toThrow("Runtime cleanup failed"); + expect(dispose).toHaveBeenCalledOnce(); + expect(onExit).not.toHaveBeenCalled(); + await expect(host.dispose()).rejects.toThrow("Runtime cleanup failed"); + expect(dispose).toHaveBeenCalledTimes(2); + }); + + it.each(["stop", "forceStop"] as const)( + "%s reports disconnection without trying host disposal", + async (method) => { + const dispose = vi.fn(() => ({})); + const onExit = vi.fn(); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + rpc.onRequest("host.dispose", dispose); + }); + await client.startAhpHost({ localServer: {}, onExit }); + await client[method](); + expect(onExit).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ reason: "ownerDisconnected" }) + ); + expect(client["hostExitCallbacks"].size).toBe(0); + expect(dispose).not.toHaveBeenCalled(); + } + ); + + it.each([false, true])( + "logs callback errors without breaking early-exit RPC handling (async: %s)", + async (asyncCallback) => { + const error = new Error("Consumer callback failed"); + const log = vi.spyOn(console, "error").mockImplementation(() => {}); + onTestFinished(() => log.mockRestore()); + const onExit = vi.fn(() => { + if (asyncCallback) return Promise.reject(error); + throw error; + }); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", async ({ hostId }: HostStartRequest) => { + await rpc.sendNotification("host.exited", { hostId, reason: "exited" }); + return info(hostId); + }); + }); + + const host = await client.startAhpHost({ localServer: {}, onExit }); + expect(onExit).toHaveBeenCalledOnce(); + expect(log).toHaveBeenCalledExactlyOnceWith("AHP host exit callback failed", { + hostId: host.hostId, + error, + }); + expect(client["hostExitCallbacks"].size).toBe(0); + await expect(client.ping()).resolves.toMatchObject({ message: "ok" }); + } + ); + + it("continues disconnect cleanup when a callback throws", async () => { + const error = new Error("Consumer callback failed"); + const log = vi.spyOn(console, "error").mockImplementation(() => {}); + onTestFinished(() => log.mockRestore()); + const client = await fixture((rpc) => { + rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); + }); + const first = await client.startAhpHost({ + localServer: {}, + onExit: () => { + throw error; + }, + }); + const secondExit = vi.fn(); + await client.startAhpHost({ localServer: {}, onExit: secondExit }); + await client.forceStop(); + expect(log).toHaveBeenCalledExactlyOnceWith("AHP host exit callback failed", { + hostId: first.hostId, + error, + }); + expect(secondExit).toHaveBeenCalledOnce(); + expect(client["hostExitCallbacks"].size).toBe(0); + }); + + it("reports owner disconnect without disposal RPCs and never reclaims on reconnect", async () => { + let disconnect: (() => void) | undefined; + const start = vi.fn(({ hostId }: HostStartRequest) => info(hostId)); + const dispose = vi.fn(() => ({})); + const onExit = vi.fn(); + const client = await fixture((rpc, socket) => { + rpc.onRequest("host.start", start); + rpc.onRequest("host.dispose", dispose); + disconnect = () => socket.destroy(); + }); + + const host = await client.startAhpHost({ localServer: {}, onExit }); + disconnect?.(); + await vi.waitFor(() => + expect(onExit).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + reason: "ownerDisconnected", + error: expect.stringContaining("cannot be acknowledged"), + }) + ) + ); + expect(client["hostExitCallbacks"].size).toBe(0); + await client.forceStop(); + await client.start(); + expect(start).toHaveBeenCalledOnce(); + await expect(host.dispose()).rejects.toThrow(); + expect(dispose).not.toHaveBeenCalled(); + expect(onExit).toHaveBeenCalledOnce(); + }); +}); diff --git a/nodejs/test/host.test.ts b/nodejs/test/host.test.ts new file mode 100644 index 0000000000..36549de70b --- /dev/null +++ b/nodejs/test/host.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, expectTypeOf, it, vi } from "vitest"; +import { AhpHost } from "../src/index.js"; +import type { + HostExitedNotification, + HostStartRequest, + HostStartResult, + HostLocalServerOptions, + HostGitHubEnvironmentOptions, +} from "../src/generated/rpc.js"; + +const info = { + hostId: "host-1", + url: "ws://127.0.0.1:12345", + token: "test-only-token", +}; + +describe("AhpHost", () => { + it("keeps generated listener options optional but nonnullable", () => { + expectTypeOf().toEqualTypeOf<{ + hostId: string; + localServer?: HostLocalServerOptions; + githubEnvironment?: HostGitHubEnvironmentOptions; + sessionFactory?: boolean; + resumeFactory?: boolean; + }>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + number | null | undefined + >(); + expectTypeOf().toEqualTypeOf(); + }); + + it("exposes connection information without starting a process or exposing closed", () => { + const dispose = vi.fn(); + const host = new AhpHost(info, dispose, vi.fn()); + + expect(host.hostId).toBe(info.hostId); + expect(host.url).toBe(info.url); + expect(host.token).toBe(info.token); + expect(host.pid).toBeUndefined(); + expect(host).not.toHaveProperty("closed"); + expect(dispose).not.toHaveBeenCalled(); + }); + + it("preserves an optional legacy separate host process ID", () => { + const host = new AhpHost({ ...info, pid: 1234 }, vi.fn(), vi.fn()); + 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()); + expect(host.token).toBeUndefined(); + }); + + it("forwards concurrent, repeated, and async disposal calls independently", async () => { + const dispose = vi.fn(async () => {}); + const host = new AhpHost(info, dispose, vi.fn()); + + await Promise.all([host.dispose(), host.dispose()]); + await host.dispose(); + await host[Symbol.asyncDispose](); + expect(dispose).toHaveBeenCalledTimes(4); + }); + + it("returns the disposal promise without handling or retrying failures", async () => { + const failure = Promise.reject(new Error("connection write failed")); + const dispose = vi.fn(() => failure); + const host = new AhpHost(info, dispose, vi.fn()); + + expect(host.dispose()).toBe(failure); + await expect(failure).rejects.toThrow("connection write failed"); + expect(dispose).toHaveBeenCalledOnce(); + }); + + it("forwards existing session publication without creating or copying a session", async () => { + const result = { sessionId: "resident", sessionUri: "copilot:/resident" }; + const publish = vi.fn(async () => result); + const host = new AhpHost(info, vi.fn(), publish); + await expect(host.publishSession("resident")).resolves.toBe(result); + expect(publish).toHaveBeenCalledExactlyOnceWith("resident"); + }); +}); diff --git a/nodejs/test/installation-confirmation.test.ts b/nodejs/test/installation-confirmation.test.ts new file mode 100644 index 0000000000..bafcb4b2e9 --- /dev/null +++ b/nodejs/test/installation-confirmation.test.ts @@ -0,0 +1,612 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +import { once } from "node:events"; +import { readFileSync } from "node:fs"; +import { createServer, type Socket } from "node:net"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { + CancellationTokenSource, + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, + type CancellationToken, + type MessageConnection, +} from "vscode-jsonrpc/node.js"; +import { + CopilotClient, + RuntimeConnection, + type ExtensionLaunchProvider, + type InstallationConfirmationHandler, + type InstallationConfirmationRequest, + type InstallationDecision, +} from "../src/index.js"; +import { createInstallationConfirmationAdapter } from "../src/installationConfirmation.js"; +import type { + CatalogClientContract, + CatalogNegotiatedContract, + InstallationReview, + McpInstallationManagementResult, + McpInstallationResult, + McpPrepareInstallRequest, +} from "../src/generated/rpc.js"; + +const capabilities: CatalogNegotiatedContract["grantedCapabilities"] = [ + "catalog-search-credential-required", + "catalog-search-session-bound", + "mcp-confirmed-remote-installation", +]; +const contract: CatalogClientContract = { + protocolVersion: 3, + requiredCapabilities: capabilities, +}; +const negotiated: CatalogNegotiatedContract = { + runtimeProtocolVersion: 3, + grantedCapabilities: capabilities, +}; +const prepareRequest: McpPrepareInstallRequest = { + contract, + planHandle: "bound-plan", + choiceId: "remote-choice", + policySessionId: "original-session", + inputs: [], + secrets: [], + source: { + kind: "url", + mediaType: "application/mcp-server-card+json", + url: "https://example.test/card", + }, + secretStorage: "keychain", +}; + +function request(operation: string): InstallationConfirmationRequest { + const value: InstallationConfirmationRequest = JSON.parse( + readFileSync( + new URL("../../rust/tests/fixtures/installation_confirmation.json", import.meta.url), + "utf8" + ) + ); + return { + ...value, + operationId: operation, + confirmationId: `challenge-${operation}`, + reviewFingerprint: `fingerprint-${operation}`, + }; +} + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((complete) => { + resolve = complete; + }); + return { promise, resolve }; +} + +async function connect( + handler?: InstallationConfirmationHandler, + extensionLaunchProvider?: ExtensionLaunchProvider +) { + const accepted = deferred<{ connection: MessageConnection; socket: Socket }>(); + const server = createServer((socket) => { + const connection = createMessageConnection( + new StreamMessageReader(socket), + new StreamMessageWriter(socket) + ); + connection.onRequest("connect", () => ({ + ok: true, + protocolVersion: 3, + version: "confirmation-transport-test", + })); + connection.onRequest("registerExtensionLaunchProvider", () => ({})); + connection.listen(); + accepted.resolve({ connection, socket }); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Missing test listener"); + const client = new CopilotClient({ + connection: RuntimeConnection.forUri(`127.0.0.1:${address.port}`), + installationConfirmationHandler: handler, + extensionLaunchProvider, + }); + onTestFinished(async () => { + await client.forceStop(); + const peer = await accepted.promise; + peer.connection.dispose(); + peer.socket.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + }); + await client.start(); + return { client, ...(await accepted.promise) }; +} + +describe("installation confirmation on the actual client transport", () => { + it("keeps MCP and Skill confirmation reviews strongly typed", () => { + const mcp = request("typed-mcp"); + if (mcp.review.resource !== "mcp") throw new Error("expected MCP review"); + expect(mcp.review.review.action).toBe("install"); + + const skillReview: InstallationReview = { + resource: "skill", + review: { + action: "install", + name: "demo", + description: "Demo Skill", + catalogue: { + resourceId: "123", + displayName: "Demo Skill", + description: "Demo Skill", + publisher: "Octo", + source: "https://agentfinder.github.com", + }, + source: { + resourceId: "123", + catalogRevisionId: "1", + repositoryId: "42", + repository: "octo/demo", + revision: "a".repeat(40), + root: "", + descriptorDigest: `sha256:${"1".repeat(64)}`, + bundleDigest: `sha256:${"2".repeat(64)}`, + }, + target: { + scope: "personal", + displayLabel: "~/.copilot/skills/demo", + relativePath: "skills/demo", + diagnosticsAbsolutePath: "/home/skills/demo", + }, + installsDisabled: true, + files: [ + { + path: "SKILL.md", + sizeBytes: 12, + mediaType: "text/markdown", + executable: false, + digest: `sha256:${"3".repeat(64)}`, + }, + ], + totalBytes: 12, + entrypointPath: "SKILL.md", + entrypointContent: "---\nname: demo\n---", + }, + }; + const skill: InstallationConfirmationRequest = { + ...mcp, + review: skillReview, + }; + if (skill.review.resource !== "skill") throw new Error("expected Skill review"); + expect(skill.review.review.name).toBe("demo"); + }); + + it("forwards all eight typed installation methods and preserves unavailable results", async () => { + const { client, connection } = await connect(); + const refusal = { + kind: "outcome", + negotiated, + outcome: { kind: "refused", reason: "lifecycle-unavailable" }, + } satisfies McpInstallationManagementResult & McpInstallationResult; + const apply = { + contract, + operationId: "prepared-operation", + policySessionId: "original-session", + }; + const removal = { + contract, + installationId: "owned-installation", + policySessionId: "original-session", + }; + const applyRemoval = { + contract, + planHandle: "removal-plan", + policySessionId: "original-session", + }; + const inventory = { contract, policySessionId: "original-session" }; + const control = { contract, operationId: "prepared-operation" }; + const calls = [ + { + method: "mcp.prepareInstall", + params: prepareRequest, + invoke: () => client.rpc.mcp.prepareInstall(prepareRequest), + }, + { + method: "mcp.applyInstall", + params: apply, + invoke: () => client.rpc.mcp.applyInstall(apply), + }, + { + method: "mcp.planUninstall", + params: removal, + invoke: () => client.rpc.mcp.planUninstall(removal), + }, + { + method: "mcp.applyUninstall", + params: applyRemoval, + invoke: () => client.rpc.mcp.applyUninstall(applyRemoval), + }, + { + method: "mcp.installations.list", + params: inventory, + invoke: () => client.rpc.mcp.installations.list(inventory), + }, + { + method: "mcp.installations.recover", + params: inventory, + invoke: () => client.rpc.mcp.installations.recover(inventory), + }, + { + method: "mcp.installations.status", + params: control, + invoke: () => client.rpc.mcp.installations.status(control), + }, + { + method: "mcp.installations.cancel", + params: control, + invoke: () => client.rpc.mcp.installations.cancel(control), + }, + ]; + for (const call of calls) { + const received = vi.fn((params: unknown) => { + expect(params).toEqual(call.params); + return refusal; + }); + connection.onRequest(call.method, received); + await expect(call.invoke()).resolves.toEqual(refusal); + expect(received).toHaveBeenCalledOnce(); + } + }); + + it("registers prepared IDs before apply and refuses late A without approving successor B", async () => { + const known = new Map(); + const handler = vi.fn((incoming) => { + if (known.get(incoming.operationId) !== incoming.policySessionId) { + throw new Error("Unknown original operation"); + } + return "decline"; + }); + const { client, connection } = await connect(handler); + connection.onRequest( + "mcp.prepareInstall", + (params: McpPrepareInstallRequest) => + ({ + kind: "outcome", + negotiated, + outcome: { + kind: "install-prepared", + operation: { + operationId: `operation-${params.planHandle}`, + expiresAtEpochMs: 42, + }, + }, + }) satisfies McpInstallationManagementResult + ); + const prepare = async (planHandle: string) => { + const result = await client.rpc.mcp.prepareInstall({ ...prepareRequest, planHandle }); + if (result.kind !== "outcome" || result.outcome.kind !== "install-prepared") { + throw new Error("Expected a prepared operation"); + } + expect(result.outcome.operation.expiresAtEpochMs).toBe(42); + const id = result.outcome.operation.operationId; + known.set(id, prepareRequest.policySessionId); + return id; + }; + const a = await prepare("a"); + expect(handler).not.toHaveBeenCalled(); + connection.onRequest("mcp.installations.cancel", (params: { operationId: string }) => { + expect(params).toEqual({ contract, operationId: a }); + return { + kind: "outcome", + negotiated, + outcome: { + kind: "operation", + operation: { + phase: "completed", + operationId: a, + cancellationRequested: true, + outcome: { kind: "cancelled", operationId: a }, + }, + }, + } satisfies McpInstallationManagementResult; + }); + await client.rpc.mcp.installations.cancel({ contract, operationId: a }); + known.delete(a); + const b = await prepare("b"); + expect(b).not.toBe(a); + expect(handler).not.toHaveBeenCalled(); + await expect(connection.sendRequest("installations.confirm", request(a))).rejects.toThrow( + "Unknown original operation" + ); + connection.onRequest("mcp.applyInstall", async (params: { operationId: string }) => { + expect(params).toEqual({ + contract, + operationId: b, + policySessionId: "original-session", + }); + const response = await connection.sendRequest("installations.confirm", request(b)); + expect(response).toEqual({ + confirmationId: `challenge-${b}`, + reviewFingerprint: `fingerprint-${b}`, + decision: "decline", + }); + return { + kind: "outcome", + negotiated, + outcome: { kind: "declined", operationId: b }, + } satisfies McpInstallationResult; + }); + await expect( + client.rpc.mcp.applyInstall({ + contract, + operationId: b, + policySessionId: "original-session", + }) + ).resolves.toMatchObject({ outcome: { kind: "declined", operationId: b } }); + expect(handler).toHaveBeenCalledTimes(2); + }); + + it("retains prepared OAuth identity and original session through login and cancellation", async () => { + const { client, connection } = await connect(); + connection.onRequest("session.create", () => ({ sessionId: "original-session" })); + const session = await client.createSession({ + sessionId: "original-session", + onPermissionRequest: () => ({ kind: "approved" }), + }); + const identity = { serverName: "example", expectedInstallationId: "owned-installation" }; + const loginId = "runtime-issued-login"; + connection.onRequest("session.mcp.oauth.prepareLogin", (params: unknown) => { + expect(params).toEqual({ + sessionId: "original-session", + ...identity, + forceReauth: true, + }); + return { loginId, expiresAt: "2026-09-24T15:00:00Z" }; + }); + const prepared = await session.rpc.mcp.oauth.prepareLogin({ + ...identity, + forceReauth: true, + }); + expect(prepared.loginId).toBe(loginId); + const started = deferred(); + const completed = deferred<{ loginId: string; status: "cancelled" }>(); + connection.onRequest("session.mcp.oauth.login", (params: unknown) => { + expect(params).toEqual({ + sessionId: "original-session", + ...identity, + loginId: prepared.loginId, + }); + started.resolve(); + return completed.promise; + }); + const login = session.rpc.mcp.oauth.login({ ...identity, loginId: prepared.loginId }); + await started.promise; + connection.onRequest("session.mcp.oauth.cancelLogin", (params: unknown) => { + expect(params).toEqual({ + sessionId: "original-session", + expectedInstallationId: identity.expectedInstallationId, + loginId: prepared.loginId, + }); + completed.resolve({ loginId, status: "cancelled" }); + return { cancelled: true }; + }); + await expect( + session.rpc.mcp.oauth.cancelLogin({ + expectedInstallationId: identity.expectedInstallationId, + loginId: prepared.loginId, + }) + ).resolves.toEqual({ cancelled: true }); + await expect(login).resolves.toEqual({ loginId, status: "cancelled" }); + }); + + it("presents typed full review and echoes the original challenge and fingerprint", async () => { + const handler = vi.fn( + async (incoming, token): Promise => { + expect(token.isCancellationRequested).toBe(false); + expect(incoming).toEqual(request("a")); + expect(incoming.review.review.action).toBe("install"); + if (incoming.review.review.action !== "install") + throw new Error("Unexpected review"); + expect(incoming.review.review.effectiveConfiguration).toEqual({ + transport: "streamable-http", + url: "https://example.test/mcp", + headers: { "X-Region": "eu" }, + tools: ["*"], + }); + incoming.confirmationId = "mutated-by-handler"; + incoming.reviewFingerprint = "mutated-by-handler"; + return "confirm"; + } + ); + const { connection } = await connect(handler); + + await expect( + connection.sendRequest("installations.confirm", request("a")) + ).resolves.toEqual({ + confirmationId: "challenge-a", + reviewFingerprint: "fingerprint-a", + decision: "confirm", + }); + expect(handler).toHaveBeenCalledOnce(); + }); + + it("keeps concurrent same-session reviews independent and allows out-of-order decisions", async () => { + const a = deferred(); + const b = deferred(); + const handler = vi.fn((incoming) => + incoming.operationId === "a" ? a.promise : b.promise + ); + const { connection } = await connect(handler); + const first = connection.sendRequest("installations.confirm", request("a")); + const second = connection.sendRequest("installations.confirm", request("b")); + await vi.waitFor(() => expect(handler).toHaveBeenCalledTimes(2)); + await expect( + connection.sendRequest("gitHubToken.getToken", { + registrationId: "unknown", + host: "github.com", + reason: "initial", + }) + ).rejects.toThrow("No GitHub token provider"); + + b.resolve("decline"); + await expect(second).resolves.toEqual({ + confirmationId: "challenge-b", + reviewFingerprint: "fingerprint-b", + decision: "decline", + }); + a.resolve("confirm"); + await expect(first).resolves.toMatchObject({ confirmationId: "challenge-a" }); + }); + + it("reaches and cancels a confirmation queued behind a blocked global callback", async () => { + const release = deferred(); + const resolveEntered = deferred(); + const extensionLaunchProvider: ExtensionLaunchProvider = { + resolve: async () => { + resolveEntered.resolve(); + await release.promise; + return {}; + }, + }; + let token: CancellationToken | undefined; + const pending = deferred(); + const { connection } = await connect((_request, incoming) => { + token = incoming; + return pending.promise; + }, extensionLaunchProvider); + const cancellation = new CancellationTokenSource(); + onTestFinished(() => cancellation.dispose()); + + const blocked = connection.sendRequest("extensionLaunchProvider.resolve", { + id: "project:blocked", + modulePath: "/extensions/blocked/index.js", + name: "Blocked", + source: "project", + }); + await resolveEntered.promise; + const confirmation = connection.sendRequest( + "installations.confirm", + request("behind-blocked-callback"), + cancellation.token + ); + const cancelled = expect(confirmation).rejects.toMatchObject({ code: -32800 }); + await vi.waitFor(() => expect(token).toBeDefined()); + cancellation.cancel(); + await cancelled; + expect(token?.isCancellationRequested).toBe(true); + + release.resolve(); + await expect(blocked).resolves.toEqual({}); + pending.resolve("confirm"); + }); + + it("retires cancelled A before a late decision and does not rebind it to successor B", async () => { + const cancellation = new CancellationTokenSource(); + onTestFinished(() => cancellation.dispose()); + const a = deferred(); + const tokens = new Map(); + const handler: InstallationConfirmationHandler = (incoming, token) => { + tokens.set(incoming.operationId, token); + return incoming.operationId === "a" ? a.promise : "confirm"; + }; + const { connection } = await connect(handler); + const first = connection.sendRequest( + "installations.confirm", + request("a"), + cancellation.token + ); + const cancelled = expect(first).rejects.toMatchObject({ code: -32800 }); + await vi.waitFor(() => expect(tokens.has("a")).toBe(true)); + cancellation.cancel(); + await cancelled; + expect(tokens.get("a")?.isCancellationRequested).toBe(true); + a.resolve("confirm"); + await expect( + connection.sendRequest("installations.confirm", request("b")) + ).resolves.toMatchObject({ + confirmationId: "challenge-b", + reviewFingerprint: "fingerprint-b", + }); + expect(tokens.get("b")?.isCancellationRequested).toBe(false); + }); + + it("rejects when cancellation and a decision are ready together", async () => { + const cancellation = new CancellationTokenSource(); + onTestFinished(() => cancellation.dispose()); + const decision = deferred(); + let token: CancellationToken | undefined; + const disposable = { dispose() {} }; + const adapter = createInstallationConfirmationAdapter( + { + onClose: () => disposable, + onDispose: () => disposable, + } as unknown as MessageConnection, + (_request, incoming) => { + token = incoming; + return decision.promise; + } + ); + const confirmation = adapter.confirm(request("simultaneous"), cancellation.token); + const cancelled = expect(confirmation).rejects.toMatchObject({ code: -32800 }); + await vi.waitFor(() => expect(token).toBeDefined()); + decision.resolve("confirm"); + cancellation.cancel(); + await cancelled; + expect(token?.isCancellationRequested).toBe(true); + }); + + it("connection closure cancels all outstanding reviews on the original connection", async () => { + const pending = deferred(); + const tokens = new Map(); + const first = await connect((incoming, token) => { + tokens.set(incoming.operationId, token); + return pending.promise; + }); + const second = await connect(() => "decline"); + const firstResponse = first.connection.sendRequest("installations.confirm", request("a")); + const secondResponse = first.connection.sendRequest("installations.confirm", request("b")); + const firstClosed = expect(firstResponse).rejects.toThrow(); + const secondClosed = expect(secondResponse).rejects.toThrow(); + await vi.waitFor(() => expect(tokens.size).toBe(2)); + first.socket.destroy(); + await vi.waitFor(() => { + expect(tokens.get("a")?.isCancellationRequested).toBe(true); + expect(tokens.get("b")?.isCancellationRequested).toBe(true); + }); + first.connection.dispose(); + await firstClosed; + await secondClosed; + pending.resolve("confirm"); + await expect( + second.connection.sendRequest("installations.confirm", request("c")) + ).resolves.toMatchObject({ + confirmationId: "challenge-c", + decision: "decline", + }); + }); + + it("refuses missing handlers and propagates explicit handler refusal", async () => { + const missing = await connect(); + await expect( + missing.connection.sendRequest("installations.confirm", request("a")) + ).rejects.toThrow("No installations client-global handler registered"); + const refusing = await connect(() => { + throw new Error("Unknown original operation or incomplete review"); + }); + await expect( + refusing.connection.sendRequest("installations.confirm", request("a")) + ).rejects.toThrow("Unknown original operation or incomplete review"); + }); + + it("does not infer optional legacy session authority", async () => { + const incoming = request("legacy"); + delete incoming.policySessionId; + const handler = vi.fn((params) => { + expect(params.policySessionId).toBeUndefined(); + return "decline"; + }); + const { connection } = await connect(handler); + await expect( + connection.sendRequest("installations.confirm", incoming) + ).resolves.toMatchObject({ + decision: "decline", + }); + }); +}); 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/legacy-parameters-fixture.ts b/nodejs/test/legacy-parameters-fixture.ts new file mode 100644 index 0000000000..577c241073 --- /dev/null +++ b/nodejs/test/legacy-parameters-fixture.ts @@ -0,0 +1,37 @@ +import type { JSONSchema7 } from "json-schema"; + +import type { ApiSchema, RpcMethod } from "../../scripts/codegen/utils.ts"; + +/** + * A server request that opts into `x-legacy-parameters`, published first with + * `contract`, `source` and `scope`, then extended by `additions` optional properties + * (`policySessionId`, then `traceId`) without changing the annotation. + */ +export function legacyRequestSchema(additions: 0 | 1 | 2, scope: "server" | "session" = "server") { + const added: Record = {}; + if (additions >= 1) added.policySessionId = { type: "string" }; + if (additions >= 2) added.traceId = { type: "string" }; + const params: JSONSchema7 & Record = { + title: "SamplePlanRequest", + type: "object", + properties: { + ...(scope === "session" ? { sessionId: { type: "string" } } : {}), + contract: { type: "string" }, + source: { type: "string" }, + scope: { type: "string" }, + ...added, + }, + required: [...(scope === "session" ? ["sessionId"] : []), "contract", "source"], + additionalProperties: false, + }; + if (additions > 0) params["x-legacy-parameters"] = ["contract", "source", "scope"]; + const methods: Record = { + plan: { + rpcMethod: `${scope === "session" ? "session." : ""}sample.plan`, + params, + result: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] }, + }, + }; + const schema: ApiSchema = { [scope]: { sample: methods } }; + return { schema, params }; +} diff --git a/nodejs/test/legacy-parameters.test.ts b/nodejs/test/legacy-parameters.test.ts new file mode 100644 index 0000000000..0c8d68673c --- /dev/null +++ b/nodejs/test/legacy-parameters.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from "vitest"; + +import { + isOmittableRequest, + readLegacyParameters, + validateLegacyDefinitions, + validateLegacyRequests, +} from "../../scripts/codegen/legacy-parameters.ts"; +import { legacyRequestSchema } from "./legacy-parameters-fixture.ts"; + +describe("x-legacy-parameters", () => { + it("leaves unmarked requests to their existing generation", () => { + expect(readLegacyParameters(legacyRequestSchema(0).params, "sample.plan")).toBeUndefined(); + }); + + it("keeps the annotation fixed across two successive optional additions", () => { + const one = readLegacyParameters(legacyRequestSchema(1).params, "sample.plan"); + const two = readLegacyParameters(legacyRequestSchema(2).params, "sample.plan"); + expect(one?.legacy).toEqual(["contract", "source", "scope"]); + expect(two?.legacy).toEqual(one?.legacy); + expect(one?.additions).toEqual(["policySessionId"]); + expect(two?.additions).toEqual(["policySessionId", "traceId"]); + expect([...(two?.required ?? [])]).toEqual(["contract", "source"]); + }); + + it("excludes implicit session properties from both sets", () => { + const parsed = readLegacyParameters( + legacyRequestSchema(1, "session").params, + "session.sample.plan", + { + implicit: ["sessionId"], + } + ); + expect(parsed?.legacy).toEqual(["contract", "source", "scope"]); + expect(parsed?.additions).toEqual(["policySessionId"]); + expect(parsed?.required.has("sessionId")).toBe(false); + }); + + it.each([ + ["a non-array annotation", "contract", {}, "expected an array"], + ["duplicate names", ["contract", "contract", "source"], {}, "duplicate"], + ["an unknown property", ["contract", "source", "missing"], {}, "unknown property missing"], + ["an omitted required input", ["contract", "scope"], {}, "required property source"], + [ + "no added property", + ["contract", "source", "scope", "policySessionId"], + {}, + "at least one property", + ], + ["a nullable request", ["contract", "source", "scope"], { nullable: true }, "nullable"], + ] as const)("rejects %s", (_name, legacy, options, message) => { + const { params } = legacyRequestSchema(1); + params["x-legacy-parameters"] = legacy; + expect(() => readLegacyParameters(params, "sample.plan", options)).toThrow(message); + }); + + it("rejects an implicit session property listed as a legacy parameter", () => { + const { params } = legacyRequestSchema(1, "session"); + params["x-legacy-parameters"] = ["sessionId", "contract", "source"]; + expect(() => + readLegacyParameters(params, "session.sample.plan", { implicit: ["sessionId"] }) + ).toThrow("implicit property sessionId"); + }); + + it("validates every section for generators with no extra projection", () => { + type Methods = Record; + const validate = (sections: Record) => + validateLegacyRequests( + sections, + (node) => Object.values(node), + (method) => method.params, + () => false + ); + const session = legacyRequestSchema(1, "session").params; + expect(() => + validate({ session: { plan: { rpcMethod: "session.sample.plan", params: session } } }) + ).not.toThrow(); + + const client = legacyRequestSchema(1).params; + expect(() => + validate({ clientSession: { plan: { rpcMethod: "sample.plan", params: client } } }) + ).toThrow( + "Invalid x-legacy-parameters for sample.plan: only server and session requests are supported" + ); + + const invalid = legacyRequestSchema(1).params; + invalid["x-legacy-parameters"] = ["contract", "scope"]; + expect(() => + validate({ server: { plan: { rpcMethod: "sample.plan", params: invalid } } }) + ).toThrow("required property source must be a legacy parameter"); + }); + + it("rejects optional requests before nullable ones", () => { + const { params } = legacyRequestSchema(1); + expect(() => + readLegacyParameters(params, "sample.plan", { optional: true, nullable: true }) + ).toThrow( + "Invalid x-legacy-parameters for sample.plan: optional requests cannot declare legacy parameters" + ); + expect(isOmittableRequest({ anyOf: [{ not: {} }, params] })).toBe(true); + expect(isOmittableRequest(params)).toBe(false); + }); + + it("requires schema property order when a positional record constructor is kept", () => { + const record = { + type: "object", + properties: { name: {}, owned: {}, status: {}, error: {} }, + required: ["name", "status"], + "x-legacy-parameters": ["name", "status", "error"], + }; + expect(readLegacyParameters(record, "Server", { ordered: true })?.additions).toEqual([ + "owned", + ]); + const misordered = { ...record, "x-legacy-parameters": ["status", "name", "error"] }; + expect(readLegacyParameters(misordered, "Server")?.additions).toEqual(["owned"]); + expect(() => readLegacyParameters(misordered, "Server", { ordered: true })).toThrow( + "Invalid x-legacy-parameters for Server: legacy parameters must follow schema property order" + ); + }); + + it("validates response definitions that no request path reads", () => { + const record = { + type: "object", + properties: { name: {}, status: {}, owned: {} }, + required: ["name", "status"], + "x-legacy-parameters": ["name", "status"], + }; + expect(() => validateLegacyDefinitions({ definitions: { Server: record } })).not.toThrow(); + expect(() => + validateLegacyDefinitions({ + $defs: { Server: { ...record, "x-legacy-parameters": ["name"] } }, + }) + ).toThrow( + "Invalid x-legacy-parameters for Server: required property status must be a legacy parameter" + ); + }); +}); diff --git a/nodejs/test/legacy-request-compatibility.test.ts b/nodejs/test/legacy-request-compatibility.test.ts new file mode 100644 index 0000000000..167536fe52 --- /dev/null +++ b/nodejs/test/legacy-request-compatibility.test.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { MessageConnection } from "vscode-jsonrpc/node.js"; +import { describe, expect, it, vi } from "vitest"; +import { + createServerRpc, + type CatalogClientContract, + type CatalogSearchRequest, + type McpPlanInstallRequest, + type McpPlanInstallSource, +} from "../src/generated/rpc.js"; + +// Requests that declare x-legacy-parameters keep one object argument in TypeScript: +// later inputs are optional properties, so existing calls are unchanged. +const contract: CatalogClientContract = { + protocolVersion: 3, + requiredCapabilities: ["mcp-install-planning"], +}; +const source: McpPlanInstallSource = { + kind: "candidate", + candidateHandle: "candidate", + searchId: "search", +}; + +function recordingRpc() { + const sendRequest = vi.fn(async () => ({ kind: "unavailable", message: "offline" })); + const rpc = createServerRpc({ sendRequest } as unknown as MessageConnection); + return { rpc, sendRequest }; +} + +describe("x-legacy-parameters in TypeScript", () => { + it("keeps existing object calls unchanged", async () => { + const { rpc, sendRequest } = recordingRpc(); + const plan: McpPlanInstallRequest = { contract, source, scope: "user" }; + const search: CatalogSearchRequest = { contract, query: "catalogue query", limit: 4 }; + + await rpc.mcp.planInstall(plan); + await rpc.catalog.search(search); + + expect(sendRequest.mock.calls).toEqual([ + ["mcp.planInstall", { contract, source, scope: "user" }], + ["catalog.search", { contract, query: "catalogue query", limit: 4 }], + ]); + }); + + it("sends added optional inputs through the same method", async () => { + const { rpc, sendRequest } = recordingRpc(); + + await rpc.mcp.planInstall({ contract, source, policySessionId: "session" }); + await rpc.catalog.search({ + contract, + query: "catalogue query", + policySessionId: "session", + }); + + expect(sendRequest.mock.calls).toEqual([ + ["mcp.planInstall", { contract, source, policySessionId: "session" }], + ["catalog.search", { contract, query: "catalogue query", policySessionId: "session" }], + ]); + }); +}); 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 8bb8efb9b2..4b330528fd 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/python-codegen.test.ts @@ -1,6 +1,100 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts"; +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), + "utf8" + ); + + it("preserves the existing Workflow checkpoint result API", () => { + const result = "SessionWorkflowPauseAtCheckpointResult"; + expect(code).toContain(`class ${result}:`); + expect(code).toContain(`-> ${result}:`); + expect(code).toContain(`return ${result}.from_dict(`); + expect(code).toContain(`"${result}",`); + }); + + it("does not restore the retired Factory checkpoint aliases", () => { + expect(code).not.toContain("SessionFactoryPauseAtCheckpointResult"); + expect(code).not.toContain('"session.factory.pauseAtCheckpoint"'); + }); + + it("keeps named connection-scoped host callback results", () => { + expect(code).toContain( + "async def materialize_session(self, params: HostSessionCreateCallback) -> HostSessionCreateResult:" + ); + expect(code).toContain( + "async def register_session(self, params: HostRegisterSessionRequest) -> HostPublishSessionResult:" + ); + expect(code).toContain("class HostEmptyResult:"); + expect(code).toContain( + "async def shutdown(self, params: HostEmptyResult) -> HostEmptyResult:" + ); + expect(code).toContain( + 'return HostEmptyResult.from_dict(await self._client.request("host.dispose"' + ); + expect(code).not.toContain("dict.from_dict"); + }); +}); + +it("deserializes open empty-object results as dictionaries", () => { + const lines: string[] = []; + emitMethod( + lines, + "dispose", + { + rpcMethod: "host.dispose", + params: { + type: "object", + properties: { hostId: { type: "string" } }, + required: ["hostId"], + }, + result: { type: "object", properties: {} }, + }, + false, + (name) => (name === "HostDisposeResult" ? "dict" : name) + ); + const code = lines.join("\n"); + expect(code).toContain('return dict(await self._client.request("host.dispose"'); + expect(code).not.toContain("dict.from_dict"); +}); describe("Python root event payload unions", () => { it.each(["anyOf", "oneOf"] as const)("preserves referenced %s payload variants", (keyword) => { @@ -46,3 +140,183 @@ describe("Python root event payload unions", () => { expect(code).toContain("return SampleEmptyData()"); }); }); + +function pythonRequestSnippet(additions: 1 | 2): string { + const traceField = additions >= 2 ? " trace_id: str | None = None\n\n" : ""; + const traceLoad = + additions >= 2 + ? ' trace_id = from_union([from_str, from_none], obj.get("traceId"))\n' + : ""; + const traceArg = additions >= 2 ? ", trace_id" : ""; + return [ + "@dataclass", + "class SamplePlanRequest:", + " contract: str", + "", + " source: str", + "", + " policy_session_id: str | None = None", + "", + " scope: str | None = None", + "", + traceField + " @staticmethod", + " def from_dict(obj: Any) -> 'SamplePlanRequest':", + " assert isinstance(obj, dict)", + ' contract = from_str(obj.get("contract"))', + ' source = from_str(obj.get("source"))', + ' policy_session_id = from_union([from_str, from_none], obj.get("policySessionId"))', + ' scope = from_union([from_str, from_none], obj.get("scope"))', + traceLoad + + ` return SamplePlanRequest(contract, source, policy_session_id, scope${traceArg})`, + "", + ].join("\n"); +} + +describe("Python x-legacy-parameters", () => { + it.each([1, 2] as const)( + "keeps legacy fields positional and makes %i added field(s) keyword-only", + (additions) => { + const { params } = legacyRequestSchema(additions); + const code = applyPythonLegacyParameters(pythonRequestSnippet(additions), { + SamplePlanRequest: params, + }); + expect(code).toContain(" contract: str\n"); + expect(code).toContain(" source: str\n"); + expect(code).toContain(" scope: str | None = None\n"); + expect(code).toContain( + " policy_session_id: str | None = field(default=None, kw_only=True)" + ); + if (additions === 2) { + expect(code).toContain( + " trace_id: str | None = field(default=None, kw_only=True)" + ); + expect(code).toContain( + "return SamplePlanRequest(contract, source, scope, policy_session_id=policy_session_id, trace_id=trace_id)" + ); + } else { + expect(code).toContain( + "return SamplePlanRequest(contract, source, scope, policy_session_id=policy_session_id)" + ); + } + } + ); + + it("leaves unmarked dataclasses unchanged", () => { + const snippet = pythonRequestSnippet(1); + expect( + applyPythonLegacyParameters(snippet, { + SamplePlanRequest: legacyRequestSchema(0).params, + }) + ).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.each(["anyOf", "oneOf"] as const)( + "includes marked fields in inline %s variants once", + (keyword) => { + expect( + pythonAppendLastFieldsPresentIn({ + Response: { + [keyword]: ["first", "second"].map((kind) => ({ + type: "object", + properties: { + kind: { const: kind }, + addedField: { + type: "string", + "x-copilot-sdk-append-last": true, + }, + }, + })), + }, + }) + ).toEqual([["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 6887a49633..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"; @@ -162,7 +162,9 @@ describe("materializeRuntimeBundle", () => { writeFileSync(wrapper, "wrapper"); writeFileSync(runtimeNode, "runtime"); mkdirSync(join(sourceDir, "ripgrep", "bin", platform), { recursive: true }); - writeFileSync(join(sourceDir, "ripgrep", "bin", platform, "rg"), "ripgrep"); + writeFileSync(join(sourceDir, "ripgrep", "bin", platform, "rg"), "ripgrep", { + mode: 0o755, + }); mkdirSync(join(sourceDir, "definitions"), { recursive: true }); writeFileSync(join(sourceDir, "definitions", "future.json"), "{}"); mkdirSync(join(sourceDir, "copilot-sdk"), { recursive: true }); @@ -170,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"); @@ -204,12 +207,16 @@ 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); expect(existsSync(join(installDir, "README.md"))).toBe(false); if (process.platform !== "win32") { expect(statSync(installedWrapper).mode & 0o111).not.toBe(0); + expect( + statSync(join(installDir, "ripgrep", "bin", platform, "rg")).mode & 0o111 + ).not.toBe(0); } }); @@ -409,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 1cf7f07987..0a169c0825 100644 --- a/nodejs/test/rust-codegen.test.ts +++ b/nodejs/test/rust-codegen.test.ts @@ -1,26 +1,391 @@ import type { ApiSchema } from "../../scripts/codegen/utils.ts"; +import { + normalizeSchemaBrandCasing, + postProcessSchema, + propagateInternalVisibility, + stripBooleanLiterals, +} from "../../scripts/codegen/utils.ts"; import type { JSONSchema7 } from "json-schema"; +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { generateApiTypesCode, + generateRpcCode, generateSessionEventsCode, - isRustCodegenEntrypoint, } from "../../scripts/codegen/rust.ts"; +import { legacyRequestSchema } from "./legacy-parameters-fixture.ts"; + +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)]"); + }); + + it.each(["anyOf", "oneOf"] as const)( + "keeps required phase results typed through %s references", + (keyword) => { + const code = generateApiTypesCode({ + definitions: { + Operation: { + [keyword]: ["prepared", "cancelled"].map((phase) => ({ + type: "object", + required: ["phase", "operationId"], + properties: { + phase: { type: "string", const: phase }, + operationId: { type: "string" }, + }, + })), + }, + Result: { + type: "object", + required: ["operation"], + properties: { operation: { $ref: "#/definitions/Operation" } }, + }, + }, + } as ApiSchema); + + expect(code).toContain("pub operation: Operation,"); + expect(code).toContain(`#[serde(untagged)] +pub enum Operation { + Prepared(OperationPrepared), + Cancelled(OperationCancelled), +}`); + expect(code).toContain("pub phase: OperationPreparedPhase,"); + expect(code).toContain("pub phase: OperationCancelledPhase,"); + expect(code).not.toContain("#[serde(other)]"); + expect(code).not.toContain("pub operation: serde_json::Value,"); + } + ); + + it.each(["anyOf", "oneOf"] as const)( + "keeps null in a referenced multi-variant %s discriminated union", + (keyword) => { + const code = generateApiTypesCode({ + definitions: { + Outcome: { + title: "Outcome", + [keyword]: [ + ...["ready", "pending"].map((kind) => ({ + type: "object", + required: ["kind"], + properties: { kind: { type: "string", const: kind } }, + })), + { type: "null" }, + ], + }, + Container: { + type: "object", + required: ["outcome"], + properties: { outcome: { $ref: "#/definitions/Outcome" } }, + }, + }, + } as ApiSchema); -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" + expect(code).toContain("pub outcome: Option,"); + expect(code).toContain("pub enum Outcome {"); + } + ); + + it("preserves the published MCP transport choice schema as a typed union", () => { + // Exact selected definitions from CLI 1.0.89-1 api.schema.json: + // sha256 a445b552b6ecef536b89f3d08cc73b6fbbe8fe0e503daae8974578529d62bc83. + const schema = JSON.parse( + readFileSync( + new URL("./fixtures/mcp-plan-transport-choice.schema.json", import.meta.url), + "utf8" ) - ).toBe(true); + ) as ApiSchema; + schema.definitions!.Plan = { + type: "object", + required: ["transportChoices"], + properties: { + transportChoices: { + type: "array", + items: { $ref: "#/definitions/McpPlanTransportChoice" }, + }, + }, + }; + const code = generateApiTypesCode(schema); + + expect(code).toContain(`#[serde(untagged)] +pub enum McpPlanTransportChoice { + Package(McpPlanTransportChoicePackage), + Remote(McpPlanTransportChoiceRemote), +}`); + expect(code).toContain("pub transport_choices: Vec,"); + expect(code).toContain("pub required_values: Vec,"); + expect(code).toContain("pub secret_placeholders: Vec,"); + expect(code).toContain( + 'deserialize_with = "McpPlanTransportChoicePackage::deserialize_install_method"' + ); + expect(code).toContain( + 'deserialize_with = "McpPlanTransportChoiceRemote::deserialize_install_method"' + ); + expect(code).toContain('if value != "package"'); + expect(code).toContain('if value != "remote"'); + expect(code).not.toContain("Vec"); + }); + + it.each(["anyOf", "oneOf"] as const)( + "supports arbitrary required enum-reference discriminators in %s unions", + (keyword) => { + const code = generateApiTypesCode({ + definitions: { + Mode: { type: "string", enum: ["first", "second"] }, + Choice: { + title: "Choice", + [keyword]: ["first", "second"].map((value) => ({ + type: "object", + required: ["mode", "value"], + properties: { + mode: { $ref: "#/definitions/Mode", const: value }, + value: { type: "string" }, + }, + })), + }, + Container: { + type: "object", + required: ["choice"], + properties: { choice: { $ref: "#/definitions/Choice" } }, + }, + }, + } as ApiSchema); + + expect(code).toContain(`#[serde(untagged)] +pub enum Choice { + First(ChoiceFirst), + Second(ChoiceSecond), +}`); + expect(code).toContain(`#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Container`); + expect(code).toContain("pub choice: Choice,"); + } + ); + + it.each(["optional", "duplicate", "missing", "number", "inline", "outside-enum"] as const)( + "retains the JSON fallback for %s discriminators without a supported union", + (invalid) => { + const variants: JSONSchema7[] = ["first", "second"].map((value) => ({ + type: "object", + required: invalid === "optional" ? ["value"] : ["mode", "value"], + properties: { + mode: + invalid === "missing" + ? { type: "string" } + : invalid === "number" + ? { type: "integer", const: value === "first" ? 1 : 2 } + : invalid === "inline" + ? { type: "string", const: value } + : { + $ref: "#/definitions/Mode", + const: invalid === "duplicate" ? "same" : value, + }, + value: { type: "string" }, + }, + })); + const code = generateApiTypesCode({ + definitions: { + Mode: { + type: "string", + enum: invalid === "outside-enum" ? ["different"] : ["first", "second"], + }, + Container: { + type: "object", + required: ["choice"], + properties: { choice: { anyOf: variants } }, + }, + }, + } as ApiSchema); + + expect(code).toContain("pub choice: serde_json::Value,"); + expect(code).not.toContain("pub enum ContainerChoice"); + } + ); + + it.each(["anyOf", "oneOf"] as const)( + "preserves raw optional metadata when newly recognising referenced %s discriminators", + (keyword) => { + const code = generateApiTypesCode({ + definitions: { + Status: { type: "string", enum: ["known", "unavailable"] }, + Snapshot: { + title: "Snapshot", + [keyword]: ["known", "unavailable"].map((status) => ({ + type: "object", + required: ["status"], + properties: { + status: { $ref: "#/definitions/Status", const: status }, + }, + })), + }, + Container: { + type: "object", + required: ["requiredSnapshot"], + properties: { + requiredSnapshot: { $ref: "#/definitions/Snapshot" }, + optionalSnapshot: { $ref: "#/definitions/Snapshot" }, + }, + }, + }, + } as ApiSchema); + + expect(code).toContain("pub required_snapshot: Snapshot,"); + expect(code).toContain("pub optional_snapshot: Option,"); + expect(code).not.toContain("pub optional_snapshot: Option,"); + } + ); + + it("emits the current runtime's boolean-discriminated command queue result", () => { + const code = generateApiTypesCode({ + definitions: { + EnqueueCommandResult: { + title: "EnqueueCommandResult", + anyOf: [ + { $ref: "#/definitions/AcceptedEnqueueCommandResult" }, + { $ref: "#/definitions/UnsupportedEnqueueCommandResult" }, + ], + }, + AcceptedEnqueueCommandResult: { + type: "object", + required: ["queued", "queueId"], + properties: { + queued: { type: "boolean", const: true }, + queueId: { type: "string" }, + }, + }, + UnsupportedEnqueueCommandResult: { + type: "object", + required: ["queued"], + properties: { queued: { type: "boolean", const: false } }, + }, + }, + } as ApiSchema); + + expect(code).toContain("pub enum EnqueueCommandResult {"); + expect(code).toContain("True(AcceptedEnqueueCommandResult)"); + 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" + ); }); -}); -describe("Rust API type codegen", () => { it.each([true, false])( "validates a reference's sibling constant without changing its enum (required: %s)", (required) => { @@ -308,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) => { @@ -352,3 +760,297 @@ pub enum ${name} { } ); }); + +describe("Rust x-legacy-parameters", () => { + const render = (additions: 0 | 1 | 2, scope: "server" | "session" = "server") => { + const { schema } = legacyRequestSchema(additions, scope); + const types = generateApiTypesCode(schema); + const rpc = generateRpcCode(schema); + const block = (code: string, pattern: RegExp) => code.match(pattern)?.[0]; + return { + types, + rpc, + request: block(types, /pub struct SamplePlanRequest \{[\s\S]*?\n\}/), + constructor: block(types, /pub fn new\([^)]*\) -> Self/), + plan: block(rpc, /pub async fn plan\([^)]*\)[^{]*/), + options: block(rpc, /pub async fn plan_with_options\([^)]*\)[^{]*/), + }; + }; + + it("leaves unmarked requests unchanged", () => { + const original = render(0); + expect(original.types).not.toContain("SamplePlanOptions"); + expect(original.rpc).not.toContain("plan_with_options"); + }); + + it("freezes the published struct and method and adds private-field options", () => { + const original = render(0); + const once = render(1); + expect(once.request).toBe(original.request); + expect(once.plan).toBe(original.plan); + expect(once.types).toContain( + "pub struct SamplePlanOptions {\n #[serde(flatten)]\n legacy: SamplePlanRequest," + ); + expect(once.types).toMatch( + /#\[serde\(skip_serializing_if = "Option::is_none"\)\]\n policy_session_id: Option,/ + ); + expect(once.constructor).toBe( + "pub fn new(contract: impl Into, source: impl Into) -> Self" + ); + expect(once.types).toContain( + "legacy: SamplePlanRequest { contract: contract.into(), source: source.into(), scope: None }," + ); + expect(once.types).toContain( + "pub fn scope(mut self, value: impl Into) -> Self {\n self.legacy.scope = Some(value.into());" + ); + expect(once.types).toContain( + "pub fn policy_session_id(mut self, value: impl Into) -> Self {\n self.policy_session_id = Some(value.into());" + ); + expect(once.options).toContain("params: SamplePlanOptions"); + const wireCalls = once.rpc.match(/rpc_methods::SAMPLE_PLAN/g) ?? []; + expect(wireCalls).toHaveLength(2); + expect(once.types).not.toContain("non_exhaustive"); + }); + + it("keeps both entry points unchanged across a second optional addition", () => { + const once = render(1); + const twice = render(2); + expect(twice.request).toBe(once.request); + expect(twice.constructor).toBe(once.constructor); + expect(twice.plan).toBe(once.plan); + expect(twice.options).toBe(once.options); + expect(twice.types).toContain( + "pub fn trace_id(mut self, value: impl Into) -> Self {" + ); + expect(twice.types).toContain(" trace_id: None,"); + }); + + it("keeps the session id injected by session-scoped wrappers", () => { + const once = render(1, "session"); + expect(once.request).toBe(render(0, "session").request); + expect(once.request).not.toContain("session_id"); + const body = once.rpc.slice(once.rpc.indexOf("pub async fn plan_with_options")); + expect(body).toMatch( + /wire_params\["sessionId"\] = serde_json::Value::String\(self\.session\.id\(\)\.to_string\(\)\);/ + ); + }); + + it("derives Default for options without required inputs", () => { + const { schema, params } = legacyRequestSchema(1); + params.required = []; + const types = generateApiTypesCode(schema); + expect(types).toContain("pub fn new() -> Self"); + expect(types).toContain( + "impl Default for SamplePlanOptions {\n fn default() -> Self {\n Self::new()" + ); + expect(render(1).types).not.toContain("impl Default for SamplePlanOptions"); + }); + + it("rejects optional requests", () => { + const { schema, params } = legacyRequestSchema(1); + const method = (schema.server as Record>).sample + .plan; + method.params = { anyOf: [{ not: {} }, params] }; + expect(() => generateApiTypesCode(schema)).toThrow( + "Invalid x-legacy-parameters for sample.plan: optional requests cannot declare legacy parameters" + ); + }); + + it("keeps an annotated response record as a Default struct with every field", () => { + const record = (required: string[]) => + ({ + definitions: { + SampleServer: { + type: "object", + properties: { + name: { type: "string" }, + owned: { type: "string" }, + status: { type: "string" }, + }, + required, + "x-legacy-parameters": ["name", "status"], + }, + }, + }) as ApiSchema; + const types = generateApiTypesCode(record(["name", "status"])); + expect(types).toMatch( + /#\[derive\(Debug, Clone, Default, Serialize, Deserialize\)\]\n#\[serde\(rename_all = "camelCase"\)\]\npub struct SampleServer \{/ + ); + expect(types).toContain("pub owned: Option,"); + expect(types).not.toContain("SampleServerOptions"); + }); + + it("rejects an annotated response record that cannot derive Default", () => { + const schema = { + definitions: { + Inner: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, + SampleServer: { + type: "object", + properties: { + name: { type: "string" }, + inner: { $ref: "#/definitions/Inner" }, + owned: { type: "string" }, + }, + required: ["name", "inner"], + "x-legacy-parameters": ["name", "inner"], + }, + }, + } as ApiSchema; + expect(() => generateApiTypesCode(schema, ["Inner"])).toThrow( + "Invalid x-legacy-parameters for SampleServer: a response record must be a struct that derives Default" + ); + }); + + it("rejects metadata that omits a required input", () => { + const { schema, params } = legacyRequestSchema(1); + (params as Record)["x-legacy-parameters"] = ["contract", "scope"]; + expect(() => generateApiTypesCode(schema)).toThrow( + "Invalid x-legacy-parameters for sample.plan: required property source must be a legacy parameter" + ); + }); + + it("gives each titled const discriminator its own Rust type", () => { + const code = generateApiTypesCode({ + definitions: { + Review: { + anyOf: ["install", "uninstall"].map((action) => ({ + type: "object", + required: ["action"], + properties: { + action: { type: "string", const: action, title: "ReviewAction" }, + }, + })), + }, + Holder: { + type: "object", + required: ["review"], + properties: { review: { $ref: "#/definitions/Review" } }, + }, + }, + } as ApiSchema); + + expect(code).toMatch( + /pub enum Review\w*InstallAction \{\n(?:.*\n)*?\s+#\[serde\(rename = "install"\)\]/ + ); + expect(code).toMatch( + /pub enum Review\w*UninstallAction \{\n(?:.*\n)*?\s+#\[serde\(rename = "uninstall"\)\]/ + ); + expect(code).not.toContain("pub enum ReviewAction"); + }); + + it("refuses to reuse a string enum name for a different value set", () => { + expect(() => + generateApiTypesCode({ + definitions: { + Kind: { type: "string", enum: ["a", "b"] }, + Owner: { + type: "object", + required: ["kind", "mode"], + properties: { + kind: { $ref: "#/definitions/Kind" }, + mode: { type: "string", enum: ["c"], title: "Kind" }, + }, + }, + }, + } as ApiSchema) + ).toThrow(/Rust string enum Kind is requested for different values/); + }); + + it("keeps every const discriminator of the committed API schema distinct in Rust", () => { + // Mirror the generator's own schema preparation so emission order matches. + const schema = propagateInternalVisibility( + postProcessSchema( + stripBooleanLiterals( + normalizeSchemaBrandCasing( + JSON.parse( + readFileSync( + new URL("../../../../generated/api.schema.json", import.meta.url), + "utf8" + ) + ) as ApiSchema + ) + ) as JSONSchema7 + ) + ) as unknown as ApiSchema; + // Generation throws if two const literals would share one enum name. + const code = generateApiTypesCode(schema); + + const enumValues = new Map(); + for (const match of code.matchAll(/^pub enum (\w+) \{\n([\s\S]*?)^\}/gm)) { + enumValues.set( + match[1], + [...match[2].matchAll(/rename = "([^"]+)"/g)].map((m) => m[1]) + ); + } + const unions: Array<{ owner: string; variants: unknown[] }> = []; + const visit = (node: unknown, owner: string): void => { + if (typeof node !== "object" || node === null) return; + const record = node as Record; + for (const key of ["anyOf", "oneOf"] as const) { + if (Array.isArray(record[key])) + unions.push({ owner, variants: record[key] as unknown[] }); + } + for (const [key, value] of Object.entries(record)) visit(value, `${owner}/${key}`); + }; + visit(schema, "api.schema.json"); + + let checked = 0; + for (const { owner, variants } of unions) { + const discriminators = variants.map((variant) => { + const properties = (variant as JSONSchema7).properties ?? {}; + return Object.entries(properties).filter( + ([name, prop]) => + typeof prop === "object" && + typeof prop.const === "string" && + (variant as JSONSchema7).required?.includes(name) && + !prop.$ref + ); + }); + for (const [name] of discriminators[0] ?? []) { + const values = discriminators.map( + (entries) => entries.find(([key]) => key === name)?.[1].const + ); + if ( + values.some((value) => typeof value !== "string") || + new Set(values).size !== values.length + ) + continue; + const titles = new Set( + discriminators.map( + (entries) => + (entries.find(([key]) => key === name)?.[1] as JSONSchema7).title + ) + ); + if (titles.size !== 1 || [...titles][0] === undefined) continue; + // Shared-title literals must each map to an enum accepting exactly that value. + for (const value of values as string[]) { + const matching = [...enumValues].filter( + ([, accepted]) => accepted.length === 1 && accepted[0] === value + ); + expect(matching.length, `${owner}.${name} = ${value}`).toBeGreaterThan(0); + } + checked += 1; + } + } + expect(checked).toBeGreaterThan(0); + }); + + it("reuses a released value enum for a const whose name matches it, as released output does", () => { + const code = generateApiTypesCode({ + definitions: { + RefType: { type: "string", enum: ["issue", "pr"] }, + Ref: { + type: "object", + required: ["referenceType", "type"], + properties: { + referenceType: { $ref: "#/definitions/RefType" }, + type: { type: "string", const: "reference" }, + }, + }, + }, + } as ApiSchema); + expect(code).toContain("pub reference_type: RefType,"); + expect(code).toContain("pub r#type: RefType,"); + expect(code.match(/pub enum RefType \{/g)).toHaveLength(1); + }); +}); diff --git a/nodejs/test/sandbox-config.test.ts b/nodejs/test/sandbox-config.test.ts index 0870f9c2e3..7494807251 100644 --- a/nodejs/test/sandbox-config.test.ts +++ b/nodejs/test/sandbox-config.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import type { SandboxConfig } from "../src/generated/rpc.js"; +import type { SandboxConfigSource } from "../src/index.js"; describe("SandboxConfig", () => { it("round-trips allowBypass and omits it when absent", () => { @@ -14,3 +15,10 @@ describe("SandboxConfig", () => { expect(JSON.parse(JSON.stringify(omitted))).toEqual({ enabled: true }); }); }); + +describe("SandboxConfigSource", () => { + it("is importable from the package root", () => { + const source: SandboxConfigSource = "user_disabled"; + expect(source).toBe("user_disabled"); + }); +}); 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 cc5cb12bf1..de03f98142 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -16,8 +16,8 @@ import { describe, expect, it } from "vitest"; import { approveAll } from "../src/index.js"; -import { FACTORY_AGENT_OPTION_KEYS } from "../src/factory.js"; -import type { FactoryAgentOptions as WireFactoryAgentOptions } from "../src/generated/rpc.js"; +import { WORKFLOW_AGENT_OPTION_KEYS } from "../src/workflow.js"; +import type { WorkflowAgentOptions as WireWorkflowAgentOptions } from "../src/generated/rpc.js"; import type { // The aggregate union; must still resolve via the package root. SessionEvent, @@ -44,6 +44,8 @@ import type { ResumeData, StartData, ToolExecutionCompleteData, + ToolExecutionCompleteFileEdit, + ToolExecutionCompleteFileEditKind, ToolExecutionPartialData, ToolExecutionProgressData, ToolExecutionStartData, @@ -65,10 +67,10 @@ import type { UserMessageAgentMode, Attachment, WorkingDirectoryContextHostType, - FactoryContext, - FactoryDefinition, - FactoryAgentOptions, - FactoryRunResult, + WorkflowContext, + WorkflowDefinition, + WorkflowAgentOptions, + WorkflowRunResult, JsonValue, } from "../src/index.js"; @@ -100,32 +102,32 @@ type _AssistantMessageEventStaysAlignedWithSessionEventUnion = _AssertEqual< Extract >; const _assistantMessageEventAlignmentCheck: _AssistantMessageEventStaysAlignedWithSessionEventUnion = true; -type _DefaultFactoryArgsAreJsonValue = _AssertEqual; -const _defaultFactoryArgsCheck: _DefaultFactoryArgsAreJsonValue = true; -type _DefaultFactoryResultIsJsonValueOrVoid = _AssertEqual< - Awaited>, +type _DefaultWorkflowArgsAreJsonValue = _AssertEqual; +const _defaultWorkflowArgsCheck: _DefaultWorkflowArgsAreJsonValue = true; +type _DefaultWorkflowResultIsJsonValueOrVoid = _AssertEqual< + Awaited>, JsonValue | void >; -const _defaultFactoryResultCheck: _DefaultFactoryResultIsJsonValueOrVoid = true; -type _FactoryRunResultIsJsonValueOrUndefined = _AssertEqual< - FactoryRunResult["result"], +const _defaultWorkflowResultCheck: _DefaultWorkflowResultIsJsonValueOrVoid = true; +type _WorkflowRunResultIsJsonValueOrUndefined = _AssertEqual< + WorkflowRunResult["result"], JsonValue | undefined >; -const _factoryRunResultCheck: _FactoryRunResultIsJsonValueOrUndefined = true; -type _FactoryAgentOptionKeysMatchPublicInterface = _AssertEqual< - (typeof FACTORY_AGENT_OPTION_KEYS)[number], - keyof FactoryAgentOptions +const _workflowRunResultCheck: _WorkflowRunResultIsJsonValueOrUndefined = true; +type _WorkflowAgentOptionKeysMatchPublicInterface = _AssertEqual< + (typeof WORKFLOW_AGENT_OPTION_KEYS)[number], + keyof WorkflowAgentOptions >; -const _factoryAgentOptionKeysCheck: _FactoryAgentOptionKeysMatchPublicInterface = true; -type _PublicFactoryAgentOptionsMatchWire = _AssertEqual< - keyof FactoryAgentOptions, - keyof WireFactoryAgentOptions +const _workflowAgentOptionKeysCheck: _WorkflowAgentOptionKeysMatchPublicInterface = true; +type _PublicWorkflowAgentOptionsMatchWire = _AssertEqual< + keyof WorkflowAgentOptions, + keyof WireWorkflowAgentOptions >; -const _publicFactoryAgentOptionsCheck: _PublicFactoryAgentOptionsMatchWire = true; -// @ts-expect-error Factory arguments must be representable on the JSON wire. -type _FactoryArgsRejectUndefined = FactoryContext; -// @ts-expect-error Factory results must be JSON values or top-level void. -type _FactoryResultRejectsFunction = FactoryDefinition void>; +const _publicWorkflowAgentOptionsCheck: _PublicWorkflowAgentOptionsMatchWire = true; +// @ts-expect-error Workflow arguments must be representable on the JSON wire. +type _WorkflowArgsRejectUndefined = WorkflowContext; +// @ts-expect-error Workflow results must be JSON values or top-level void. +type _WorkflowResultRejectsFunction = WorkflowDefinition void>; type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual< PermissionRequestedEvent, Extract @@ -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/skill-provider.test.ts b/nodejs/test/skill-provider.test.ts new file mode 100644 index 0000000000..3037e3b621 --- /dev/null +++ b/nodejs/test/skill-provider.test.ts @@ -0,0 +1,559 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { PassThrough } from "node:stream"; +import { + CancellationTokenSource, + createMessageConnection, + type MessageConnection, + ResponseError, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node.js"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { + approveAll, + CopilotClient, + RuntimeConnection, + type CopilotClientOptions, + type SkillProvider, + type SkillProviderCallOptions, + type SkillProviderDescriptor, +} from "../src/index.js"; + +interface RecordedRequest { + method: string; + params: any; +} + +type RuntimeRequestHandler = ( + method: string, + params: any, + runtime: MessageConnection +) => Promise | unknown; + +interface Harness { + client: CopilotClient; + runtime: MessageConnection; + requests: RecordedRequest[]; +} + +/** + * Starts a client over an in-memory JSON-RPC pipe. The `runtime` end records + * every request and can call the client's reverse-RPC handlers. + */ +async function startClient( + options: Partial = {}, + onRuntimeRequest?: RuntimeRequestHandler +): Promise { + const clientToRuntime = new PassThrough(); + const runtimeToClient = new PassThrough(); + const clientConnection = createMessageConnection( + new StreamMessageReader(runtimeToClient), + new StreamMessageWriter(clientToRuntime) + ); + const runtime = createMessageConnection( + new StreamMessageReader(clientToRuntime), + new StreamMessageWriter(runtimeToClient) + ); + const requests: RecordedRequest[] = []; + runtime.onRequest(async (method: string, params: any) => { + requests.push({ method, params }); + const custom = await onRuntimeRequest?.(method, params, runtime); + if (custom !== undefined) { + return custom; + } + if (method === "session.create" || method === "session.resume") { + return { sessionId: params.sessionId }; + } + if (method === "session.detach") { + return { success: true }; + } + return {}; + }); + runtime.listen(); + + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + ...options, + }); + vi.spyOn(client as any, "connectToServer").mockImplementation(async () => { + (client as any).connection = clientConnection; + (client as any).attachConnectionHandlers(); + clientConnection.listen(); + }); + vi.spyOn(client as any, "verifyProtocolVersion").mockResolvedValue(undefined); + onTestFinished(async () => { + await client.forceStop(); + clientConnection.dispose(); + runtime.dispose(); + }); + + await client.start(); + return { client, runtime, requests }; +} + +function payload(requests: RecordedRequest[], method: string): any { + const request = requests.find((r) => r.method === method); + expect(request, `${method} was not sent`).toBeDefined(); + return request!.params; +} + +async function rejection(promise: Promise): Promise> { + const error = await promise.then( + () => undefined, + (e: unknown) => e + ); + expect(error).toBeInstanceOf(ResponseError); + return error as ResponseError; +} + +const reviewSkill: SkillProviderDescriptor = { + name: "review", + description: "Reviews code", +}; + +function recordingProvider( + skills: SkillProviderDescriptor[] | null = [reviewSkill], + markdown: Record = { review: "Review carefully." } +): SkillProvider & { calls: string[] } { + const calls: string[] = []; + return { + calls, + listSkills: () => { + calls.push("list"); + return skills as SkillProviderDescriptor[]; + }, + readSkill: (name) => { + calls.push(`read:${name}`); + return markdown[name] ?? null; + }, + }; +} + +describe("skill providers", () => { + describe("session payloads", () => { + it("leaves create and resume payloads unchanged without a provider", async () => { + const { client, requests } = await startClient(); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll }); + + expect(payload(requests, "session.create")).not.toHaveProperty("hasSkillProvider"); + expect(payload(requests, "session.resume")).not.toHaveProperty("hasSkillProvider"); + }); + + it("flags create and resume payloads when a provider is supplied", async () => { + const { client, requests } = await startClient(); + const provider = recordingProvider(); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: provider, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + skillProvider: provider, + }); + + const create = payload(requests, "session.create"); + expect(create.hasSkillProvider).toBe(true); + expect(create).not.toHaveProperty("skillProvider"); + expect(create.enableSkills).toBeUndefined(); + const resume = payload(requests, "session.resume"); + expect(resume.hasSkillProvider).toBe(true); + expect(resume).not.toHaveProperty("skillProvider"); + expect(provider.calls).toEqual([]); + }); + + it("keeps empty mode's enableSkills default when a provider is supplied", async () => { + const { client, requests } = await startClient({ + mode: "empty", + baseDirectory: "/tmp/copilot-test", + }); + + await client.createSession({ + onPermissionRequest: approveAll, + availableTools: [], + skillProvider: recordingProvider(), + }); + + const create = payload(requests, "session.create"); + expect(create.enableSkills).toBe(false); + expect(create.hasSkillProvider).toBe(true); + }); + + it.each(["session.create", "session.resume"] as const)( + "serves callbacks the runtime makes while handling %s", + async (openMethod) => { + let earlyList: unknown; + const { client } = await startClient({}, async (method, params, runtime) => { + if (method === openMethod) { + earlyList = await runtime.sendRequest("skillProvider.list", { + sessionId: params.sessionId, + }); + return { sessionId: params.sessionId }; + } + return undefined; + }); + const provider = recordingProvider(); + const config = { onPermissionRequest: approveAll, skillProvider: provider }; + + if (openMethod === "session.create") { + await client.createSession(config); + } else { + await client.resumeSession("existing-session", config); + } + + expect(earlyList).toEqual({ skills: [reviewSkill] }); + } + ); + }); + + describe("cloud sessions", () => { + it("rejects a provider before starting the client or sending anything", async () => { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + }); + const start = vi.spyOn(client, "start"); + const provider = recordingProvider(); + + await expect( + client.createSession({ + onPermissionRequest: approveAll, + cloud: {}, + skillProvider: provider, + }) + ).rejects.toThrow("Skill providers are not supported for cloud sessions."); + + expect(start).not.toHaveBeenCalled(); + expect(provider.calls).toEqual([]); + }); + }); + + describe("dispatch", () => { + it.each([ + ["synchronous", (value: any) => value], + ["asynchronous", (value: any) => Promise.resolve(value)], + ])("serves list and read from a %s provider", async (_kind, wrap) => { + const { client, runtime } = await startClient(); + const descriptors: SkillProviderDescriptor[] = [ + reviewSkill, + { + name: "deploy", + description: "Deploys the service", + userInvocable: false, + disableModelInvocation: true, + argumentHint: "[environment]", + }, + ]; + const reads: string[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: { + listSkills: () => wrap(descriptors), + readSkill: (name) => { + reads.push(name); + return wrap(`# ${name}`); + }, + }, + }); + + await expect( + runtime.sendRequest("skillProvider.list", { sessionId: session.sessionId }) + ).resolves.toEqual({ skills: descriptors }); + await expect( + runtime.sendRequest("skillProvider.read", { + sessionId: session.sessionId, + name: "deploy", + }) + ).resolves.toEqual({ markdown: "# deploy" }); + expect(reads).toEqual(["deploy"]); + }); + + it("omits unset optional descriptor fields", async () => { + const { client, runtime } = await startClient(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: recordingProvider([ + { name: "review", description: "Reviews code", argumentHint: undefined }, + ]), + }); + + const result: any = await runtime.sendRequest("skillProvider.list", { + sessionId: session.sessionId, + }); + + expect(Object.keys(result.skills[0])).toEqual(["name", "description"]); + }); + + it("answers a null catalog with an empty list", async () => { + const { client, runtime } = await startClient(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: recordingProvider(null), + }); + + await expect( + runtime.sendRequest("skillProvider.list", { sessionId: session.sessionId }) + ).resolves.toEqual({ skills: [] }); + }); + + it.each([null, undefined])("answers a %s read with null markdown", async (missing) => { + const { client, runtime } = await startClient(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: { listSkills: () => [reviewSkill], readSkill: () => missing }, + }); + + await expect( + runtime.sendRequest("skillProvider.read", { + sessionId: session.sessionId, + name: "review", + }) + ).resolves.toEqual({ markdown: null }); + }); + + it.each(["listSkills", "readSkill"] as const)( + "reports a %s failure generically without forwarding its message", + async (failing) => { + const { client, runtime } = await startClient(); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + onTestFinished(() => consoleError.mockRestore()); + const secret = "db-password-in-error"; + const providerError = new Error(secret); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: { + listSkills: () => { + if (failing === "listSkills") throw providerError; + return [reviewSkill]; + }, + readSkill: async () => { + throw providerError; + }, + }, + }); + + const error = await rejection( + failing === "listSkills" + ? runtime.sendRequest("skillProvider.list", { + sessionId: session.sessionId, + }) + : runtime.sendRequest("skillProvider.read", { + sessionId: session.sessionId, + name: "review", + }) + ); + + expect(error.code).toBe(-32603); + expect(error.data).toBeUndefined(); + expect(error.message).not.toContain(secret); + expect(consoleError).toHaveBeenCalledWith(`Skill provider ${failing} failed`, { + sessionId: session.sessionId, + error: providerError, + }); + } + ); + + it.each(["listSkills", "readSkill"] as const)( + "aborts a %s call when the runtime cancels it", + async (operation) => { + const { client, runtime } = await startClient(); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + onTestFinished(() => consoleError.mockRestore()); + let entered!: () => void; + const started = new Promise((resolve) => (entered = resolve)); + const block = ({ signal }: SkillProviderCallOptions) => + new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => reject(signal.reason), { + once: true, + }); + entered(); + }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: { + listSkills: (options) => block(options), + readSkill: (_name, options) => block(options), + }, + }); + const cancellation = new CancellationTokenSource(); + + const pending = rejection( + operation === "listSkills" + ? runtime.sendRequest( + "skillProvider.list", + { sessionId: session.sessionId }, + cancellation.token + ) + : runtime.sendRequest( + "skillProvider.read", + { sessionId: session.sessionId, name: "review" }, + cancellation.token + ) + ); + await started; + cancellation.cancel(); + + expect((await pending).code).toBe(-32800); + expect(consoleError).not.toHaveBeenCalled(); + } + ); + + it("rejects a request for an unknown session", async () => { + const { runtime } = await startClient(); + + await rejection(runtime.sendRequest("skillProvider.list", { sessionId: "missing" })); + }); + + it("rejects a request for a session without a provider", async () => { + const { client, runtime } = await startClient(); + const session = await client.createSession({ onPermissionRequest: approveAll }); + + await rejection( + runtime.sendRequest("skillProvider.read", { + sessionId: session.sessionId, + name: "review", + }) + ); + }); + }); + + describe("teardown", () => { + it("stops calling the provider after the session disconnects", async () => { + const { client, runtime } = await startClient(); + const provider = recordingProvider(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: provider, + }); + const sessionId = session.sessionId; + + await session.disconnect(); + await rejection(runtime.sendRequest("skillProvider.list", { sessionId })); + + expect(provider.calls).toEqual([]); + }); + + it("keeps a session that failed to open from serving its provider", async () => { + const provider = recordingProvider(); + let sessionId: string | undefined; + const { client, runtime } = await startClient({}, (method, params) => { + if (method === "session.create") { + sessionId = params.sessionId; + throw new Error("create failed"); + } + return undefined; + }); + + await expect( + client.createSession({ onPermissionRequest: approveAll, skillProvider: provider }) + ).rejects.toThrow("create failed"); + await rejection(runtime.sendRequest("skillProvider.list", { sessionId: sessionId! })); + + expect(provider.calls).toEqual([]); + }); + + it("keeps the resident provider when a resume fails validation", async () => { + const { client, runtime, requests } = await startClient({ + mode: "empty", + baseDirectory: "/tmp/copilot-test", + }); + const resident = recordingProvider(); + const replacement = recordingProvider(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + availableTools: [], + skillProvider: resident, + }); + + await expect( + client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + skillProvider: replacement, + }) + ).rejects.toThrow("availableTools"); + await runtime.sendRequest("skillProvider.list", { sessionId: session.sessionId }); + + expect(requests.map((r) => r.method)).not.toContain("session.resume"); + expect(resident.calls).toEqual(["list"]); + expect(replacement.calls).toEqual([]); + }); + + it("restores the resident provider when the runtime rejects a resume", async () => { + const { client, runtime } = await startClient({}, (method) => { + if (method === "session.resume") { + throw new Error("resume failed"); + } + return undefined; + }); + const resident = recordingProvider(); + const replacement = recordingProvider(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: resident, + }); + + await expect( + client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + skillProvider: replacement, + }) + ).rejects.toThrow("resume failed"); + await runtime.sendRequest("skillProvider.list", { sessionId: session.sessionId }); + + expect(resident.calls).toEqual(["list"]); + expect(replacement.calls).toEqual([]); + }); + + it.each(["create", "resume"] as const)( + "does not register a provider when session-FS setup fails on %s", + async (open) => { + const { client, runtime, requests } = await startClient({ + sessionFs: { + initialCwd: "/workspace", + sessionStatePath: "/state", + conventions: "posix", + }, + }); + const provider = recordingProvider(); + const config = { + onPermissionRequest: approveAll, + sessionId: "fs-failure", + skillProvider: provider, + }; + + await expect( + open === "create" + ? client.createSession(config) + : client.resumeSession("fs-failure", config) + ).rejects.toThrow("createSessionFsProvider is required"); + await rejection( + runtime.sendRequest("skillProvider.list", { sessionId: "fs-failure" }) + ); + + expect(requests.map((r) => r.method)).not.toContain(`session.${open}`); + expect(provider.calls).toEqual([]); + } + ); + + it("serves the resumed provider instead of the original one", async () => { + const { client, runtime } = await startClient(); + const original = recordingProvider(); + const replacement = recordingProvider(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider: original, + }); + await session.disconnect(); + + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + skillProvider: replacement, + }); + await runtime.sendRequest("skillProvider.list", { sessionId: session.sessionId }); + + expect(original.calls).toEqual([]); + expect(replacement.calls).toEqual(["list"]); + }); + }); +}); diff --git a/nodejs/test/stdio-shutdown.test.ts b/nodejs/test/stdio-shutdown.test.ts new file mode 100644 index 0000000000..d17575f700 --- /dev/null +++ b/nodejs/test/stdio-shutdown.test.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { CopilotClient, RuntimeConnection } from "../src/index.js"; + +const fixture = fileURLToPath( + new URL("../../test/harness/stdio-shutdown-runtime.cjs", import.meta.url) +); + +describe("owned stdio shutdown", () => { + it.each([ + "stop", + "dispose", + "force", + "fallback", + "start-failure", + "force-during-stop", + ] as const)( + "%s preserves the owned-process cleanup contract", + async (mode) => { + const directory = mkdtempSync(join(tmpdir(), "copilot-node-shutdown-")); + const marker = join(directory, "telemetry.jsonl"); + const pidFile = join(directory, "runtime.pid"); + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ + path: process.execPath, + args: [ + fixture, + marker, + mode === "force-during-stop" ? "fallback" : mode, + pidFile, + ], + }), + useLoggedInUser: false, + }); + onTestFinished(async () => { + await client.forceStop(); + rmSync(directory, { + recursive: true, + force: true, + maxRetries: 10, + retryDelay: 100, + }); + }); + + if (mode === "start-failure") { + await expect(client.start()).rejects.toThrow(/protocol version/i); + } else { + await client.start(); + const started = performance.now(); + if (mode === "force") { + await client.forceStop(); + expect(performance.now() - started).toBeLessThan(10_000); + } else if (mode === "force-during-stop") { + const stopping = client.stop(); + await expect.poll(() => existsSync(marker), { timeout: 5000 }).toBe(true); + await client.forceStop(); + expect(await stopping).toEqual([]); + expect(performance.now() - started).toBeLessThan(10_000); + } else if (mode === "dispose") { + await client[Symbol.asyncDispose](); + } else { + expect(await client.stop()).toEqual([]); + } + if (mode === "fallback") { + expect(performance.now() - started).toBeGreaterThanOrEqual(10_000); + } + } + + const pid = Number(readFileSync(pidFile, "utf8")); + const hasExited = () => { + try { + process.kill(pid, 0); + return false; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; + return true; + } + }; + if (mode === "force" || mode === "start-failure") { + // Force-stop sends the kill signal without waiting to reap the child. + await expect.poll(hasExited, { timeout: 5000 }).toBe(true); + expect(existsSync(marker)).toBe(false); + } else { + expect(hasExited()).toBe(true); + expect(readFileSync(marker, "utf8")).toBe('{"type":"span"}\n'); + } + expect(await client.stop()).toEqual([]); + await client[Symbol.asyncDispose](); + }, + 40_000 + ); +}); 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 8177f6d826..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({ @@ -93,6 +164,32 @@ describe("typescript schema codegen", () => { expect(code).toContain("[k: string]: JsonValue;"); }); + it("keeps a titled discriminator literal inline", async () => { + const variant = (action: string) => ({ + type: "object" as const, + properties: { + action: { type: "string" as const, const: action, title: "ReviewAction" }, + }, + required: ["action"], + }); + const code = await compile( + normalizeSchemaForTypeScript({ + title: "Review", + anyOf: [variant("install"), variant("uninstall")], + }), + "Review", + { + bannerComment: "", + style: { semi: true, singleQuote: false }, + additionalProperties: false, + } + ); + + expect(code).toContain('action: "install";'); + expect(code).toContain('action: "uninstall";'); + expect(code).not.toContain("ReviewAction"); + }); + it("maps a bare opaque JSON array item to JsonValue", 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..a612193d24 --- /dev/null +++ b/nodejs/test/unified-release.test.ts @@ -0,0 +1,1142 @@ +/*--------------------------------------------------------------------------------------------- + * 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( + 7 + ); + expect(args).toEqual( + expect.arrayContaining([ + expect.stringContaining( + "-Dcopilot.native.external.linuxmusl.arm64.classifier.path=" + ), + ]) + ); + 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(["latest", "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" + : channel === "latest" + ? "0.4.0" + : 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[1] === "repos/github/copilot-sdk/releases/latest") { + return JSON.stringify({ + tag_name: "v0.3.0", + draft: false, + prerelease: false, + published_at: "2026-09-20T00:00:00Z", + }); + } + if (args.includes("--paginate")) { + return [ + JSON.stringify({ + tag_name: "runtime-1.0.89-unstable.1", + draft: false, + prerelease: true, + published_at: "2026-09-22T00:00:00Z", + }), + JSON.stringify({ + tag_name: "v0.3.0", + draft: false, + prerelease: false, + published_at: "2026-09-20T00:00:00Z", + }), + ].join("\n"); + } + 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(-2)).toEqual([ + "api", + ...(channel === "latest" + ? ["repos/github/copilot-sdk/releases/latest"] + : [ + "--paginate", + "--jq", + ".[] | {tag_name, draft, prerelease, published_at}", + "repos/github/copilot-sdk/releases?per_page=100", + ]), + ]); + expect(requests.at(-1)).toContain( + channel === "latest" ? "--latest" : "--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/test/webSocketResponsesServer.test.ts b/nodejs/test/webSocketResponsesServer.test.ts new file mode 100644 index 0000000000..2778f156ab --- /dev/null +++ b/nodejs/test/webSocketResponsesServer.test.ts @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import http from "node:http"; +import { describe, expect, it } from "vitest"; +import { WebSocket } from "ws"; +import { startWebSocketResponsesServer } from "./e2e/harness/webSocketResponsesServer.js"; +import { withTestCleanup } from "./helpers/withTestCleanup.js"; + +async function listen(server: http.Server) { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Expected a bound TCP address for the test upstream."); + } + return `http://127.0.0.1:${address.port}`; +} + +async function close(server: http.Server) { + if (!server.listening) return; + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); +} + +describe("WebSocket provider HTTP forwarding", () => { + it.each([false, true])( + "closes the upstream with response headers sent: %s", + async (sendHeaders) => { + const accepted = Promise.withResolvers(); + const receivedBody = Promise.withResolvers(); + const openSockets = new Set(); + const configured = http.createServer((req, res) => { + openSockets.add(req.socket); + req.socket.once("close", () => openSockets.delete(req.socket)); + accepted.resolve(); + if (sendHeaders) res.write("held upstream body"); + }); + let provider: Awaited> | undefined; + let request: http.ClientRequest | undefined; + await withTestCleanup( + async () => { + const configuredUrl = await listen(configured); + provider = await startWebSocketResponsesServer(); + provider.forwardHttpTo(configuredUrl); + request = http.get(`${provider.baseUrl}/held`, (response) => { + response.on("error", () => response.destroy()); + response.once("data", () => receivedBody.resolve()); + response.resume(); + }); + request.on("error", () => request?.destroy()); + await accepted.promise; + if (sendHeaders) await receivedBody.promise; + const stopping = provider.stop(); + provider = undefined; + await stopping; + await expect.poll(() => openSockets.size).toBe(0); + }, + () => { + request?.destroy(); + }, + () => provider?.stop(), + () => close(configured) + ); + } + ); + + it("reports an upstream response reset and closes the downstream", async () => { + const receivedBody = Promise.withResolvers(); + const downstreamClosed = Promise.withResolvers(); + let upstreamSocket: http.IncomingMessage["socket"] | undefined; + const configured = http.createServer((req, res) => { + upstreamSocket = req.socket; + res.write("partial body"); + }); + let provider: Awaited> | undefined; + let request: http.ClientRequest | undefined; + await withTestCleanup( + async () => { + const configuredUrl = await listen(configured); + provider = await startWebSocketResponsesServer(); + provider.forwardHttpTo(configuredUrl); + request = http.get(`${provider.baseUrl}/reset`, (response) => { + response.on("error", () => response.destroy()); + response.once("data", () => receivedBody.resolve()); + response.once("close", () => downstreamClosed.resolve()); + response.resume(); + }); + request.on("error", () => request?.destroy()); + await receivedBody.promise; + upstreamSocket?.destroy(); + await downstreamClosed.promise; + const stopping = provider.stop(); + provider = undefined; + await expect(stopping).rejects.toMatchObject({ + message: "WebSocket provider fixture failed.", + errors: [expect.objectContaining({ code: "ECONNRESET" })], + }); + }, + () => { + request?.destroy(); + }, + () => provider?.stop(), + () => close(configured) + ); + }); + + it("keeps the destination fixed while forwarding request paths, methods, and bodies", async () => { + const configured = http.createServer((req, res) => { + req.setEncoding("utf8"); + let body = ""; + req.on("data", (chunk: string) => (body += chunk)); + req.on("end", () => { + res.writeHead(200, { + "content-type": "application/json; charset=utf-8", + "x-content-type-options": "nosniff", + }); + res.end( + JSON.stringify({ + server: "configured", + path: req.url, + method: req.method, + host: req.headers.host, + body, + }) + ); + }); + }); + let alternateRequests = 0; + const alternate = http.createServer((_req, res) => { + alternateRequests++; + res.end(JSON.stringify({ server: "alternate" })); + }); + let provider: Awaited> | undefined; + await withTestCleanup( + async () => { + const configuredUrl = await listen(configured); + const alternateUrl = await listen(alternate); + const upstream = await startWebSocketResponsesServer(); + provider = upstream; + upstream.forwardHttpTo(configuredUrl); + const paths = [ + "/copilot_internal/user?include_quota=true", + `${alternateUrl}/absolute-target`, + `//${new URL(alternateUrl).host}/network-target`, + ]; + for (const path of paths) { + const body = JSON.stringify({ + forwarded: path, + content: "fixture input", + }); + const response = await new Promise<{ + body: string; + headers: http.IncomingHttpHeaders; + }>((resolve, reject) => { + const request = http.request( + upstream.baseUrl, + { + path, + method: "POST", + headers: { "content-type": "application/json" }, + }, + (res) => { + res.setEncoding("utf8"); + let text = ""; + res.on("data", (chunk: string) => (text += chunk)); + res.on("error", reject); + res.on("end", () => resolve({ body: text, headers: res.headers })); + } + ); + request.on("error", reject); + request.end(body); + }); + expect(response.headers["content-type"]).toBe( + "application/json; charset=utf-8" + ); + expect(response.headers["x-content-type-options"]).toBe("nosniff"); + expect(JSON.parse(response.body)).toEqual({ + server: "configured", + path, + method: "POST", + host: new URL(configuredUrl).host, + body, + }); + } + expect(alternateRequests).toBe(0); + }, + () => provider?.stop(), + () => close(configured), + () => close(alternate) + ); + }); +}); + +describe("WebSocket provider message failures", () => { + it.each(["malformed JSON", "synchronous reply failure"])( + "reports %s through cleanup", + async (failure) => { + const provider = await startWebSocketResponsesServer(); + let stopped = false; + const socket = new WebSocket(provider.baseUrl.replace("http:", "ws:")); + await withTestCleanup( + async () => { + await new Promise((resolve, reject) => { + socket.once("open", resolve); + socket.once("error", reject); + }); + const replyError = new Error("reply failed synchronously"); + provider.state.responses.push(() => { + throw replyError; + }); + socket.send( + failure === "malformed JSON" + ? "{" + : JSON.stringify({ type: "response.create" }) + ); + await expect.poll(() => socket.readyState).toBe(WebSocket.CLOSED); + const stopping = provider.stop(); + stopped = true; + await expect(stopping).rejects.toMatchObject({ + message: "WebSocket provider fixture failed.", + errors: [ + failure === "malformed JSON" ? expect.any(SyntaxError) : replyError, + ], + }); + }, + () => socket.terminate(), + async () => { + if (!stopped) await provider.stop(); + } + ); + } + ); +}); diff --git a/nodejs/test/withTestCleanup.test.ts b/nodejs/test/withTestCleanup.test.ts new file mode 100644 index 0000000000..2f195ada5a --- /dev/null +++ b/nodejs/test/withTestCleanup.test.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it, vi } from "vitest"; +import { withTestCleanup } from "./helpers/withTestCleanup.js"; + +describe("withTestCleanup", () => { + it("returns the body result after every cleanup", async () => { + const first = vi.fn(); + const second = vi.fn(); + await expect(withTestCleanup(async () => 42, first, second)).resolves.toBe(42); + expect(first).toHaveBeenCalledOnce(); + expect(second).toHaveBeenCalledOnce(); + }); + + it("preserves the original failure when cleanup succeeds", async () => { + const failure = new Error("original assertion"); + const cleanup = vi.fn(); + await expect( + withTestCleanup(async () => { + throw failure; + }, cleanup) + ).rejects.toBe(failure); + expect(cleanup).toHaveBeenCalledOnce(); + }); + + it("preserves body and all teardown failures in order", async () => { + const failure = new Error("original assertion"); + const first = new Error("server teardown"); + const second = new Error("directory teardown"); + await expect( + withTestCleanup( + async () => { + throw failure; + }, + () => { + throw first; + }, + async () => { + throw second; + } + ) + ).rejects.toMatchObject({ + errors: [failure, first, second], + cause: failure, + }); + }); + + it("fails an otherwise-passing test when teardown fails, without skipping later cleanup", async () => { + const failure = new Error("server teardown"); + const last = vi.fn(); + await expect( + withTestCleanup( + async () => undefined, + () => { + throw failure; + }, + last + ) + ).rejects.toBe(failure); + expect(last).toHaveBeenCalledOnce(); + }); + + it("aggregates multiple teardown failures without inventing a body failure", async () => { + const first = new Error("server teardown"); + const second = new Error("directory teardown"); + await expect( + withTestCleanup( + async () => undefined, + () => { + throw first; + }, + () => { + throw second; + } + ) + ).rejects.toMatchObject({ errors: [first, second] }); + }); +}); diff --git a/nodejs/test/workflow.test.ts b/nodejs/test/workflow.test.ts index 0efb221fc9..2464cf6668 100644 --- a/nodejs/test/workflow.test.ts +++ b/nodejs/test/workflow.test.ts @@ -6,12 +6,11 @@ import { describe, expect, it, vi } from "vitest"; import { ErrorCodes, ResponseError } from "vscode-jsonrpc/node.js"; import { CopilotClient } from "../src/client.js"; import { CopilotSession } from "../src/session.js"; -import { defineFactory } from "../src/factory.js"; import { defineWorkflow } from "../src/workflow.js"; function runUpdatedEvent(runId: string, revision: number): Record { return { - type: "factory.run_updated", + type: "workflow.run_updated", id: `event-${runId}-${revision}`, parentId: null, timestamp: new Date().toISOString(), @@ -109,7 +108,7 @@ describe("dynamic workflows", () => { ); }); - it("serializes workflow metadata and rejects mixed contribution generations", async () => { + it("serializes workflow metadata", async () => { const client = new CopilotClient(); const workflow = defineWorkflow({ meta: { @@ -120,14 +119,6 @@ describe("dynamic workflows", () => { }, run: async () => ({ ok: true }), }); - const factory = defineFactory({ - meta: { - name: "legacy", - description: "Legacy factory", - phases: [], - }, - run: async () => ({ ok: true }), - }); const sendRequest = vi.fn(async (method: string, params: Record) => { if (method === "session.resume") { return { sessionId: params.sessionId }; @@ -145,54 +136,10 @@ describe("dynamic workflows", () => { const payload = sendRequest.mock.calls.find( ([method]) => method === "session.resume" )![1] as { - factories?: unknown[]; workflows?: unknown[]; }; expect(payload.workflows).toEqual([workflow.meta]); - expect(payload.factories).toBeUndefined(); expect(payload.workflows?.[0]).not.toHaveProperty("run"); - - await expect( - client.resumeSessionForExtension( - "session-mixed-registration", - { onPermissionRequest: () => ({ kind: "approved" }) }, - { factories: [factory], workflows: [workflow] } - ) - ).rejects.toThrow("cannot include both factories and workflows"); - }); - - it("preserves the legacy factory-array extension call shape", async () => { - const client = new CopilotClient(); - const factory = defineFactory({ - meta: { - name: "legacy", - description: "Legacy factory", - phases: [], - }, - run: async () => ({ ok: true }), - }); - const sendRequest = vi.fn(async (method: string, params: Record) => { - if (method === "session.resume") { - return { sessionId: params.sessionId }; - } - throw new Error(`Unexpected method: ${method}`); - }); - (client as never as { connection: unknown }).connection = { sendRequest }; - - await client.resumeSessionForExtension( - "session-legacy-factory-registration", - { onPermissionRequest: () => ({ kind: "approved" }) }, - [factory] - ); - - const payload = sendRequest.mock.calls.find( - ([method]) => method === "session.resume" - )![1] as { - factories?: unknown[]; - workflows?: unknown[]; - }; - expect(payload.factories).toEqual([factory.meta]); - expect(payload.workflows).toBeUndefined(); }); it("uses workflow-specific validation errors", async () => { diff --git a/nodejs/tsconfig.test.json b/nodejs/tsconfig.test.json index b3bda3f2b0..6f8ef72510 100644 --- a/nodejs/tsconfig.test.json +++ b/nodejs/tsconfig.test.json @@ -9,10 +9,13 @@ "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", - "test/message-source.test.ts" + "test/sandbox-config.test.ts", + "test/message-source.test.ts", + "test/legacy-request-compatibility.test.ts" ], "exclude": ["node_modules", "dist"] } diff --git a/nodejs/vitest.config.ts b/nodejs/vitest.config.ts index 4ac56bb45e..54c1d1681b 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,27 @@ 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", + "**/test/e2e/websocket_terminals.e2e.test.ts", +]; + export default defineConfig({ test: { + ...(isByokBackend ? { include: ["test/e2e/*.e2e.test.ts"] } : {}), globals: true, environment: "node", testTimeout: integrationTestTimeout, @@ -60,6 +82,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 +90,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 a4f34e7f03..6f8ab3127a 100644 --- a/python/README.md +++ b/python/README.md @@ -73,6 +73,15 @@ 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. + ```python import asyncio @@ -141,6 +150,12 @@ or supply its reply. If you need more control over the lifecycle, you can call `start()`, `stop()`, and `disconnect()` manually: +For an SDK-owned stdio runtime, `stop()` and async context-manager exit request +shutdown, close stdin, and wait up to 10 seconds for the process to finish host +cleanup, including telemetry flushing. A process that does not exit is terminated, +then killed if necessary, with bounded waits. `force_stop()` skips graceful cleanup; +externally managed runtimes are not shut down. + ```python import asyncio @@ -190,6 +205,58 @@ asyncio.run(main()) - ✅ Async/await native - ✅ Async context manager support for automatic resource cleanup +### Skill providers (experimental) + +Skill providers let a session serve skills from SDK-owned storage instead of +`SKILL.md` files on disk. Implement `SkillProvider`, return generated +`SkillProviderDescriptor` values from `list_skills()`, and return the skill +markdown from `read_skill(name)`. Both methods can be sync or async; keep sync +providers fast because they run on the event loop. + +```python +import asyncio + +from copilot import CopilotClient, SkillProvider, SkillProviderDescriptor + + +class MemorySkills(SkillProvider): + def list_skills(self): + return [ + SkillProviderDescriptor( + name="release-notes", + description="Draft release notes from recent changes", + ) + ] + + async def read_skill(self, name: str) -> str | None: + if name != "release-notes": + return None + return "Draft concise release notes with highlights and migration notes." + + +async def main(): + async with CopilotClient(mode="empty", base_directory="./.copilot-empty") as client: + async with await client.create_session( + available_tools=[], + enable_skills=True, + skill_provider=MemorySkills(), + ) as session: + await session.send("Use /release-notes for this change set") + + +asyncio.run(main()) +``` + +In `"empty"` mode, set `enable_skills=True`; otherwise the provider is bound but +dormant. Providers are ephemeral and are not persisted, so pass `skill_provider=` +again to `resume_session()`. Cloud sessions do not support skill providers. +Providers must be safe for concurrent calls because the runtime may list or read +skills from multiple agent paths at the same time. When the runtime cancels a +call, for example after its 30-second limit or when the session disconnects, an +async provider's task is cancelled and receives `asyncio.CancelledError`. Closing +the connection or force-stopping the client doesn't cancel a running call; it +continues until it returns. + ## API Reference ### CopilotClient @@ -241,9 +308,10 @@ All options are kw-only parameters: - `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods. - `base_directory` (str | None): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When `None`, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using a `UriRuntimeConnection`. - `extension_launch_provider` (ExtensionLaunchProviderHandler | None): Experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can be created. +- `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. @@ -270,6 +338,51 @@ conn.env = {"MY_VAR": "value"} client = CopilotClient(connection=conn) # do NOT also pass env=... here ``` +### Installation confirmation (experimental) + +Set `installation_confirmation_handler` on `CopilotClient` to receive the +runtime's `installations.confirm` callback through +`InstallationConfirmationHandler`. The handler receives the generated +`InstallationConfirmationRequest` and an `InstallationConfirmationContext`, and +returns only an explicit `InstallationDecision` (or `"confirm"`, `"decline"` or +`"cancel"`). The SDK echoes the original challenge and review fingerprint; it +never infers approval. + +Match `operation_id` and `policy_session_id` against the original action on this +exact connection before presenting the complete review. Missing legacy session +metadata does not select a default session. Refuse unknown operations or +incomplete reviews. Concurrent reviews are independent and do not block the +request router. + +`context.cancelled` is an `asyncio.Event` set when this review is retired, +whether by the runtime's numeric `$/cancelRequest`, runtime-enforced expiry, or +loss of the original connection. Separately spawned UI work must observe this +signal and close itself when it is set. A late handler result cannot approve a +retired request. Dropping an outbound installation or OAuth future does not +cancel that operation. + +Call `client.rpc.mcp.prepare_install(...)` before `apply_install(...)`. Register +its inert runtime-issued `operation_id`, original expiry and captured session on +this client before applying. Removal uses `plan_uninstall(...)` then +`apply_uninstall(...)`; its `operation_id` identifies the operation, while +`plan_handle` is the one-use removal input. Never interchange them. The +`installations` namespace exposes `list`, `recover`, `status` and `cancel`. +Control uncertain work using its original connection and operation ID, without +selecting a replacement session or replaying apply. + +Owned OAuth uses `session.rpc.mcp.oauth.prepare_login(...)` to return `login_id` +before browser, network or cached-reconnect work. Keep that ID with the original +session and `expected_installation_id` for `login(...)` and `cancel_login(...)`. +Preparation freezes reauthentication and display options. Dropping the login +future is not a substitute for `cancel_login(...)`. Manual MCP OAuth retains its +direct `login(...)` path. + +These methods require a matching runtime and available owned-lifecycle support. +Capability negotiation does not promise availability; preserve typed refusals +instead of falling back to raw configuration writes. Generated presence and +transport tests do not establish a working installer, live OAuth or restart +safety. + ### In-process (FFI) transport > ⚠️ **Experimental.** The in-process transport loads the runtime's native shared @@ -457,6 +570,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. @@ -471,6 +605,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: @@ -830,6 +988,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): @@ -995,6 +1155,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. @@ -1033,6 +1203,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 @@ -1078,6 +1256,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 { @@ -1095,6 +1289,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: @@ -1109,8 +1305,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 ce0baaab39..9e9a9fa8bf 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -110,6 +110,7 @@ PermissionDecisionOutcome, PermissionDecisionSurface, PermissionResponseCapability, + SkillProviderDescriptor, ) from .generated.session_events import ( AutoTierSwitchFailureReason, @@ -119,6 +120,18 @@ SessionEvent, SessionEventType, ) +from .host import ( + AhpHost, + AhpHostExit, + AhpHostOptions, + AhpSessionCreateRequest, + AhpSessionResumeRequest, +) +from .installation_confirmation import ( + InstallationConfirmationContext, + InstallationConfirmationDecision, + InstallationConfirmationHandler, +) from .session import ( AgentMessageSource, AgentStopHandler, @@ -179,6 +192,7 @@ PreToolUseHookInput, PreToolUseHookOutput, ProviderConfig, + ProviderConfigModelProvider, ProviderModelConfig, ProviderTokenArgs, ReasoningSummary, @@ -196,8 +210,16 @@ SessionStartHookOutput, SessionUiApi, SessionUiCapabilities, + SkillProvider, + SubagentStartHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHandler, + SubagentStopHookInput, + SubagentStopHookOutput, SystemMessageConfig, ToolSearchConfig, + TranscriptRecoveryReport, UserInputHandler, UserInputRequest, UserInputResponse, @@ -210,11 +232,13 @@ create_attributed_permission_result, ) from .session_fs_provider import ( + SessionFsBinaryProvider, SessionFsFileInfo, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, SessionFsSqliteTransactionFailure, + SessionFsWriteFailure, create_session_fs_adapter, ) from .tools import ( @@ -236,6 +260,11 @@ __version__ = "0.0.0.dev0" __all__ = [ + "AhpHost", + "AhpHostExit", + "AhpHostOptions", + "AhpSessionCreateRequest", + "AhpSessionResumeRequest", "AgentMessageSource", "AgentStopHandler", "AgentStopHookInput", @@ -271,6 +300,7 @@ "CopilotClientMode", "CopilotExpAssignmentResponse", "CopilotSession", + "TranscriptRecoveryReport", "CopilotRequestContext", "CopilotRequestHandler", "CopilotWebSocketCloseStatus", @@ -312,6 +342,9 @@ "GitHubTokenResult", "GitHubTokenCancelledResult", "InfiniteSessionConfig", + "InstallationConfirmationContext", + "InstallationConfirmationDecision", + "InstallationConfirmationHandler", "InProcessRuntimeConnection", "InputOptions", "LargeToolOutputConfig", @@ -368,6 +401,7 @@ "PreToolUseHookInput", "PreToolUseHookOutput", "ProviderConfig", + "ProviderConfigModelProvider", "ProviderModelConfig", "ProviderTokenArgs", "ReasoningSummary", @@ -391,9 +425,11 @@ "SessionFsConfig", "SessionFsFileInfo", "SessionFsProvider", + "SessionFsBinaryProvider", "SessionFsSqliteProvider", "SessionFsSqliteQueryResult", "SessionFsSqliteTransactionFailure", + "SessionFsWriteFailure", "SessionHooks", "SessionLimitsConfig", "SessionLifecycleEvent", @@ -406,11 +442,19 @@ "SessionStartHandler", "SessionStartHookInput", "SessionStartHookOutput", + "SkillProvider", + "SkillProviderDescriptor", "SessionUiApi", "SessionUiCapabilities", "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/_jsonrpc.py b/python/copilot/_jsonrpc.py index d78fe8baf2..60af2a2074 100644 --- a/python/copilot/_jsonrpc.py +++ b/python/copilot/_jsonrpc.py @@ -36,7 +36,46 @@ class ProcessExitedError(Exception): pass +class _RequestCancelledError(Exception): + """Raised when the peer cancels an incoming request.""" + + RequestHandler = Callable[[dict], dict | Awaitable[dict]] +RawRequestHandler = Callable[[dict], None | Awaitable[None]] + + +class _IncomingRequestCancellation: + def __init__(self) -> None: + self._cancelled = False + self._callbacks: list[Callable[[], None]] = [] + self._lock = threading.Lock() + + def cancel(self) -> None: + with self._lock: + if self._cancelled: + return + self._cancelled = True + callbacks = list(self._callbacks) + self._callbacks.clear() + for callback in callbacks: + callback() + + def bind_event(self, loop: asyncio.AbstractEventLoop) -> asyncio.Event: + event = asyncio.Event() + + def set_event() -> None: + if not loop.is_closed(): + loop.call_soon_threadsafe(event.set) + + with self._lock: + if self._cancelled: + cancelled = True + else: + self._callbacks.append(set_event) + cancelled = False + if cancelled: + event.set() + return event def _log_request_timing( @@ -82,6 +121,9 @@ def __init__(self, process): self.notification_handler: Callable[[str, dict], None] | None = None self.notification_method_handlers: dict[str, Callable[[dict], Any]] = {} self.request_handlers: dict[str, RequestHandler] = {} + self._cancellable_request_methods: set[str] = set() + self.raw_request_handlers: dict[str, RawRequestHandler] = {} + self._incoming_request_cancellations: dict[int, _IncomingRequestCancellation] = {} self._running = False self._read_thread: threading.Thread | None = None self._stderr_thread: threading.Thread | None = None @@ -257,11 +299,46 @@ def set_notification_method_handler(self, method: str, handler: Callable[[dict], else: self.notification_method_handlers[method] = handler - def set_request_handler(self, method: str, handler: RequestHandler): + def set_request_handler( + self, method: str, handler: RequestHandler | None, *, cancellable: bool = False + ): + """Register a handler for incoming requests of ``method``. + + When ``cancellable`` is true, a ``$/cancelRequest`` for a pending request + cancels the handler's task and fails the request with code -32800. + """ if handler is None: self.request_handlers.pop(method, None) + self._cancellable_request_methods.discard(method) else: self.request_handlers[method] = handler + if cancellable: + self._cancellable_request_methods.add(method) + else: + self._cancellable_request_methods.discard(method) + + def set_raw_request_handler(self, method: str, handler: RawRequestHandler | None): + """Register a request handler that receives the full JSON-RPC message. + + Raw handlers are responsible for sending their own response. Use this + for request lifecycles that depend on the JSON-RPC request id. + """ + if handler is None: + self.raw_request_handlers.pop(method, None) + else: + self.raw_request_handlers[method] = handler + + def incoming_request_cancelled_event(self, request_id: Any) -> asyncio.Event: + """Return the ``$/cancelRequest`` event for a pending numeric request id.""" + if self._loop is None: + raise RuntimeError("Client not started. Call start() first.") + if not isinstance(request_id, int) or isinstance(request_id, bool): + return asyncio.Event() + with self._pending_lock: + signal = self._incoming_request_cancellations.get(request_id) + if signal is None: + return asyncio.Event() + return signal.bind_event(self._loop) async def _send_message(self, message: dict): """Send a JSON-RPC message with a Content-Length header.""" @@ -440,6 +517,9 @@ def _handle_message(self, message: dict): if "method" in message and "id" not in message: method = message["method"] params = message.get("params", {}) + if method == "$/cancelRequest": + self._handle_cancel_request(params) + return handler = self.notification_method_handlers.get(method) if handler is not None and self._loop: # Method-specific notification handler takes precedence. @@ -456,8 +536,26 @@ def _handle_message(self, message: dict): def _handle_request(self, message: dict): method = message.get("method", "") + request_id = message.get("id") + cancellation_id: int | None = None + if isinstance(request_id, int) and not isinstance(request_id, bool): + with self._pending_lock: + self._incoming_request_cancellations[request_id] = _IncomingRequestCancellation() + cancellation_id = request_id + raw_handler = self.raw_request_handlers.get(method) + if raw_handler is not None: + if not self._loop: + return + asyncio.run_coroutine_threadsafe( + self._dispatch_raw_request(message, raw_handler, cancellation_id), + self._loop, + ) + return handler = self.request_handlers.get(method) if not handler: + if cancellation_id is not None: + with self._pending_lock: + self._incoming_request_cancellations.pop(cancellation_id, None) if self._loop: asyncio.run_coroutine_threadsafe( self._send_error_response( @@ -469,10 +567,24 @@ def _handle_request(self, message: dict): if not self._loop: return asyncio.run_coroutine_threadsafe( - self._dispatch_request(message, handler), + self._dispatch_request( + message, + handler, + cancellation_id, + cancellable=method in self._cancellable_request_methods, + ), self._loop, ) + def _handle_cancel_request(self, params: dict) -> None: + cancel_id = params.get("id") if isinstance(params, dict) else None + if not isinstance(cancel_id, int) or isinstance(cancel_id, bool): + return + with self._pending_lock: + signal = self._incoming_request_cancellations.get(cancel_id) + if signal is not None: + signal.cancel() + def _dispatch_notification(self, handler: Callable[[dict], Any], params: dict): """Invoke a method-specific notification handler. Runs on the event loop; coroutine results are scheduled and any error is logged (notifications @@ -492,12 +604,41 @@ async def _await_outcome(): asyncio.create_task(_await_outcome()) - async def _dispatch_request(self, message: dict, handler: RequestHandler): + async def _dispatch_raw_request( + self, message: dict, handler: RawRequestHandler, cancellation_id: int | None + ): + try: + outcome = handler(message) + if inspect.isawaitable(outcome): + await outcome + except Exception: # pylint: disable=broad-except + logger.debug( + "Error handling raw JSON-RPC method %s", + message.get("method", ""), + exc_info=True, + ) + await self._send_error_response(message["id"], -32603, "Internal error", None) + finally: + if cancellation_id is not None: + with self._pending_lock: + self._incoming_request_cancellations.pop(cancellation_id, None) + + async def _dispatch_request( + self, + message: dict, + handler: RequestHandler, + cancellation_id: int | None, + *, + cancellable: bool = False, + ): try: params = message.get("params", {}) outcome = handler(params) if inspect.isawaitable(outcome): - outcome = await outcome + if cancellable and cancellation_id is not None: + outcome = await self._await_cancellable(outcome, cancellation_id) + else: + outcome = await outcome if outcome is not None and not isinstance( outcome, dict | list | str | int | float | bool ): @@ -506,6 +647,9 @@ async def _dispatch_request(self, message: dict, handler: RequestHandler): f"got {type(outcome).__name__}" ) await self._send_response(message["id"], outcome) + except _RequestCancelledError: + logger.debug("JSON-RPC method %s cancelled", message.get("method", "")) + await self._send_error_response(message["id"], -32800, "Request cancelled", None) except JsonRpcError as exc: logger.debug( "Error handling JSON-RPC method %s: %s", message.get("method", ""), exc.message @@ -519,6 +663,31 @@ async def _dispatch_request(self, message: dict, handler: RequestHandler): exc_info=True, ) await self._send_error_response(message["id"], -32603, str(exc), None) + finally: + if cancellation_id is not None: + with self._pending_lock: + self._incoming_request_cancellations.pop(cancellation_id, None) + + async def _await_cancellable(self, outcome: Awaitable[Any], cancellation_id: int) -> Any: + """Await ``outcome``, cancelling it if the peer cancels the request.""" + with self._pending_lock: + signal = self._incoming_request_cancellations.get(cancellation_id) + if signal is None or self._loop is None: + return await outcome + task = asyncio.ensure_future(outcome) + waiter = asyncio.ensure_future(signal.bind_event(self._loop).wait()) + try: + await asyncio.wait({task, waiter}, return_when=asyncio.FIRST_COMPLETED) + except BaseException: + task.cancel() + raise + finally: + waiter.cancel() + if task.done(): + return task.result() + task.cancel() + await asyncio.wait({task}) + raise _RequestCancelledError() async def _send_response(self, request_id: str, result: Any): response = { 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 058f7a9788..fa360f2350 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -96,6 +96,11 @@ SessionEvent, session_event_from_dict, ) +from .host import AhpHost, AhpHostOptions, _AhpHostManager +from .installation_confirmation import ( + InstallationConfirmationHandler, + _InstallationConfirmationAdapter, +) from .session import ( AutoModeSwitchHandler, AutoTier, @@ -124,11 +129,14 @@ SessionFsConfig, SessionHooks, SessionLimitsConfig, + SkillProvider, 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 @@ -179,6 +187,14 @@ class GitHubTokenCancelledResult(TypedDict): ambient authentication. """ + +def _required_string_param(params: dict, name: str) -> str: + value = params.get(name) if isinstance(params, dict) else None + if not isinstance(value, str): + raise JsonRpcError(-32602, "Invalid params") + return value + + # ============================================================================ # Connection Types # ============================================================================ @@ -352,6 +368,12 @@ class ManagedSettingsPermissions: allow: list[str] | None = None """Operations permitted without prompting. Every declared ``allow`` list across managed layers must admit an operation for it to be allowed.""" + limit_to: list[str] | None = None + """Closed-world host boundary expressed as ``Domain(hostname)``, + ``Domain(IP)``, or ``Domain(*.example.com)`` rules. Schemes, ports, paths, + queries, and fragments are rejected. Multiple managed layers intersect + their lists; an empty list denies all hosts. Sent on the wire as + ``limitTo``.""" @dataclass @@ -385,6 +407,8 @@ def _managed_settings_to_dict(settings: ManagedSettings) -> dict[str, Any]: perms["ask"] = list(permissions.ask) if permissions.allow is not None: perms["allow"] = list(permissions.allow) + if permissions.limit_to is not None: + perms["limitTo"] = list(permissions.limit_to) wire["permissions"] = perms return wire @@ -819,6 +843,7 @@ class _CopilotClientOptions: base_directory: str | None = None builtin_plugin_directories: tuple[str, ...] = () extension_launch_provider: ExtensionLaunchProviderHandler | None = None + installation_confirmation_handler: InstallationConfirmationHandler | None = None use_logged_in_user: bool | None = None telemetry: TelemetryConfig | None = None session_fs: SessionFsConfig | None = None @@ -1420,6 +1445,7 @@ def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent: # Servers reporting a version below this are rejected. _MIN_PROTOCOL_VERSION = 3 _RUNTIME_SHUTDOWN_TIMEOUT_SECONDS = 10 +_CLI_PROCESS_GRACEFUL_EXIT_TIMEOUT_SECONDS = 10 _CLI_PROCESS_EXIT_TIMEOUT_SECONDS = 5 @@ -1575,6 +1601,7 @@ def __init__( base_directory: str | None = None, builtin_plugin_directories: Sequence[str] | None = None, extension_launch_provider: ExtensionLaunchProviderHandler | None = None, + installation_confirmation_handler: InstallationConfirmationHandler | None = None, use_logged_in_user: bool | None = None, telemetry: TelemetryConfig | None = None, session_fs: SessionFsConfig | None = None, @@ -1617,6 +1644,11 @@ def __init__( extension_launch_provider: Connection-level extension launch profile provider. When set, it is registered during startup before any session can be created. + installation_confirmation_handler: Experimental connection-global + human review handler for ``installations.confirm``. Receives the + typed request and independent request/connection cancellation + signals, and returns only an explicit decision. This does not + enable installation capabilities. use_logged_in_user: Use the logged-in user for authentication. ``None`` (default) resolves to ``True`` unless ``github_token`` is set. @@ -1671,6 +1703,7 @@ def __init__( base_directory=base_directory, builtin_plugin_directories=tuple(builtin_plugin_directories or ()), extension_launch_provider=extension_launch_provider, + installation_confirmation_handler=installation_confirmation_handler, use_logged_in_user=use_logged_in_user, telemetry=telemetry, session_fs=session_fs, @@ -1768,6 +1801,9 @@ def __init__( self._start_lock = asyncio.Lock() self._sessions: dict[str, CopilotSession] = {} self._sessions_lock = threading.Lock() + self._ahp_hosts = _AhpHostManager( + self._get_session, self.create_session, self.resume_session + ) self._github_token_providers: dict[str, _GitHubTokenProviderRegistration] = {} self._github_token_providers_lock = threading.Lock() self._github_token_provider_adapter = _GitHubTokenProviderAdapter(self) @@ -1785,6 +1821,7 @@ def __init__( self._session_fs_config = options.session_fs self._request_handler = options.request_handler self._llm_inference_adapter: _CopilotRequestAdapterHandler | None = None + self._installation_confirmation_adapter: _InstallationConfirmationAdapter | None = None def _resolve_runtime_entrypoint( self, @@ -2062,8 +2099,8 @@ async def stop(self) -> None: This method performs graceful cleanup: 1. Closes all active sessions (releases in-memory resources) 2. Requests runtime shutdown for SDK-owned CLI processes - 3. Closes the JSON-RPC connection - 4. Terminates the CLI server process (if spawned by this client) + 3. Closes owned stdio input and waits for host cleanup and natural exit + 4. Closes the JSON-RPC connection and terminates any remaining owned process Note: session data on disk is preserved, so sessions can be resumed later. To permanently remove session data before stopping, call @@ -2080,6 +2117,9 @@ async def stop(self) -> None: ... print(f"Cleanup error: {error.message}") """ errors: list[StopError] = [] + if self._installation_confirmation_adapter is not None: + self._installation_confirmation_adapter.close_connection() + self._ahp_hosts.disconnect() if self._llm_inference_adapter is not None: self._llm_inference_adapter.cancel_pending() @@ -2128,11 +2168,32 @@ async def stop(self) -> None: ) errors.append(StopError(message=f"Failed to gracefully shut down runtime: {e}")) + # Host telemetry is finalized after stdio EOF, not the shutdown response. + # Keep the readers alive while the child drains its final output. + if ( + self._cli_process is not None + and not self._is_external_server + and isinstance(self._connection, StdioRuntimeConnection) + and self._cli_process.poll() is None + ): + try: + if self._cli_process.stdin is not None: + self._cli_process.stdin.close() + await asyncio.to_thread( + self._cli_process.wait, + timeout=_CLI_PROCESS_GRACEFUL_EXIT_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired: + logger.debug("Timed out waiting for graceful CLI exit; terminating the process") + except OSError: + logger.debug("Error while closing Copilot CLI stdin", exc_info=True) + # Close client if self._client: await self._client.stop() self._client = None self._rpc = None + self._installation_confirmation_adapter = None # Clear models cache async with self._models_cache_lock: @@ -2141,7 +2202,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 @@ -2155,15 +2216,7 @@ async def stop(self) -> None: logger.debug("Error while closing Copilot runtime transport", exc_info=True) self._process = None - # Terminate CLI process (only if we spawned it). - # - # Per the runtime.shutdown contract, the runtime completes all cleanup - # *before* responding and then leaves termination to the caller ("callers - # may then terminate the owned runtime process"). It deliberately keeps - # its JSON-RPC server alive to send the response and does not self-exit, - # so there is no point waiting a grace window for a self-exit that will - # never come. Once shutdown has completed (or failed) we terminate the - # child immediately and only wait to reap it. + # Terminate and reap an owned process that did not exit gracefully. if self._cli_process and not self._is_external_server: poll = getattr(self._cli_process, "poll", None) is_running = poll is None or poll() is None @@ -2216,6 +2269,9 @@ async def force_stop(self) -> None: ... except asyncio.TimeoutError: ... await client.force_stop() """ + if self._installation_confirmation_adapter is not None: + self._installation_confirmation_adapter.close_connection() + self._ahp_hosts.disconnect() if self._llm_inference_adapter is not None: self._llm_inference_adapter.cancel_pending() @@ -2231,7 +2287,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: @@ -2251,7 +2308,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 @@ -2267,6 +2324,7 @@ async def force_stop(self) -> None: ) self._client = None self._rpc = None + self._installation_confirmation_adapter = None # Clear models cache async with self._models_cache_lock: @@ -2333,6 +2391,7 @@ async def create_session( enable_skills: bool | None = None, included_builtin_skills: list[str] | None = None, skill_directories: list[str] | None = None, + skill_provider: SkillProvider | None = None, plugin_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, @@ -2483,6 +2542,11 @@ async def create_session( enable_session_store: Enables the cross-session store. enable_skills: Enables skill loading. skill_directories: Directories to search for skills. + skill_provider: **Experimental.** Session-scoped provider for SDK-owned + skills. Providers are not persisted; re-supply the provider when + resuming the session. Synchronous providers run on the event loop, + so keep them fast or make them async. Not supported for cloud + sessions. instruction_directories: Additional directories to search for custom instruction files. disabled_skills: Skills to disable. @@ -2575,29 +2639,12 @@ async def create_session( raise ValueError("github_token and github_token_provider are mutually exclusive") if ask_user_variant not in (None, "legacy", "elicitation"): raise ValueError('ask_user_variant must be "legacy" or "elicitation"') + if cloud is not None and skill_provider is not None: + raise ValueError("Skill providers are not supported for cloud sessions.") 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 @@ -2714,7 +2761,7 @@ async def create_session( # Add working directory if provided if working_directory: payload["workingDirectory"] = working_directory - if additional_directories: + if additional_directories is not None: payload["additionalDirectories"] = additional_directories # Add streaming option if provided @@ -2817,6 +2864,8 @@ async def create_session( payload["enableSessionStore"] = enable_session_store if enable_skills is not None: payload["enableSkills"] = enable_skills + if skill_provider is not None: + payload["hasSkillProvider"] = True # Add skill directories configuration if provided if skill_directories: @@ -2936,9 +2985,18 @@ 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) + s._register_skill_provider(skill_provider) s._register_permission_handler(on_permission_request) s._register_mcp_auth_handler(on_mcp_auth_request) if on_user_input_request: @@ -2974,6 +3032,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 @@ -2998,15 +3070,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 @@ -3037,9 +3134,18 @@ 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 + failed_session = session + if failed_session is not None: + failed_session._clear_skill_provider() + 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( @@ -3064,6 +3170,7 @@ def _register_inline(raw_response: Any) -> None: self._commit_github_token_provider( session.session_id, github_token_provider_registration_id ) + self._ahp_hosts.capture(session, payload) log_timing( logger, @@ -3130,6 +3237,7 @@ async def resume_session( enable_skills: bool | None = None, included_builtin_skills: list[str] | None = None, skill_directories: list[str] | None = None, + skill_provider: SkillProvider | None = None, plugin_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, @@ -3149,6 +3257,8 @@ async def resume_session( github_token_provider: GitHubTokenProvider | None = None, 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, @@ -3276,6 +3386,10 @@ async def resume_session( enable_session_store: Enables the cross-session store. enable_skills: Enables skill loading. skill_directories: Directories to search for skills. + skill_provider: **Experimental.** Session-scoped provider for SDK-owned + skills. Providers are not persisted; callers must re-supply the + provider on resume. Synchronous providers run on the event loop, + so keep them fast or make them async. instruction_directories: Additional directories to search for custom instruction files. disabled_skills: Skills to disable. @@ -3308,7 +3422,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 @@ -3368,26 +3489,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 @@ -3526,7 +3628,7 @@ async def resume_session( if working_directory: payload["workingDirectory"] = working_directory - if additional_directories: + if additional_directories is not None: payload["additionalDirectories"] = additional_directories if config_directory: payload["configDir"] = config_directory @@ -3546,11 +3648,17 @@ async def resume_session( payload["enableSessionStore"] = enable_session_store if enable_skills is not None: payload["enableSkills"] = enable_skills + if skill_provider is not None: + payload["hasSkillProvider"] = True 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 - # TODO: disable_resume is not a keyword arg yet; keeping for future use if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) if diagnostics is not None: @@ -3659,9 +3767,18 @@ 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) + session._register_skill_provider(skill_provider) session._register_permission_handler(on_permission_request) session._register_mcp_auth_handler(on_mcp_auth_request) if on_user_input_request: @@ -3684,6 +3801,7 @@ async def resume_session( if on_event: session.on(on_event) with self._sessions_lock: + replaced_session = self._sessions.get(session_id) self._sessions[session_id] = session log_timing( logger, @@ -3717,6 +3835,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") @@ -3730,8 +3855,15 @@ async def resume_session( {"sessionId": session.session_id, "eventType": "mcp.oauth_required"}, ) except BaseException as exc: + # The runtime keeps the resident session's bindings when a resume + # fails, so its callbacks must keep reaching the replaced session. with self._sessions_lock: - self._sessions.pop(session_id, None) + if self._sessions.get(session_id) is session: + if replaced_session is not None: + self._sessions[session_id] = replaced_session + else: + del self._sessions[session_id] + session._clear_skill_provider() self._unregister_github_token_provider(github_token_provider_registration_id) if not isinstance(exc, asyncio.CancelledError): log_timing( @@ -3754,6 +3886,7 @@ async def resume_session( included_builtin_skills, ) self._commit_github_token_provider(session_id, github_token_provider_registration_id) + self._ahp_hosts.capture(session, payload) log_timing( logger, @@ -3764,6 +3897,22 @@ async def resume_session( ) return session + 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 + callbacks. Stopping the host releases participation, not session ownership. + 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) + async def ping(self, message: str | None = None) -> PingResponse: """ Send a ping request to the server to verify connectivity. @@ -3975,6 +4124,7 @@ async def delete_session(self, session_id: str) -> None: with self._sessions_lock: session = self._sessions.pop(session_id, None) if session is not None: + session._clear_skill_provider() session._run_disconnect_callback() async def get_last_session_id(self) -> str | None: @@ -4241,6 +4391,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: @@ -4275,6 +4427,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: @@ -4450,6 +4604,8 @@ async def _start_cli_server(self) -> None: # credentials don't leak through a shared keytar store. if opts.mode == "empty": env["COPILOT_DISABLE_KEYTAR"] = "1" + if opts.mode != "empty": + env["COPILOT_RUNTIME_PROCESS_FILE_LOGGING"] = "1" if self._effective_connection_token: env["COPILOT_CONNECTION_TOKEN"] = self._effective_connection_token @@ -4683,6 +4839,7 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler( "systemMessage.transform", self._handle_system_message_transform ) + self._register_session_scoped_request_handlers() register_client_session_api_handlers(self._client, self._get_client_session_handlers) self._register_client_global_handlers() @@ -4802,6 +4959,7 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler( "systemMessage.transform", self._handle_system_message_transform ) + self._register_session_scoped_request_handlers() register_client_session_api_handlers(self._client, self._get_client_session_handlers) self._register_client_global_handlers() @@ -4888,6 +5046,7 @@ def _register_client_global_handlers(self) -> None: if not self._client: return self._llm_inference_adapter = None + self._installation_confirmation_adapter = None if self._request_handler is not None: self._llm_inference_adapter = create_copilot_request_adapter( self._request_handler, @@ -4906,6 +5065,19 @@ def _register_client_global_handlers(self) -> None: git_hub_token=self._github_token_provider_adapter, ), ) + self._installation_confirmation_adapter = _InstallationConfirmationAdapter( + self._client, + self._options.installation_confirmation_handler, + ) + self._installation_confirmation_adapter.register() + self._client.set_request_handler("host.materializeSession", self._ahp_hosts.materialize) + self._client.set_notification_method_handler( + "host.sessionReleased", + lambda params: self._ahp_hosts.notification("host.sessionReleased", params), + ) + self._client.set_notification_method_handler( + "host.exited", lambda params: self._ahp_hosts.notification("host.exited", params) + ) def _register_github_token_provider( self, provider: GitHubTokenProvider | None, session_id: str | None @@ -4921,16 +5093,21 @@ def _register_github_token_provider( def _handle_connection_close(self) -> None: self._state = "disconnected" + if self._installation_confirmation_adapter is not None: + self._installation_confirmation_adapter.close_connection() with self._sessions_lock: sessions = list(self._sessions.values()) with self._github_token_providers_lock: self._github_token_providers.clear() + for session in sessions: + session._clear_skill_provider() client = self._client llm_inference_adapter = self._llm_inference_adapter loop = client._loop if client is not None else None if loop is not None and not loop.is_closed(): def cancel_pending_external_tools() -> None: + self._ahp_hosts.disconnect() if llm_inference_adapter is not None: llm_inference_adapter.cancel_pending() for session in sessions: @@ -4974,6 +5151,17 @@ def _get_session(self, session_id: str) -> CopilotSession | None: with self._sessions_lock: return self._sessions.get(session_id) + def _register_session_scoped_request_handlers(self) -> None: + if not self._client: + return + # Cancellable so a runtime ``$/cancelRequest`` cancels an async provider's task. + self._client.set_request_handler( + "skillProvider.list", self._handle_skill_provider_list, cancellable=True + ) + self._client.set_request_handler( + "skillProvider.read", self._handle_skill_provider_read, cancellable=True + ) + async def _set_llm_inference_provider(self) -> None: if self._request_handler is None or self._rpc is None: return @@ -4986,6 +5174,21 @@ def _get_client_session_handlers(self, session_id: str) -> ClientSessionApiHandl raise ValueError(f"unknown session {session_id}") return session._client_session_apis + async def _handle_skill_provider_list(self, params: dict) -> dict: + session_id = _required_string_param(params, "sessionId") + session = self._get_session(session_id) + if session is None: + raise JsonRpcError(-32603, f"Session not found: {session_id}") + return await session._handle_skill_provider_list() + + async def _handle_skill_provider_read(self, params: dict) -> dict: + session_id = _required_string_param(params, "sessionId") + name = _required_string_param(params, "name") + session = self._get_session(session_id) + if session is None: + raise JsonRpcError(-32603, f"Session not found: {session_id}") + return await session._handle_skill_provider_read(name) + async def _handle_user_input_request(self, params: dict) -> dict: """ Handle a user input request from the CLI server. diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 12de08401e..3f8dca1aa9 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: @@ -454,6 +460,26 @@ def to_dict(self) -> dict: result["resetDate"] = from_union([from_str, from_none], self.reset_date) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class AccountKind(Enum): + """The provider kind stamped on a signed-in account. + + 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. + + The provider kind to sign in with. + """ + ENTRA = "entra" + ENTRA_EMU = "entraEmu" + GITHUB_DOT_COM = "githubDotCom" + LOKI = "loki" + PROXIMA = "proxima" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AccountLoginRequest: @@ -554,6 +580,20 @@ def to_dict(self) -> dict: result["hasMoreUsers"] = from_bool(self.has_more_users) return result +class AuthEnumerateQueryKind(Enum): + ACCOUNTS = "accounts" + PROVIDERS = "providers" + +class AuthReadQueryKind(Enum): + ACTIVE_ACCOUNT = "activeAccount" + LAST_ERRORS = "lastErrors" + STATUS = "status" + +class AuthWriteKind(Enum): + LOGOUT = "logout" + SET_CREDENTIALS = "setCredentials" + SWITCH_ACTIVE = "switchActive" + # Experimental: this type is part of an experimental API and may change or be removed. class AdaptiveThinkingSupport(Enum): """Resolved Anthropic adaptive-thinking capability for a model. @@ -699,6 +739,70 @@ 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. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsCustomAgentInitialModelDecisionParams: + """The models a custom agent asks for, and the models actually available.""" + + agent_models_json: str + """The agent's declared `model:` entry, serialized. A single name or an ordered list of + acceptable names. + """ + available_models_json: str + """The models available to this session, serialized in the shape the model list carries.""" + + @staticmethod + def from_dict(obj: Any) -> '_AgentsCustomAgentInitialModelDecisionParams': + assert isinstance(obj, dict) + agent_models_json = from_str(obj.get("agentModelsJson")) + available_models_json = from_str(obj.get("availableModelsJson")) + return _AgentsCustomAgentInitialModelDecisionParams(agent_models_json, available_models_json) + + def to_dict(self) -> dict: + result: dict = {} + result["agentModelsJson"] = from_str(self.agent_models_json) + result["availableModelsJson"] = from_str(self.available_models_json) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsCustomAgentInitialModelDecisionResult: + """The model to switch to, and the warning to show when the agent's preference could not be + met. + """ + reasoning_effort: str | None = None + """The reasoning effort attached to the selected model preference. Absent when that + preference does not specify an effort. + """ + target_model: str | None = None + """The first available model that matches the agent's preferences. Absent when none of the + requested models is available. + """ + warning: str | None = None + """What to tell the user about an unmet preference. Absent when the preference was met. A + warning with no `targetModel` means the agent's models are all unavailable. + """ + + @staticmethod + def from_dict(obj: Any) -> '_AgentsCustomAgentInitialModelDecisionResult': + assert isinstance(obj, dict) + reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) + target_model = from_union([from_str, from_none], obj.get("targetModel")) + warning = from_union([from_str, from_none], obj.get("warning")) + return _AgentsCustomAgentInitialModelDecisionResult(reasoning_effort, target_model, warning) + + def to_dict(self) -> dict: + result: dict = {} + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) + if self.target_model is not None: + result["targetModel"] = from_union([from_str, from_none], self.target_model) + if self.warning is not None: + result["warning"] = from_union([from_str, from_none], self.warning) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AgentsDiscoverRequest: @@ -728,6 +832,195 @@ def to_dict(self) -> dict: result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsGetAvailableBuiltinsRequest: + """The feature flags to evaluate shipped agents against.""" + + context: str | None = None + """The surface asking, which gates agents that only apply to one client. Omit or pass null + to apply no client filter. + """ + feature_flags: dict[str, Any] | None = None + """Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or + pass null for no flags. + """ + overrides: dict[str, Any] | None = None + """Flag overrides keyed by name. A null entry uses the corresponding base flag; false + explicitly disables it. Omit or pass null for no overrides. + """ + + @staticmethod + def from_dict(obj: Any) -> '_AgentsGetAvailableBuiltinsRequest': + assert isinstance(obj, dict) + context = from_union([from_none, from_str], obj.get("context")) + feature_flags = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("featureFlags")) + overrides = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("overrides")) + return _AgentsGetAvailableBuiltinsRequest(context, feature_flags, overrides) + + def to_dict(self) -> dict: + result: dict = {} + if self.context is not None: + result["context"] = from_union([from_none, from_str], self.context) + if self.feature_flags is not None: + result["featureFlags"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.feature_flags) + if self.overrides is not None: + result["overrides"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.overrides) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _BuiltinAgentSummary: + """A shipped agent, named and described.""" + + description: str + """One-line description of what the agent does.""" + + name: str + """The agent name, as it appears in `getBuiltins`.""" + + @staticmethod + def from_dict(obj: Any) -> '_BuiltinAgentSummary': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + name = from_str(obj.get("name")) + return _BuiltinAgentSummary(description, name) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["name"] = from_str(self.name) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsGetBuiltinDefinitionRequest: + """The shipped agent whose definition to load.""" + + name: str + """The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside + that list is special-cased in code and has no definition, and is reported as an error + rather than as an empty definition. + """ + + @staticmethod + def from_dict(obj: Any) -> '_AgentsGetBuiltinDefinitionRequest': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + return _AgentsGetBuiltinDefinitionRequest(name) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsGetBuiltinDefinitionResult: + """One shipped agent's definition.""" + + definition_json: str + """The agent's definition, serialized as JSON. It carries the authored keys plus the + runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather + than an object because the runtime parses it with the agent schema's tolerant shape, + which accepts keys this contract does not name. + """ + + @staticmethod + def from_dict(obj: Any) -> '_AgentsGetBuiltinDefinitionResult': + assert isinstance(obj, dict) + definition_json = from_str(obj.get("definitionJson")) + return _AgentsGetBuiltinDefinitionResult(definition_json) + + def to_dict(self) -> dict: + result: dict = {} + result["definitionJson"] = from_str(self.definition_json) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsGetBuiltinListingDefinitionRequest: + """The shipped agent whose listing entry to load.""" + + name: str + """The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the + agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here + too, from its in-code definition. + """ + + @staticmethod + def from_dict(obj: Any) -> '_AgentsGetBuiltinListingDefinitionRequest': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + return _AgentsGetBuiltinListingDefinitionRequest(name) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsGetBuiltinListingDefinitionResult: + """One shipped agent, projected for a listing.""" + + definition_json: str + """The agent projected as a custom agent, serialized as JSON. It is a string rather than an + object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying + definition with the agent schema's tolerant shape, which accepts keys this contract does + not name. + """ + + @staticmethod + def from_dict(obj: Any) -> '_AgentsGetBuiltinListingDefinitionResult': + assert isinstance(obj, dict) + definition_json = from_str(obj.get("definitionJson")) + return _AgentsGetBuiltinListingDefinitionResult(definition_json) + + def to_dict(self) -> dict: + result: dict = {} + result["definitionJson"] = from_str(self.definition_json) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsGetBuiltinsResult: + """The agents this runtime ships, named so a consumer can tell them apart from authored ones.""" + + disableable_names: list[str] + """The subset of `names` a user is allowed to turn off. A shipped agent outside this list is + always active and a client should not offer a toggle for it. + """ + names: list[str] + """Every agent name this runtime ships.""" + + yaml_based_names: list[str] + """The subset of `names` defined by a shipped YAML definition. The remainder are + special-cased in code and have no definition to load. + """ + + @staticmethod + def from_dict(obj: Any) -> '_AgentsGetBuiltinsResult': + assert isinstance(obj, dict) + disableable_names = from_list(from_str, obj.get("disableableNames")) + names = from_list(from_str, obj.get("names")) + yaml_based_names = from_list(from_str, obj.get("yamlBasedNames")) + return _AgentsGetBuiltinsResult(disableable_names, names, yaml_based_names) + + def to_dict(self) -> dict: + result: dict = {} + result["disableableNames"] = from_list(from_str, self.disableable_names) + result["names"] = from_list(from_str, self.names) + result["yamlBasedNames"] = from_list(from_str, self.yaml_based_names) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AgentsGetDiscoveryPathsRequest: @@ -760,6 +1053,83 @@ def to_dict(self) -> dict: class APIKeyAuthInfoType(Enum): API_KEY = "api-key" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginAdvanceRequest: + """Advance an in-flight login flow, optionally fulfilling an input-required step.""" + + flow_id: str + """Opaque flow id from begin.""" + + input: str | None = None + """Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored + otherwise. + """ + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginAdvanceRequest': + assert isinstance(obj, dict) + flow_id = from_str(obj.get("flowId")) + input = from_union([from_str, from_none], obj.get("input")) + return AuthLoginAdvanceRequest(flow_id, input) + + def to_dict(self) -> dict: + result: dict = {} + result["flowId"] = from_str(self.flow_id) + if self.input is not None: + result["input"] = from_union([from_str, from_none], self.input) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class LoginProviderKind(Enum): + """The neutral provider kind. + + A provider a consumer may interactively sign in with. + + The provider kind to sign in with. + """ + ENTRA = "entra" + GITHUB_DOT_COM = "githubDotCom" + PROXIMA = "proxima" + +class AuthLoginStepKind(Enum): + AWAITING = "awaiting" + COMPLETED = "completed" + ERROR = "error" + INPUT_REQUIRED = "input-required" + NEEDS_INTERACTION = "needs-interaction" + OPEN_URL = "open-url" + +# Experimental: this type is part of an experimental API and may change or be removed. +class AuthLoginResultStatus(Enum): + """Current disposition of the login, including pending user decisions. + + 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. +@dataclass +class AuthLoginCancelRequest: + """Cancel an in-flight login flow.""" + + flow_id: str + """Opaque flow id from begin.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginCancelRequest': + assert isinstance(obj, dict) + flow_id = from_str(obj.get("flowId")) + return AuthLoginCancelRequest(flow_id) + + def to_dict(self) -> dict: + result: dict = {} + result["flowId"] = from_str(self.flow_id) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AuthValidationError: @@ -787,6 +1157,75 @@ def to_dict(self) -> dict: result["githubMessage"] = from_union([from_str, from_none], self.github_message) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthStatusDto: + """The neutral authentication status summary. + + Neutral authentication status summary. + """ + account_count: int + """Number of signed-in accounts in the roster.""" + + is_authenticated: bool + """Whether the session has resolved authentication.""" + + active_host: str | None = None + """Active account host, if authenticated.""" + + active_login: str | None = None + """Active account login, if authenticated.""" + + copilot_plan: str | None = None + """Copilot plan tier of the active account, if known.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthStatusDto': + assert isinstance(obj, dict) + account_count = from_int(obj.get("accountCount")) + is_authenticated = from_bool(obj.get("isAuthenticated")) + active_host = from_union([from_str, from_none], obj.get("activeHost")) + active_login = from_union([from_str, from_none], obj.get("activeLogin")) + copilot_plan = from_union([from_str, from_none], obj.get("copilotPlan")) + return AuthStatusDto(account_count, is_authenticated, active_host, active_login, copilot_plan) + + def to_dict(self) -> dict: + result: dict = {} + result["accountCount"] = from_int(self.account_count) + result["isAuthenticated"] = from_bool(self.is_authenticated) + if self.active_host is not None: + result["activeHost"] = from_union([from_str, from_none], self.active_host) + if self.active_login is not None: + result["activeLogin"] = from_union([from_str, from_none], self.active_login) + if self.copilot_plan is not None: + result["copilotPlan"] = from_union([from_str, from_none], self.copilot_plan) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthWriteResult: + """Result of a non-interactive accounts mutation.""" + + ok: bool + """Whether the mutation was applied.""" + + more_users: bool | None = None + """For a logout, whether other signed-in accounts remain.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthWriteResult': + assert isinstance(obj, dict) + ok = from_bool(obj.get("ok")) + more_users = from_union([from_bool, from_none], obj.get("moreUsers")) + return AuthWriteResult(ok, more_users) + + def to_dict(self) -> dict: + result: dict = {} + result["ok"] = from_bool(self.ok) + if self.more_users is not None: + result["moreUsers"] = from_union([from_bool, from_none], self.more_users) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AutopilotObjectiveCreditLimit: @@ -1205,9 +1644,16 @@ class CatalogTrustTier(Enum): # Experimental: this type is part of an experimental API and may change or be removed. class CatalogAISkillInstallability(Enum): - """Typed non-installable state for an AI skill candidate""" + """Whether this AI skill candidate can be planned for verified installation in the selected + session. + Typed installability state for an AI skill candidate + """ + FEATURE_DISABLED = "feature-disabled" + INSTALLABLE = "installable" + MATERIALISATION_UNAVAILABLE = "materialisation-unavailable" NOT_INSTALLABLE_KIND = "not-installable-kind" + POLICY_FORBIDS = "policy-forbids" # Experimental: this type is part of an experimental API and may change or be removed. class CatalogAISkillCandidateKind(Enum): @@ -1243,16 +1689,22 @@ class CatalogAuthenticationRequiredReason(Enum): CREDENTIAL_REJECTED = "credential-rejected" NO_CREDENTIAL = "no-credential" -class CatalogCandidateInstallability(Enum): +class CatalogInstallability(Enum): """Whether this MCP server can be planned for installation, and if policy prevents it. Whether an MCP server candidate can be planned for installation - Typed non-installable state for an AI skill candidate + Whether this AI skill candidate can be planned for verified installation in the selected + session. + + Typed installability state for an AI skill candidate """ + FEATURE_DISABLED = "feature-disabled" INSTALLABLE = "installable" + MATERIALISATION_UNAVAILABLE = "materialisation-unavailable" NOT_INSTALLABLE_KIND = "not-installable-kind" NOT_INSTALLABLE_POLICY = "not-installable-policy" + POLICY_FORBIDS = "policy-forbids" # Experimental: this type is part of an experimental API and may change or be removed. class MCPServerCardEmbeddedKind(Enum): @@ -1276,46 +1728,17 @@ class CatalogCapability(Enum): AI_SKILL_DISCOVERY = "ai-skill-discovery" CATALOG_SEARCH_CREDENTIAL_REQUIRED = "catalog-search-credential-required" CATALOG_SEARCH_PAGINATION = "catalog-search-pagination" + CATALOG_SEARCH_SESSION_BOUND = "catalog-search-session-bound" CATALOG_SELECTION = "catalog-selection" LEGACY_MCP_SERVER_CARD = "legacy-mcp-server-card" + MCP_CONFIGURED_REMOTE_INSTALLATION = "mcp-configured-remote-installation" + MCP_CONFIRMED_REMOTE_INSTALLATION = "mcp-confirmed-remote-installation" MCP_INSTALL_PLANNING = "mcp-install-planning" MCP_SERVER_CARD = "mcp-server-card" MULTIPLE_TRANSPORT_CHOICE = "multiple-transport-choice" + SKILL_CONFIRMED_INSTALLATION = "skill-confirmed-installation" TRUST_SNAPSHOT = "trust-snapshot" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class CatalogClientContract: - """The protocol version and capability set a caller requires, supplied on every catalog - request so negotiation cannot be skipped by omission. - - Protocol version and capabilities the caller requires. - """ - protocol_version: int - """SDK protocol version the caller was generated against. A caller below the runtime's - minimum supported version is refused rather than served a partial result. - """ - required_capabilities: list[str] - """Wire features the caller requires the runtime to understand. Identifiers are bounded but - extensible so a newer caller can negotiate with an older runtime. Requiring an unknown - feature yields a typed refusal listing what is understood, never a partial grant. A grant - does not promise that a deployment has enabled the operation; typed unavailable results - report that separately. - """ - - @staticmethod - def from_dict(obj: Any) -> 'CatalogClientContract': - assert isinstance(obj, dict) - protocol_version = from_int(obj.get("protocolVersion")) - required_capabilities = from_list(from_str, obj.get("requiredCapabilities")) - return CatalogClientContract(protocol_version, required_capabilities) - - def to_dict(self) -> dict: - result: dict = {} - result["protocolVersion"] = from_int(self.protocol_version) - result["requiredCapabilities"] = from_list(from_str, self.required_capabilities) - return result - class CatalogContractViolationErrorKind(Enum): CONTRACT_VIOLATION = "contract-violation" @@ -1368,6 +1791,7 @@ class CatalogInvalidRequestField(Enum): LIMIT = "limit" OUTCOME = "outcome" PAGE = "page" + POLICY_SESSION_ID = "policySessionId" QUERY = "query" SCOPE = "scope" SELECTION_REF = "selectionRef" @@ -1393,7 +1817,7 @@ class CatalogMalformedCardReason(Enum): UNSUPPORTED_MEDIA_TYPE = "unsupported-media-type" # Experimental: this type is part of an experimental API and may change or be removed. -class CatalogMCPServerInstallabilityEnum(Enum): +class CatalogMCPServerInstallability(Enum): """Whether this MCP server can be planned for installation, and if policy prevents it. Whether an MCP server candidate can be planned for installation @@ -1486,9 +1910,11 @@ class MCPPlanPolicySource(Enum): # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CatalogSearchPage: - """An explicit numbered-page request. The SDK treats the token as opaque; only the runtime - decodes it and changes its targetPage. Authority validation binds navigation to the - original search. No snapshot stability or token TTL is promised. + """An explicit numbered-page request. SDK consumers treat the token as opaque. For bound + search, the runtime unwraps an expiring owner-bound reference to the private authority + token; only the runtime changes the authority token's targetPage. Legacy unbound + navigation keeps its authority-issued token semantics. No snapshot stability is + promised. Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh @@ -1500,8 +1926,9 @@ class CatalogSearchPage: available pages beyond that signed pageCount. """ token: str - """Opaque authority-issued pagination token from an earlier response. Never decode, modify - or log it in an SDK consumer. + """Opaque pagination token from an earlier response, owner-bound when session-bound search + was requested. Never decode, modify or log it in an SDK consumer. Expired or foreign + bound references require a fresh bound search, not a legacy retry. """ @staticmethod @@ -2222,6 +2649,8 @@ class ConnectorCatalogStatus(Enum): """Current authoritative service connection state. Authoritative service connection state for one Connector. + + Status. """ CONNECTED = "connected" ERROR = "error" @@ -2309,6 +2738,25 @@ def to_dict(self) -> dict: result["pollIntervalMs"] = from_int(self.poll_interval_ms) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorDiscoveryAccountRequest: + """Selected account.""" + + account_id: str + """Opaque account ID.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDiscoveryAccountRequest': + assert isinstance(obj, dict) + account_id = from_str(obj.get("accountId")) + return ConnectorDiscoveryAccountRequest(account_id) + + def to_dict(self) -> dict: + result: dict = {} + result["accountId"] = from_str(self.account_id) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ConnectorReconcileRequest: @@ -2322,17 +2770,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 @@ -2731,6 +3184,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 @@ -2745,8 +3203,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 = {} @@ -2760,10 +3219,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. @@ -3089,6 +3593,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: @@ -3118,6 +3758,71 @@ def to_dict(self) -> dict: class EnvAuthInfoType(Enum): ENV = "env" +# Experimental: this type is part of an experimental API and may change or be removed. +class EnvironmentKind(Enum): + """GitHub Mission Control compute kind. + + Compute kind reported by Mission Control. + + Restrict discovery to this compute kind. + """ + MANAGED_ACTIONS = "managed-actions" + MANAGED_CCA = "managed-cca" + MANAGED_SANDBOX = "managed-sandbox" + USER_CODESPACE = "user-codespace" + USER_LOCAL = "user-local" + +# 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.""" + + 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): """Agent-scope filter: 'primary' returns only main-agent events plus events whose type @@ -3393,874 +4098,532 @@ class KindEnum(Enum): # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryAbortRequest: - """Parameters for cooperatively aborting a factory body.""" - - execution_token: str - """Opaque token identifying the execution attempt to abort.""" - - run_id: str - """Factory run identifier.""" +class FleetStartRequest: + """Parameters for starting fleet orchestration: an optional user prompt combined with the + fleet instructions, plus the send options forwarded to the resulting turn. + """ + attachments: list[Attachment] | None = None + """Optional attachments (files, directories, selections, blobs, GitHub references) to + include with the fleet request + """ + # Internal: this field is an internal SDK API and is not part of the public surface. + billable: bool | None = None + """If false, this request will not trigger a Premium Request Unit charge. User requests + default to billable. + """ + prompt: str | None = None + """Optional user prompt to combine with fleet instructions""" - session_id: str - """Target session identifier""" + wait: bool | None = None + """If true, await completion of the agentic loop for this fleet request before returning. + Defaults to false. + """ @staticmethod - def from_dict(obj: Any) -> 'FactoryAbortRequest': + def from_dict(obj: Any) -> 'FleetStartRequest': assert isinstance(obj, dict) - execution_token = from_str(obj.get("executionToken")) - run_id = from_str(obj.get("runId")) - session_id = from_str(obj.get("sessionId")) - return FactoryAbortRequest(execution_token, run_id, session_id) + attachments = from_union([lambda x: from_list(_load_Attachment, x), from_none], obj.get("attachments")) + billable = from_union([from_bool, from_none], obj.get("billable")) + prompt = from_union([from_str, from_none], obj.get("prompt")) + wait = from_union([from_bool, from_none], obj.get("wait")) + return FleetStartRequest(attachments, billable, prompt, wait) def to_dict(self) -> dict: result: dict = {} - result["executionToken"] = from_str(self.execution_token) - result["runId"] = from_str(self.run_id) - result["sessionId"] = from_str(self.session_id) + if self.attachments is not None: + result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(Attachment, x), x), from_none], self.attachments) + if self.billable is not None: + result["billable"] = from_union([from_bool, from_none], self.billable) + if self.prompt is not None: + result["prompt"] = from_union([from_str, from_none], self.prompt) + if self.wait is not None: + result["wait"] = from_union([from_bool, from_none], self.wait) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryACKResult: - """Acknowledgement that a factory request was accepted.""" +class FleetStartResult: + """Indicates whether fleet mode was successfully activated.""" + + started: bool + """Whether fleet mode was successfully activated""" + @staticmethod - def from_dict(obj: Any) -> 'FactoryACKResult': + def from_dict(obj: Any) -> 'FleetStartResult': assert isinstance(obj, dict) - return FactoryACKResult() + started = from_bool(obj.get("started")) + return FleetStartResult(started) def to_dict(self) -> dict: result: dict = {} + result["started"] = from_bool(self.started) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryAgentOptions: - """Options for one factory-scoped subagent call. - - Subagent execution options. - """ - agent: str | None = None - """Optional built-in or custom agent name whose definition configures the subagent.""" - - context_tier: ContextTier | None = None - """Optional context tier override for the subagent.""" - - label: str | None = None - """Optional label distinguishing otherwise identical memoized agent calls.""" - - model: str | None = None - """Optional model identifier for the subagent.""" - - reasoning_effort: str | None = None - """Optional reasoning effort override for the subagent.""" +class FolderTrustAddParams: + """Folder path to add to trusted folders.""" - schema: Any = None - """Optional JSON Schema for structured agent output.""" + path: str + """Folder path to mark as trusted""" @staticmethod - def from_dict(obj: Any) -> 'FactoryAgentOptions': + def from_dict(obj: Any) -> 'FolderTrustAddParams': assert isinstance(obj, dict) - agent = from_union([from_str, from_none], obj.get("agent")) - context_tier = from_union([ContextTier, from_none], obj.get("contextTier")) - label = from_union([from_str, from_none], obj.get("label")) - model = from_union([from_str, from_none], obj.get("model")) - reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) - schema = obj.get("schema") - return FactoryAgentOptions(agent, context_tier, label, model, reasoning_effort, schema) + path = from_str(obj.get("path")) + return FolderTrustAddParams(path) def to_dict(self) -> dict: result: dict = {} - if self.agent is not None: - result["agent"] = from_union([from_str, from_none], self.agent) - if self.context_tier is not None: - result["contextTier"] = from_union([lambda x: to_enum(ContextTier, x), from_none], self.context_tier) - if self.label is not None: - result["label"] = from_union([from_str, from_none], self.label) - if self.model is not None: - result["model"] = from_union([from_str, from_none], self.model) - if self.reasoning_effort is not None: - result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) - if self.schema is not None: - result["schema"] = self.schema + 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 FactoryAgentResult: - """Result of one factory-scoped subagent call.""" +class FolderTrustCheckParams: + """Folder path to check for trust.""" - result: Any = None - """Agent result, omitted when the agent produced no result.""" + path: str + """Folder path to check""" @staticmethod - def from_dict(obj: Any) -> 'FactoryAgentResult': + def from_dict(obj: Any) -> 'FolderTrustCheckParams': assert isinstance(obj, dict) - result = obj.get("result") - return FactoryAgentResult(result) + path = from_str(obj.get("path")) + return FolderTrustCheckParams(path) def to_dict(self) -> dict: result: dict = {} - if self.result is not None: - result["result"] = self.result + 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 FactoryCancelRequest: - """Parameters for cancelling a factory run.""" +class FolderTrustCheckResult: + """Folder trust check result.""" - run_id: str - """Factory run identifier.""" + trusted: bool + """Whether the folder is trusted""" @staticmethod - def from_dict(obj: Any) -> 'FactoryCancelRequest': + def from_dict(obj: Any) -> 'FolderTrustCheckResult': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - return FactoryCancelRequest(run_id) + trusted = from_bool(obj.get("trusted")) + return FolderTrustCheckResult(trusted) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) + result["trusted"] = from_bool(self.trusted) return result +class GhCLIAuthInfoType(Enum): + GH_CLI = "gh-cli" + # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryCurrentPhase: - """Current factory phase identity.""" - - id: str - """Current phase identifier.""" +class _GitCurrentBranchRemoteResult: + """The remote the checked-out branch tracks.""" - ordinal: int | None = None - """Zero-based declared phase ordinal, or null for an undeclared phase.""" + remote: str + """Name of the tracked remote. Reports `origin` whenever the working tree has no tracking + configuration to read, including on a detached HEAD, so this is never null and never + empty. + """ @staticmethod - def from_dict(obj: Any) -> 'FactoryCurrentPhase': + def from_dict(obj: Any) -> '_GitCurrentBranchRemoteResult': assert isinstance(obj, dict) - id = from_str(obj.get("id")) - ordinal = from_union([from_none, from_int], obj.get("ordinal")) - return FactoryCurrentPhase(id, ordinal) + remote = from_str(obj.get("remote")) + return _GitCurrentBranchRemoteResult(remote) def to_dict(self) -> dict: result: dict = {} - result["id"] = from_str(self.id) - result["ordinal"] = from_union([from_none, from_int], self.ordinal) + result["remote"] = from_str(self.remote) return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryDeclaredLimits: - """Declared or approved factory resource ceilings. - - Resource ceilings declared by the factory. - """ - max_ai_credits: float | None = None - """Maximum AI credits consumed by subagents and descendants.""" - - max_concurrent_subagents: int | None = None - """Maximum concurrently active subagents.""" +class _GitCwdRequest: + """Working-tree path a git query applies to.""" - max_total_subagents: int | None = None - """Maximum total subagents spawned by the run.""" - - timeout_seconds: float | None = None - """Maximum accumulated active execution time in seconds.""" + cwd: str + """Absolute path to a directory inside the git working tree to query.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryDeclaredLimits': + def from_dict(obj: Any) -> '_GitCwdRequest': assert isinstance(obj, dict) - max_ai_credits = from_union([from_float, from_none], obj.get("maxAiCredits")) - max_concurrent_subagents = from_union([from_int, from_none], obj.get("maxConcurrentSubagents")) - max_total_subagents = from_union([from_int, from_none], obj.get("maxTotalSubagents")) - timeout_seconds = from_union([from_float, from_none], obj.get("timeoutSeconds")) - return FactoryDeclaredLimits(max_ai_credits, max_concurrent_subagents, max_total_subagents, timeout_seconds) + cwd = from_str(obj.get("cwd")) + return _GitCwdRequest(cwd) def to_dict(self) -> dict: result: dict = {} - if self.max_ai_credits is not None: - result["maxAiCredits"] = from_union([to_float, from_none], self.max_ai_credits) - if self.max_concurrent_subagents is not None: - result["maxConcurrentSubagents"] = from_union([from_int, from_none], self.max_concurrent_subagents) - if self.max_total_subagents is not None: - result["maxTotalSubagents"] = from_union([from_int, from_none], self.max_total_subagents) - if self.timeout_seconds is not None: - result["timeoutSeconds"] = from_union([to_float, from_none], self.timeout_seconds) + result["cwd"] = from_str(self.cwd) return result # Experimental: this type is part of an experimental API and may change or be removed. -class DurableOperation(Enum): - """Execution-critical factory storage operation. - - Execution-critical durable operation that failed. - - Execution-critical workflow storage operation. - """ - ADD_ELAPSED = "addElapsed" - CHARGE_CREDIT = "chargeCredit" - CREATE_RUN = "createRun" - FINISH_RUN = "finishRun" - JOURNAL_GET = "journalGet" - JOURNAL_PUT = "journalPut" - MARK_RUN_STARTED = "markRunStarted" - RECONCILE_CREDIT_TOTAL = "reconcileCreditTotal" - REFRESH_LEASE = "refreshLease" - RELEASE_AGENT = "releaseAgent" - RESERVE_AGENT = "reserveAgent" - -# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryExecuteRequest: - """Parameters sent to the owning extension to execute a factory closure.""" - - args: Any - """Factory input value.""" - - execution_token: str - """Opaque token identifying this factory execution attempt.""" - - name: str - """Registered factory name.""" - - run_id: str - """Factory run identifier.""" +class _GitHubOwnerOption: + """A GitHub login the authenticated user may act as: their own account, or an organization + they belong to. + """ + login: str + """The owner's GitHub login.""" - session_id: str - """Target session identifier""" + type: str + """Which kind of owner this is. The authenticated user's own account is always reported as + `user`. + """ @staticmethod - def from_dict(obj: Any) -> 'FactoryExecuteRequest': + def from_dict(obj: Any) -> '_GitHubOwnerOption': assert isinstance(obj, dict) - args = obj.get("args") - execution_token = from_str(obj.get("executionToken")) - name = from_str(obj.get("name")) - run_id = from_str(obj.get("runId")) - session_id = from_str(obj.get("sessionId")) - return FactoryExecuteRequest(args, execution_token, name, run_id, session_id) + login = from_str(obj.get("login")) + type = from_str(obj.get("type")) + return _GitHubOwnerOption(login, type) def to_dict(self) -> dict: result: dict = {} - result["args"] = self.args - result["executionToken"] = from_str(self.execution_token) - result["name"] = from_str(self.name) - result["runId"] = from_str(self.run_id) - result["sessionId"] = from_str(self.session_id) + result["login"] = from_str(self.login) + result["type"] = from_str(self.type) return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryExecuteResult: - """Result returned by an extension factory closure.""" +class _GitHubOwnersCancelRequest: + """The owner listing to abandon.""" - result: Any = None - """Factory result value.""" + request_id: int + """Request id the listing was started with.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryExecuteResult': + def from_dict(obj: Any) -> '_GitHubOwnersCancelRequest': assert isinstance(obj, dict) - result = obj.get("result") - return FactoryExecuteResult(result) + request_id = from_int(obj.get("requestId")) + return _GitHubOwnersCancelRequest(request_id) def to_dict(self) -> dict: result: dict = {} - if self.result is not None: - result["result"] = self.result + result["requestId"] = from_int(self.request_id) return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryGetRunProgressRequest: - """Parameters for paging factory progress.""" +class _GitHubOwnersCancelResult: + """Whether the id named a running owner listing.""" - run_id: str - """Factory run identifier.""" - - after_seq: int | None = None - """Exclusive forward cursor.""" - - before_seq: int | None = None - """Exclusive backward cursor.""" - - limit: int | None = None - """Maximum records to return. Defaults to 200 and is capped at 500.""" - - phase_id: str | None = None - """Optional phase identifier used to scope records and cursors.""" + canceled: bool + """True when a listing with the id was running and the cancel stopped it. False when the id + was never registered, was registered but unused, was released after being abandoned, or + its listing had ended. An unused id is released and cannot start a later listing. + """ @staticmethod - def from_dict(obj: Any) -> 'FactoryGetRunProgressRequest': + def from_dict(obj: Any) -> '_GitHubOwnersCancelResult': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - after_seq = from_union([from_int, from_none], obj.get("afterSeq")) - before_seq = from_union([from_int, from_none], obj.get("beforeSeq")) - limit = from_union([from_int, from_none], obj.get("limit")) - phase_id = from_union([from_str, from_none], obj.get("phaseId")) - return FactoryGetRunProgressRequest(run_id, after_seq, before_seq, limit, phase_id) + canceled = from_bool(obj.get("canceled")) + return _GitHubOwnersCancelResult(canceled) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) - if self.after_seq is not None: - result["afterSeq"] = from_union([from_int, from_none], self.after_seq) - if self.before_seq is not None: - result["beforeSeq"] = from_union([from_int, from_none], self.before_seq) - if self.limit is not None: - result["limit"] = from_union([from_int, from_none], self.limit) - if self.phase_id is not None: - result["phaseId"] = from_union([from_str, from_none], self.phase_id) + result["canceled"] = from_bool(self.canceled) return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryGetRunRequest: - """Parameters for retrieving a factory run.""" +class _GitHubOwnersListRequest: + """Credential to list owners under, and the request id that makes the listing cancellable.""" - run_id: str - """Factory run identifier.""" + auth_info: Any + """The credential the listing runs under, carried opaquely because its shape is the host's + own and the runtime only resolves a token and a GitHub host from it. No credential + travels: this selects one the runtime already holds. + """ + request_id: int + """Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled + before use, released after being abandoned, or already used is refused rather than + silently running uncancellable. + """ @staticmethod - def from_dict(obj: Any) -> 'FactoryGetRunRequest': + def from_dict(obj: Any) -> '_GitHubOwnersListRequest': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - return FactoryGetRunRequest(run_id) + auth_info = obj.get("authInfo") + request_id = from_int(obj.get("requestId")) + return _GitHubOwnersListRequest(auth_info, request_id) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) + result["authInfo"] = self.auth_info + result["requestId"] = from_int(self.request_id) return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryJournalGetRequest: - """Parameters for reading a factory journal entry.""" - - execution_token: str - """Opaque token identifying the current factory execution attempt.""" - - key: str - """Namespaced journal key.""" - - run_id: str - """Factory run identifier.""" +class GitHubOwnersRequestIDResult: + """A freshly registered request id. Registering it before the listing starts is what lets a + cancel that races the request still find the owner listing slot. The id serves one + listing only. Long-abandoned unused ids can be released by later allocations. + """ + request_id: int + """Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryJournalGetRequest': + def from_dict(obj: Any) -> 'GitHubOwnersRequestIDResult': assert isinstance(obj, dict) - execution_token = from_str(obj.get("executionToken")) - key = from_str(obj.get("key")) - run_id = from_str(obj.get("runId")) - return FactoryJournalGetRequest(execution_token, key, run_id) + request_id = from_int(obj.get("requestId")) + return GitHubOwnersRequestIDResult(request_id) def to_dict(self) -> dict: result: dict = {} - result["executionToken"] = from_str(self.execution_token) - result["key"] = from_str(self.key) - result["runId"] = from_str(self.run_id) + result["requestId"] = from_int(self.request_id) return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryJournalGetResult: - """Result of reading a factory journal entry.""" +class _GitHubRepositoryAtPathRequest: + """Working-tree path whose owning GitHub repository should be resolved.""" - hit: bool - """Whether the journal contained the requested key.""" - - result_json: Any = None - """Cached JSON result. The hit field distinguishes a cached JSON null from a miss.""" + path: str + """Absolute path to a directory inside the git working tree to resolve.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryJournalGetResult': + def from_dict(obj: Any) -> '_GitHubRepositoryAtPathRequest': assert isinstance(obj, dict) - hit = from_bool(obj.get("hit")) - result_json = obj.get("resultJson") - return FactoryJournalGetResult(hit, result_json) + path = from_str(obj.get("path")) + return _GitHubRepositoryAtPathRequest(path) def to_dict(self) -> dict: result: dict = {} - result["hit"] = from_bool(self.hit) - if self.result_json is not None: - result["resultJson"] = self.result_json + 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 FactoryJournalPutRequest: - """Parameters for storing a factory journal entry.""" +class GitHubTelemetryClientInfo: + """Client environment metadata describing the process that produced a telemetry event. - execution_token: str - """Opaque token identifying the current factory execution attempt.""" + Client environment metadata. + """ + cli_version: str + """Copilot CLI version string.""" - key: str - """Namespaced journal key.""" + node_version: str + """Node.js runtime version string.""" - result_json: Any - """JSON result to memoize.""" + os_arch: str + """Operating system architecture (e.g. arm64, x64).""" - run_id: str - """Factory run identifier.""" + os_platform: str + """Operating system platform (e.g. darwin, linux, win32).""" - @staticmethod - def from_dict(obj: Any) -> 'FactoryJournalPutRequest': - assert isinstance(obj, dict) - execution_token = from_str(obj.get("executionToken")) - key = from_str(obj.get("key")) - result_json = obj.get("resultJson") - run_id = from_str(obj.get("runId")) - return FactoryJournalPutRequest(execution_token, key, result_json, run_id) + os_version: str + """Operating system version string.""" - def to_dict(self) -> dict: - result: dict = {} - result["executionToken"] = from_str(self.execution_token) - result["key"] = from_str(self.key) - result["resultJson"] = self.result_json - result["runId"] = from_str(self.run_id) - return result + client_name: str | None = None + """Name of the client application.""" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryListRunsRequest: - """Parameters for paging factory runs.""" + client_type: str | None = None + """Type of client.""" - after_seq: int | None = None - """Exclusive forward cursor.""" + copilot_plan: str | None = None + """Copilot subscription plan, when known.""" - before_seq: int | None = None - """Exclusive backward cursor.""" + cpu_count: int | None = None + """Number of logical CPU cores on the host.""" - limit: int | None = None - """Maximum terminal runs to return. Defaults to 200 and is capped at 500.""" + cpu_model: str | None = None + """Distinct CPU model names for the host, comma-separated.""" + + dev_device_id: str | None = None + """Stable machine identifier for the device.""" + + is_staff: bool | None = None + """Whether the user is a GitHub/Microsoft staff member.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryListRunsRequest': + def from_dict(obj: Any) -> 'GitHubTelemetryClientInfo': assert isinstance(obj, dict) - after_seq = from_union([from_int, from_none], obj.get("afterSeq")) - before_seq = from_union([from_int, from_none], obj.get("beforeSeq")) - limit = from_union([from_int, from_none], obj.get("limit")) - return FactoryListRunsRequest(after_seq, before_seq, limit) + cli_version = from_str(obj.get("cli_version")) + node_version = from_str(obj.get("node_version")) + os_arch = from_str(obj.get("os_arch")) + os_platform = from_str(obj.get("os_platform")) + os_version = from_str(obj.get("os_version")) + client_name = from_union([from_str, from_none], obj.get("client_name")) + client_type = from_union([from_str, from_none], obj.get("client_type")) + copilot_plan = from_union([from_str, from_none], obj.get("copilot_plan")) + cpu_count = from_union([from_int, from_none], obj.get("cpu_count")) + cpu_model = from_union([from_str, from_none], obj.get("cpu_model")) + dev_device_id = from_union([from_str, from_none], obj.get("dev_device_id")) + is_staff = from_union([from_bool, from_none], obj.get("is_staff")) + return GitHubTelemetryClientInfo(cli_version, node_version, os_arch, os_platform, os_version, client_name, client_type, copilot_plan, cpu_count, cpu_model, dev_device_id, is_staff) def to_dict(self) -> dict: result: dict = {} - if self.after_seq is not None: - result["afterSeq"] = from_union([from_int, from_none], self.after_seq) - if self.before_seq is not None: - result["beforeSeq"] = from_union([from_int, from_none], self.before_seq) - if self.limit is not None: - result["limit"] = from_union([from_int, from_none], self.limit) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryRunConsumed: - """Durable resource consumption. - - Durable factory resource consumption. - """ - active_ms: int - """Accumulated active execution time in milliseconds.""" - - nano_aiu: int - """AI usage consumed by the run in nano-AIU.""" - - subagents: int - """Total subagents spawned by the run.""" - - @staticmethod - def from_dict(obj: Any) -> 'FactoryRunConsumed': - assert isinstance(obj, dict) - active_ms = from_int(obj.get("activeMs")) - nano_aiu = from_int(obj.get("nanoAiu")) - subagents = from_int(obj.get("subagents")) - return FactoryRunConsumed(active_ms, nano_aiu, subagents) - - def to_dict(self) -> dict: - result: dict = {} - result["activeMs"] = from_int(self.active_ms) - result["nanoAiu"] = from_int(self.nano_aiu) - result["subagents"] = from_int(self.subagents) + result["cli_version"] = from_str(self.cli_version) + result["node_version"] = from_str(self.node_version) + result["os_arch"] = from_str(self.os_arch) + result["os_platform"] = from_str(self.os_platform) + result["os_version"] = from_str(self.os_version) + if self.client_name is not None: + result["client_name"] = from_union([from_str, from_none], self.client_name) + if self.client_type is not None: + result["client_type"] = from_union([from_str, from_none], self.client_type) + if self.copilot_plan is not None: + result["copilot_plan"] = from_union([from_str, from_none], self.copilot_plan) + if self.cpu_count is not None: + result["cpu_count"] = from_union([from_int, from_none], self.cpu_count) + if self.cpu_model is not None: + result["cpu_model"] = from_union([from_str, from_none], self.cpu_model) + if self.dev_device_id is not None: + result["dev_device_id"] = from_union([from_str, from_none], self.dev_device_id) + if self.is_staff is not None: + result["is_staff"] = from_union([from_bool, from_none], self.is_staff) return result -# Experimental: this type is part of an experimental API and may change or be removed. -class RunStatus(Enum): - """Current factory run status. - - Current or terminal state of a factory run. - - Current or terminal factory run status. - - Current workflow run status. +class GitHubTokenAcquireReason(Enum): + """Why the runtime is requesting a GitHub credential.""" - Current or terminal state of a workflow run. + INITIAL = "initial" + REFRESH = "refresh" - Current or terminal workflow run status. - """ +class GitHubTokenAcquireResultKind(Enum): CANCELLED = "cancelled" - COMPLETED = "completed" - ERROR = "error" - HALTED = "halted" - PAUSED = "paused" - PENDING = "pending" - RUNNING = "running" + TOKEN = "token" # Experimental: this type is part of an experimental API and may change or be removed. -class RunFailureKind(Enum): - """Resource ceiling that stopped the run. - - Cumulative resource ceiling that stopped a factory run. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _GitRemoteRepository: + """A GitHub repository one of a working tree's remotes points at.""" - Cumulative resource ceiling that stopped a workflow run. + host: str + """GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise + remote. """ - MAX_AI_CREDITS = "maxAiCredits" - MAX_TOTAL_SUBAGENTS = "maxTotalSubagents" - TIMEOUT_SECONDS = "timeoutSeconds" - -class FactoryRunFailureType(Enum): - FACTORY_ACCOUNTING_INCOMPLETE = "factory_accounting_incomplete" - FACTORY_DURABLE_FAILURE = "factory_durable_failure" - FACTORY_LIMIT_REACHED = "factory_limit_reached" - FACTORY_PROVIDER_DISCONNECTED = "factory_provider_disconnected" - FACTORY_RESUME_DECLINED = "factory_resume_declined" - -class PauseInfoType(Enum): - CHECKPOINT = "checkpoint" - USER = "user" - -# Experimental: this type is part of an experimental API and may change or be removed. -class LogLineKind(Enum): - """Progress line kind. - - Kind of factory progress line. + name: str + """Repository name, without the owner.""" - Progress record kind. + owner: str + """Account or organization owning the repository.""" - Kind of workflow progress line. + remote_name: str + """Name of the first remote that produced this distinct repository entry, such as `origin` + or `upstream`. """ - LOG = "log" - PHASE = "phase" - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryPauseCheckpointRequest: - """Parameters for an owned durable pause checkpoint.""" - - execution_token: str - """Opaque token identifying the execution attempt that reached the checkpoint.""" - - key: str - """Stable author-defined checkpoint key.""" - - run_id: str - """Factory run identifier.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryPauseCheckpointRequest': + def from_dict(obj: Any) -> '_GitRemoteRepository': assert isinstance(obj, dict) - execution_token = from_str(obj.get("executionToken")) - key = from_str(obj.get("key")) - run_id = from_str(obj.get("runId")) - return FactoryPauseCheckpointRequest(execution_token, key, run_id) + host = from_str(obj.get("host")) + name = from_str(obj.get("name")) + owner = from_str(obj.get("owner")) + remote_name = from_str(obj.get("remoteName")) + return _GitRemoteRepository(host, name, owner, remote_name) def to_dict(self) -> dict: result: dict = {} - result["executionToken"] = from_str(self.execution_token) - result["key"] = from_str(self.key) - result["runId"] = from_str(self.run_id) + result["host"] = from_str(self.host) + result["name"] = from_str(self.name) + result["owner"] = from_str(self.owner) + result["remoteName"] = from_str(self.remote_name) return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryPauseRequest: - """Parameters for pausing a running factory.""" +class _GitReposFromRemotesRequest: + """Git working tree whose GitHub remotes should be listed.""" - run_id: str - """Factory run identifier.""" + git_root: str + """Absolute path to the root of the git working tree.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryPauseRequest': + def from_dict(obj: Any) -> '_GitReposFromRemotesRequest': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - return FactoryPauseRequest(run_id) + git_root = from_str(obj.get("gitRoot")) + return _GitReposFromRemotesRequest(git_root) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) + result["gitRoot"] = from_str(self.git_root) return result # Experimental: this type is part of an experimental API and may change or be removed. -class PhaseStatus(Enum): - """Derived lifecycle state of the phase. - - Derived lifecycle state of a factory phase. - - Derived lifecycle state of a workflow phase. - """ - ACTIVE = "active" - COMPLETED = "completed" - PENDING = "pending" - SKIPPED = "skipped" - -# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryRunLimits: - """Optional per-invocation resource ceiling overrides. - - Wire-only per-invocation factory resource ceiling overrides. - - Per-invocation resource ceiling overrides. - """ - max_ai_credits: float | None = None - """Maximum AI credits consumed by factory subagents and their descendants. The post-paid - ceiling is soft: parallel turns can settle beyond it before the run stops. - """ - max_concurrent_subagents: int | None = None - """Maximum number of factory subagents that may run concurrently.""" - - max_total_subagents: int | None = None - """Maximum total number of factory subagents that may be admitted.""" +class _GlobalStateLoadForConfigDirRequest: + """Selects the configuration directory whose machine-wide state to read.""" - timeout_seconds: float | None = None - """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. + config_dir: str | None = None + """Copilot configuration directory to read the state document from, taking precedence over + the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + read the directory the server resolved for itself. """ @staticmethod - def from_dict(obj: Any) -> 'FactoryRunLimits': + def from_dict(obj: Any) -> '_GlobalStateLoadForConfigDirRequest': assert isinstance(obj, dict) - max_ai_credits = from_union([from_float, from_none], obj.get("maxAiCredits")) - max_concurrent_subagents = from_union([from_none, from_int], obj.get("maxConcurrentSubagents")) - max_total_subagents = from_union([from_none, from_int], obj.get("maxTotalSubagents")) - timeout_seconds = from_union([from_none, from_float], obj.get("timeoutSeconds")) - return FactoryRunLimits(max_ai_credits, max_concurrent_subagents, max_total_subagents, timeout_seconds) + config_dir = from_union([from_str, from_none], obj.get("configDir")) + return _GlobalStateLoadForConfigDirRequest(config_dir) def to_dict(self) -> dict: result: dict = {} - if self.max_ai_credits is not None: - result["maxAiCredits"] = from_union([to_float, from_none], self.max_ai_credits) - if self.max_concurrent_subagents is not None: - result["maxConcurrentSubagents"] = from_union([from_none, from_int], self.max_concurrent_subagents) - if self.max_total_subagents is not None: - result["maxTotalSubagents"] = from_union([from_none, from_int], self.max_total_subagents) - if self.timeout_seconds is not None: - result["timeoutSeconds"] = from_union([from_none, to_float], self.timeout_seconds) + if self.config_dir is not None: + result["configDir"] = from_union([from_str, from_none], self.config_dir) return result +class InstalledPluginSourceURLSource(Enum): + GITHUB = "github" + LOCAL = "local" + URL = "url" + # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FleetStartRequest: - """Parameters for starting fleet orchestration: an optional user prompt combined with the - fleet instructions, plus the send options forwarded to the resulting turn. - """ - attachments: list[Attachment] | None = None - """Optional attachments (files, directories, selections, blobs, GitHub references) to - include with the fleet request - """ - # Internal: this field is an internal SDK API and is not part of the public surface. - billable: bool | None = None - """If false, this request will not trigger a Premium Request Unit charge. User requests - default to billable. +class _GlobalStateWriteKeyRequest: + """A single top-level key to record in the host's machine-wide state. The write replaces + only that key and leaves the rest of the document untouched, so two writers recording + different one-off flags do not overwrite each other. The stored credential keys cannot be + written through this method. """ - prompt: str | None = None - """Optional user prompt to combine with fleet instructions""" - - wait: bool | None = None - """If true, await completion of the agentic loop for this fleet request before returning. - Defaults to false. + key: str + """Top-level key to write, named as it appears in the result of `globalState.load`. It must + be one of the writable keys that `globalState.writeKey` lists. """ - - @staticmethod - def from_dict(obj: Any) -> 'FleetStartRequest': - assert isinstance(obj, dict) - attachments = from_union([lambda x: from_list(_load_Attachment, x), from_none], obj.get("attachments")) - billable = from_union([from_bool, from_none], obj.get("billable")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - wait = from_union([from_bool, from_none], obj.get("wait")) - return FleetStartRequest(attachments, billable, prompt, wait) - - def to_dict(self) -> dict: - result: dict = {} - if self.attachments is not None: - result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(Attachment, x), x), from_none], self.attachments) - if self.billable is not None: - result["billable"] = from_union([from_bool, from_none], self.billable) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) - if self.wait is not None: - result["wait"] = from_union([from_bool, from_none], self.wait) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FleetStartResult: - """Indicates whether fleet mode was successfully activated.""" - - started: bool - """Whether fleet mode was successfully activated""" - - @staticmethod - def from_dict(obj: Any) -> 'FleetStartResult': - assert isinstance(obj, dict) - started = from_bool(obj.get("started")) - return FleetStartResult(started) - - def to_dict(self) -> dict: - result: dict = {} - result["started"] = from_bool(self.started) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FolderTrustAddParams: - """Folder path to add to trusted folders.""" - - path: str - """Folder path to mark as trusted""" - - @staticmethod - def from_dict(obj: Any) -> 'FolderTrustAddParams': - assert isinstance(obj, dict) - path = from_str(obj.get("path")) - return FolderTrustAddParams(path) - - def to_dict(self) -> dict: - result: dict = {} - 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 FolderTrustCheckParams: - """Folder path to check for trust.""" - - path: str - """Folder path to check""" - - @staticmethod - def from_dict(obj: Any) -> 'FolderTrustCheckParams': - assert isinstance(obj, dict) - path = from_str(obj.get("path")) - return FolderTrustCheckParams(path) - - def to_dict(self) -> dict: - result: dict = {} - 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 FolderTrustCheckResult: - """Folder trust check result.""" - - trusted: bool - """Whether the folder is trusted""" - - @staticmethod - def from_dict(obj: Any) -> 'FolderTrustCheckResult': - assert isinstance(obj, dict) - trusted = from_bool(obj.get("trusted")) - return FolderTrustCheckResult(trusted) - - def to_dict(self) -> dict: - result: dict = {} - result["trusted"] = from_bool(self.trusted) - return result - -class GhCLIAuthInfoType(Enum): - GH_CLI = "gh-cli" - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class GitHubTelemetryClientInfo: - """Client environment metadata describing the process that produced a telemetry event. - - Client environment metadata. + config_dir: str | None = None + """Copilot configuration directory to write the state document in, taking precedence over + the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + write the directory the server resolved for itself. Mirrors + `globalState.loadForConfigDir`, so a caller can read and write the same directory. """ - cli_version: str - """Copilot CLI version string.""" - - node_version: str - """Node.js runtime version string.""" - - os_arch: str - """Operating system architecture (e.g. arm64, x64).""" - - os_platform: str - """Operating system platform (e.g. darwin, linux, win32).""" - - os_version: str - """Operating system version string.""" - - client_name: str | None = None - """Name of the client application.""" - - client_type: str | None = None - """Type of client.""" - - copilot_plan: str | None = None - """Copilot subscription plan, when known.""" - - cpu_count: int | None = None - """Number of logical CPU cores on the host.""" - - cpu_model: str | None = None - """Distinct CPU model names for the host, comma-separated.""" - - dev_device_id: str | None = None - """Stable machine identifier for the device.""" - - is_staff: bool | None = None - """Whether the user is a GitHub/Microsoft staff member.""" + value: Any = None + """Value to store for the key. Omit it, or pass null, to remove the key instead.""" @staticmethod - def from_dict(obj: Any) -> 'GitHubTelemetryClientInfo': + def from_dict(obj: Any) -> '_GlobalStateWriteKeyRequest': assert isinstance(obj, dict) - cli_version = from_str(obj.get("cli_version")) - node_version = from_str(obj.get("node_version")) - os_arch = from_str(obj.get("os_arch")) - os_platform = from_str(obj.get("os_platform")) - os_version = from_str(obj.get("os_version")) - client_name = from_union([from_str, from_none], obj.get("client_name")) - client_type = from_union([from_str, from_none], obj.get("client_type")) - copilot_plan = from_union([from_str, from_none], obj.get("copilot_plan")) - cpu_count = from_union([from_int, from_none], obj.get("cpu_count")) - cpu_model = from_union([from_str, from_none], obj.get("cpu_model")) - dev_device_id = from_union([from_str, from_none], obj.get("dev_device_id")) - is_staff = from_union([from_bool, from_none], obj.get("is_staff")) - return GitHubTelemetryClientInfo(cli_version, node_version, os_arch, os_platform, os_version, client_name, client_type, copilot_plan, cpu_count, cpu_model, dev_device_id, is_staff) + key = from_str(obj.get("key")) + config_dir = from_union([from_str, from_none], obj.get("configDir")) + value = obj.get("value") + return _GlobalStateWriteKeyRequest(key, config_dir, value) def to_dict(self) -> dict: result: dict = {} - result["cli_version"] = from_str(self.cli_version) - result["node_version"] = from_str(self.node_version) - result["os_arch"] = from_str(self.os_arch) - result["os_platform"] = from_str(self.os_platform) - result["os_version"] = from_str(self.os_version) - if self.client_name is not None: - result["client_name"] = from_union([from_str, from_none], self.client_name) - if self.client_type is not None: - result["client_type"] = from_union([from_str, from_none], self.client_type) - if self.copilot_plan is not None: - result["copilot_plan"] = from_union([from_str, from_none], self.copilot_plan) - if self.cpu_count is not None: - result["cpu_count"] = from_union([from_int, from_none], self.cpu_count) - if self.cpu_model is not None: - result["cpu_model"] = from_union([from_str, from_none], self.cpu_model) - if self.dev_device_id is not None: - result["dev_device_id"] = from_union([from_str, from_none], self.dev_device_id) - if self.is_staff is not None: - result["is_staff"] = from_union([from_bool, from_none], self.is_staff) + result["key"] = from_str(self.key) + if self.config_dir is not None: + result["configDir"] = from_union([from_str, from_none], self.config_dir) + if self.value is not None: + result["value"] = self.value return result -class GitHubTokenAcquireReason(Enum): - """Why the runtime is requesting a GitHub credential.""" - - INITIAL = "initial" - REFRESH = "refresh" - -class GitHubTokenAcquireResultKind(Enum): - CANCELLED = "cancelled" - TOKEN = "token" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class HandlePendingToolCallResult: @@ -4697,669 +5060,1624 @@ def to_dict(self) -> dict: result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) return result -class InstalledPluginSourceURLSource(Enum): - GITHUB = "github" - LOCAL = "local" - URL = "url" - -class PurpleSource(Enum): - GITHUB = "github" - -class FluffySource(Enum): - LOCAL = "local" - # Experimental: this type is part of an experimental API and may change or be removed. -class InstructionLocation(Enum): - """Which tier this target belongs to +@dataclass +class HostGitHubEnvironmentOptions: + """Requested GitHub Mission Control registration. - Where this source lives — used for UI grouping + GitHub Mission Control registration options. The compute ID is application-owned and + stable. + + Registers a GitHub Mission Control environment and enables its relay transport. """ - PLUGIN = "plugin" - REPOSITORY = "repository" - USER = "user" - WORKING_DIRECTORY = "working-directory" + compute_id: str + """Stable application installation identity, reused across host restarts.""" -# Experimental: this type is part of an experimental API and may change or be removed. -class InstructionSourceType(Enum): - """Category of instruction source — used for merge logic""" + name: str + """Human-readable environment display name.""" - CHILD_INSTRUCTIONS = "child-instructions" - HOME = "home" - MODEL = "model" - NESTED_AGENTS = "nested-agents" - PLUGIN = "plugin" - REPO = "repo" - VSCODE = "vscode" + @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 InstructionsDiscoverRequest: - """Optional project paths to include in instruction discovery.""" +class HostLocalServerConfiguration: + """Normalized local listener settings, absent for relay-only hosts. - exclude_host_instructions: bool | None = None - """When true, omit the host's instruction sources (user/home-level files and plugin rules), - leaving only repository and working-directory sources. For multitenant deployments. - """ - project_paths: list[str] | None = None - """Optional list of project directory paths to scan for repository/working-directory - instruction sources. When omitted or empty, only user-level and plugin instruction - sources are returned (no project scan). + Normalized local WebSocket listener settings. """ + hostname: str + """Hostname or IP address to bind.""" + + port: int + """Port to bind, with zero requesting OS allocation.""" + + require_connection_token: bool + """Whether the listener requires token authentication.""" + + token: str | None = None + """Secret connection token, absent when authentication is disabled.""" @staticmethod - def from_dict(obj: Any) -> 'InstructionsDiscoverRequest': + def from_dict(obj: Any) -> 'HostLocalServerConfiguration': assert isinstance(obj, dict) - exclude_host_instructions = from_union([from_bool, from_none], obj.get("excludeHostInstructions")) - project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths")) - return InstructionsDiscoverRequest(exclude_host_instructions, project_paths) + hostname = from_str(obj.get("hostname")) + port = from_int(obj.get("port")) + require_connection_token = from_bool(obj.get("requireConnectionToken")) + token = from_union([from_str, from_none], obj.get("token")) + return HostLocalServerConfiguration(hostname, port, require_connection_token, token) def to_dict(self) -> dict: result: dict = {} - if self.exclude_host_instructions is not None: - result["excludeHostInstructions"] = from_union([from_bool, from_none], self.exclude_host_instructions) - if self.project_paths is not None: - result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) + result["hostname"] = from_str(self.hostname) + result["port"] = from_int(self.port) + result["requireConnectionToken"] = from_bool(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 InstructionsGetDiscoveryPathsRequest: - """Optional project paths to include when enumerating instruction discovery targets.""" +class HostDisposeRequest: + """Stops a connection-owned listener and joins its teardown.""" - exclude_host_instructions: bool | None = None - """When true, omit the host's user-level instruction targets, leaving only repository - targets. For multitenant deployments (mirrors `discover`'s `excludeHostInstructions`). - """ - project_paths: list[str] | None = None - """Optional list of project directory paths. When omitted or empty, only the user-level - targets are returned. - """ + host_id: str + """Listener UUID. Unknown or successfully stopped IDs are harmless.""" @staticmethod - def from_dict(obj: Any) -> 'InstructionsGetDiscoveryPathsRequest': + def from_dict(obj: Any) -> 'HostDisposeRequest': assert isinstance(obj, dict) - exclude_host_instructions = from_union([from_bool, from_none], obj.get("excludeHostInstructions")) - project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths")) - return InstructionsGetDiscoveryPathsRequest(exclude_host_instructions, project_paths) + host_id = from_str(obj.get("hostId")) + return HostDisposeRequest(host_id) def to_dict(self) -> dict: result: dict = {} - if self.exclude_host_instructions is not None: - result["excludeHostInstructions"] = from_union([from_bool, from_none], self.exclude_host_instructions) - if self.project_paths is not None: - result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) + 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 InterruptMainTurnRequest: - """Parameters for interrupting the main agent turn.""" - - flush_queued: bool | None = None - """When true, the user's queued prompts are preserved and run as the next turn once the - interrupted turn unwinds; when false (the default), the queue is cleared like a plain - abort. - """ - +class HostEmptyResult: + """Empty acknowledgement for a completed host lifecycle operation.""" @staticmethod - def from_dict(obj: Any) -> 'InterruptMainTurnRequest': + def from_dict(obj: Any) -> 'HostEmptyResult': assert isinstance(obj, dict) - flush_queued = from_union([from_bool, from_none], obj.get("flushQueued")) - return InterruptMainTurnRequest(flush_queued) + return HostEmptyResult() def to_dict(self) -> dict: result: dict = {} - if self.flush_queued is not None: - result["flushQueued"] = from_union([from_bool, from_none], self.flush_queued) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class InterruptMainTurnResult: - """Result of interrupting the main agent turn.""" +class HostEnvironmentCredentials: + """Private credentials delivered only to a runtime-owned Mission Control hosting participant.""" - interrupted: bool - """Whether an in-flight main agent turn was interrupted. False when the main loop was not - processing. - """ + 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) -> 'InterruptMainTurnResult': + def from_dict(obj: Any) -> 'HostEnvironmentCredentials': assert isinstance(obj, dict) - interrupted = from_bool(obj.get("interrupted")) - return InterruptMainTurnResult(interrupted) + 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["interrupted"] = from_bool(self.interrupted) + 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.""" + + DISPOSED = "disposed" + EXITED = "exited" + OWNER_DISCONNECTED = "ownerDisconnected" + RUNTIME_SHUTDOWN = "runtimeShutdown" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class JSONSchemaResponseFormat: - """A JSON Schema output contract. OpenAI receives the name, description, schema and strict - setting; Anthropic receives the schema in output_config.format and always uses its native - strict enforcement. +class HostLocalServerOptions: + """Local WebSocket transport options. - JSON Schema and provider options for the turn's output. + Enables a local WebSocket listener. """ - name: str - """Name of the output schema, subject to the provider's naming restrictions.""" + hostname: str | None = None + """Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed.""" - schema: Any = None - """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 - and schema restrictions are determined by the provider. - """ - description: str | None = None - """Optional description passed to OpenAI providers.""" + port: int | None = None + """Listener port. Omitted or zero requests an OS-allocated port.""" - strict: bool | None = None - """Optional strict enforcement setting for OpenAI providers. Omitted uses the provider - default. Anthropic always enforces its supported schema subset. - """ + 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) -> 'JSONSchemaResponseFormat': + def from_dict(obj: Any) -> 'HostLocalServerOptions': assert isinstance(obj, dict) - name = from_str(obj.get("name")) - schema = obj.get("schema") - description = from_union([from_str, from_none], obj.get("description")) - strict = from_union([from_bool, from_none], obj.get("strict")) - return JSONSchemaResponseFormat(name, schema, description, strict) + 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 = {} - result["name"] = from_str(self.name) - result["schema"] = self.schema - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) - if self.strict is not None: - result["strict"] = from_union([from_bool, from_none], self.strict) + 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 LlmInferenceHTTPRequestChunkRequest: - """A request body chunk or cancellation signal.""" - - data: str - """Body byte range. UTF-8 text when `binary` is absent or false; base64-encoded bytes when - `binary` is true. May be empty. - """ - request_id: str - """Matches the requestId from the originating httpRequestStart frame.""" - - agent_invocation_id: str | None = None - """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. - """ - binary: bool | None = None - """When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text.""" +class HostPublishSessionRequest: + """Publishes a resident session attached to the listener's owning connection.""" - cancel: bool | None = None - """When true, the runtime is cancelling the in-flight request (e.g. upstream consumer - aborted). `data` is ignored. Implies end-of-request. - """ - cancel_reason: str | None = None - """Optional human-readable reason for the cancellation, propagated for logging.""" + host_id: str + """Listener UUID returned by host.start.""" - end: bool | None = None - """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. - """ + session_id: str + """Canonical runtime session ID attached to the listener's owning connection.""" @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceHTTPRequestChunkRequest': + def from_dict(obj: Any) -> 'HostPublishSessionRequest': assert isinstance(obj, dict) - data = from_str(obj.get("data")) - request_id = from_str(obj.get("requestId")) - agent_invocation_id = from_union([from_str, from_none], obj.get("agentInvocationId")) - binary = from_union([from_bool, from_none], obj.get("binary")) - cancel = from_union([from_bool, from_none], obj.get("cancel")) - cancel_reason = from_union([from_str, from_none], obj.get("cancelReason")) - end = from_union([from_bool, from_none], obj.get("end")) - return LlmInferenceHTTPRequestChunkRequest(data, request_id, agent_invocation_id, binary, cancel, cancel_reason, end) + host_id = from_str(obj.get("hostId")) + session_id = from_str(obj.get("sessionId")) + return HostPublishSessionRequest(host_id, session_id) def to_dict(self) -> dict: result: dict = {} - result["data"] = from_str(self.data) - result["requestId"] = from_str(self.request_id) - if self.agent_invocation_id is not None: - result["agentInvocationId"] = from_union([from_str, from_none], self.agent_invocation_id) - if self.binary is not None: - result["binary"] = from_union([from_bool, from_none], self.binary) - if self.cancel is not None: - result["cancel"] = from_union([from_bool, from_none], self.cancel) - if self.cancel_reason is not None: - result["cancelReason"] = from_union([from_str, from_none], self.cancel_reason) - if self.end is not None: - result["end"] = from_union([from_bool, from_none], self.end) + result["hostId"] = from_str(self.host_id) + 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 LlmInferenceHTTPRequestChunkResult: - """Acknowledgement. The SDK is free to ignore the ack and treat chunk delivery as - fire-and-forget. - """ - @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceHTTPRequestChunkResult': - assert isinstance(obj, dict) - return LlmInferenceHTTPRequestChunkResult() +class HostPublishSessionResult: + """The existing runtime identity and its resource on the listener.""" - def to_dict(self) -> dict: - result: dict = {} - return result + session_id: str + """Canonical runtime ID of the published session.""" -class LlmInferenceHTTPRequestStartTransport(Enum): - """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. - """ - HTTP = "http" - WEBSOCKET = "websocket" + session_uri: str + """AHP resource URI for the session on this listener.""" -@dataclass -class LlmInferenceHTTPRequestStartResult: - """Acknowledgement. Returning successfully simply means the SDK accepted the start frame; it - does not imply the request will succeed. - """ @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceHTTPRequestStartResult': + def from_dict(obj: Any) -> 'HostPublishSessionResult': assert isinstance(obj, dict) - return LlmInferenceHTTPRequestStartResult() + session_id = from_str(obj.get("sessionId")) + session_uri = from_str(obj.get("sessionUri")) + return HostPublishSessionResult(session_id, session_uri) def to_dict(self) -> dict: result: dict = {} + result["sessionId"] = from_str(self.session_id) + result["sessionUri"] = from_str(self.session_uri) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class LlmInferenceHTTPResponseChunkError: - """Set to terminate the response with a transport-level failure. Implies end-of-stream; any - further chunks for this requestId are ignored. - """ - message: str - """Human-readable failure description.""" - - code: str | None = None - """Optional machine-readable error code.""" - - @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseChunkError': - assert isinstance(obj, dict) - message = from_str(obj.get("message")) - code = from_union([from_str, from_none], obj.get("code")) - return LlmInferenceHTTPResponseChunkError(message, code) +class HostReadyRequest: + """Readiness reported by the supervised hosting participant on its own SDK connection.""" - def to_dict(self) -> dict: - result: dict = {} - result["message"] = from_str(self.message) - if self.code is not None: - result["code"] = from_union([from_str, from_none], self.code) - return result + address: str | None = None + """Actual bound WebSocket URL.""" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class LlmInferenceHTTPResponseChunkResult: - """Whether the chunk was accepted.""" + environment_id: str | None = None + """Registered environment ID, reported only once the relay transport is connected.""" - accepted: bool - """True when the chunk was matched to a pending request; false when unknown.""" + token: str | None = None + """Configured secret token, absent when authentication is disabled.""" @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseChunkResult': + def from_dict(obj: Any) -> 'HostReadyRequest': assert isinstance(obj, dict) - accepted = from_bool(obj.get("accepted")) - return LlmInferenceHTTPResponseChunkResult(accepted) + 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, environment_id, token) def to_dict(self) -> dict: result: dict = {} - result["accepted"] = from_bool(self.accepted) + 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 -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class LlmInferenceHTTPResponseStartRequest: - """Response head.""" +class HostRegisterSessionRequest: + """Listener-scoped registration, not a copy or durable adoption of a session.""" - headers: dict[str, list[str]] - """HTTP response headers, preserving multiple values per name.""" + session_id: str + """Canonical ID of the existing resident runtime session.""" - request_id: str - """Matches the requestId from the originating httpRequestStart frame.""" + working_directory: str + """Absolute working directory of the resident session.""" - status: int - """HTTP status code.""" + additional_directories: list[str] | None = None + """Additional directories already granted to the resident session.""" - status_text: str | None = None - """Optional HTTP status reason phrase.""" + created_at_unix_ms: int | None = None + """Session creation time in milliseconds since the Unix epoch, when available.""" + + modified_at_unix_ms: int | None = None + """Last session modification time in milliseconds since the Unix epoch, when available.""" + + title: str | None = None + """Current display title of the resident session, when available.""" @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseStartRequest': + def from_dict(obj: Any) -> 'HostRegisterSessionRequest': assert isinstance(obj, dict) - headers = from_dict(lambda x: from_list(from_str, x), obj.get("headers")) - request_id = from_str(obj.get("requestId")) - status = from_int(obj.get("status")) - status_text = from_union([from_str, from_none], obj.get("statusText")) - return LlmInferenceHTTPResponseStartRequest(headers, request_id, status, status_text) + session_id = from_str(obj.get("sessionId")) + working_directory = from_str(obj.get("workingDirectory")) + additional_directories = from_union([lambda x: from_list(from_str, x), from_none], obj.get("additionalDirectories")) + created_at_unix_ms = from_union([from_int, from_none], obj.get("createdAtUnixMs")) + modified_at_unix_ms = from_union([from_int, from_none], obj.get("modifiedAtUnixMs")) + title = from_union([from_str, from_none], obj.get("title")) + return HostRegisterSessionRequest(session_id, working_directory, additional_directories, created_at_unix_ms, modified_at_unix_ms, title) def to_dict(self) -> dict: result: dict = {} - result["headers"] = from_dict(lambda x: from_list(from_str, x), self.headers) - result["requestId"] = from_str(self.request_id) - result["status"] = from_int(self.status) - if self.status_text is not None: - result["statusText"] = from_union([from_str, from_none], self.status_text) + result["sessionId"] = from_str(self.session_id) + result["workingDirectory"] = from_str(self.working_directory) + if self.additional_directories is not None: + result["additionalDirectories"] = from_union([lambda x: from_list(from_str, x), from_none], self.additional_directories) + if self.created_at_unix_ms is not None: + result["createdAtUnixMs"] = from_union([from_int, from_none], self.created_at_unix_ms) + if self.modified_at_unix_ms is not None: + result["modifiedAtUnixMs"] = from_union([from_int, from_none], self.modified_at_unix_ms) + 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 LlmInferenceHTTPResponseStartResult: - """Whether the start frame was accepted.""" +class HostSessionCreateResult: + """The resident session the application has materialized on its own connection.""" - accepted: bool - """True when the response start was matched to a pending request; false when unknown.""" + session_id: str + """Runtime session UUID materialized on the application's SDK connection.""" @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseStartResult': + def from_dict(obj: Any) -> 'HostSessionCreateResult': assert isinstance(obj, dict) - accepted = from_bool(obj.get("accepted")) - return LlmInferenceHTTPResponseStartResult(accepted) + session_id = from_str(obj.get("sessionId")) + return HostSessionCreateResult(session_id) def to_dict(self) -> dict: result: dict = {} - result["accepted"] = from_bool(self.accepted) + 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 LlmInferenceSetProviderResult: - """Indicates whether the calling client was registered as the LLM inference provider.""" +class HostSessionReleaseRequest: + """Ends one participation, not the application's session lifetime.""" - success: bool - """Whether the provider was set successfully""" + handoff_id: str + """Identity of the participation to release without destroying the session.""" @staticmethod - def from_dict(obj: Any) -> 'LlmInferenceSetProviderResult': + def from_dict(obj: Any) -> 'HostSessionReleaseRequest': assert isinstance(obj, dict) - success = from_bool(obj.get("success")) - return LlmInferenceSetProviderResult(success) + handoff_id = from_str(obj.get("handoffId")) + return HostSessionReleaseRequest(handoff_id) def to_dict(self) -> dict: result: dict = {} - result["success"] = from_bool(self.success) + result["handoffId"] = from_str(self.handoff_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. -class HostType(Enum): - """Repository host type - - Hosting platform type of the repository - - Repository host type, if known - - Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. - """ - ADO = "ado" - GITHUB = "github" - -# Experimental: this type is part of an experimental API and may change or be removed. -class SessionLogLevel(Enum): - """Log severity level. Determines how the message is displayed in the timeline. Defaults to - "info". - """ - ERROR = "error" - INFO = "info" - WARNING = "warning" - -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class LogResult: - """Identifier of the session event that was emitted for the log message.""" +class HostSessionReleasedNotification: + """Releases the original application session object retained for one handoff.""" - event_id: UUID - """The unique identifier of the emitted session event""" + handoff_id: str + """Identity of the handoff retaining the original application session object.""" + + host_id: str + """Listener UUID whose application session participation ended.""" @staticmethod - def from_dict(obj: Any) -> 'LogResult': + def from_dict(obj: Any) -> 'HostSessionReleasedNotification': assert isinstance(obj, dict) - event_id = UUID(obj.get("eventId")) - return LogResult(event_id) + handoff_id = from_str(obj.get("handoffId")) + host_id = from_str(obj.get("hostId")) + return HostSessionReleasedNotification(handoff_id, host_id) def to_dict(self) -> dict: result: dict = {} - result["eventId"] = str(self.event_id) + result["handoffId"] = from_str(self.handoff_id) + 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 LspInitializeRequest: - """Parameters for (re)loading the merged LSP configuration set.""" - - force: bool | None = None - """Force re-initialization even when LSP configs were already loaded for the working - directory. - """ - git_root: str | None = None - """Git root used as the boundary when traversing for project-level LSP configs (supports - monorepos). +class HostStartResult: + """Listener readiness, returned only after binding and the supervised participant's SDK + handshake. """ - working_directory: str | None = None - """Working directory used to load project-level LSP configs. Defaults to the session working - directory when omitted. + host_id: str + """Caller-generated listener UUID.""" + + 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 + listeners. """ + 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) -> 'LspInitializeRequest': + def from_dict(obj: Any) -> 'HostStartResult': assert isinstance(obj, dict) - force = from_union([from_bool, from_none], obj.get("force")) - git_root = from_union([from_str, from_none], obj.get("gitRoot")) - working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) - return LspInitializeRequest(force, git_root, working_directory) + host_id = from_str(obj.get("hostId")) + 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")) + 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 = {} - if self.force is not None: - result["force"] = from_union([from_bool, from_none], self.force) - if self.git_root is not None: - result["gitRoot"] = from_union([from_str, from_none], self.git_root) - if self.working_directory is not None: - result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) + result["hostId"] = from_str(self.host_id) + 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. @dataclass -class ManagedMCPServerConfig: - """Non-secret host-managed HTTP MCP server configuration. The containing map key is the - stable managed identity; credentials are supplied dynamically by the host. +class InstallationCatalogueIdentity: + """Catalogue identity retained from a bound candidate or plan at installation time. + + Catalogue identity retained from the bound candidate when available. + + Catalogue identity retained from the bound candidate before consent. + + Catalogue identity retained at install time. + + Catalogue identity retained from the installed plan when available. """ display_name: str - """Human-readable catalog display name.""" + """Human display name retained from the catalogue candidate.""" - url: str - """Hosted MCP streamable HTTP endpoint.""" + source: str + """Catalogue authority/source string that supplied the candidate.""" - headers_refresh_ttl_ms: int | None = None - """Maximum dynamic-header cache lifetime in milliseconds.""" + description: str | None = None + """Catalogue description retained at install planning time.""" - timeout: int | None = None - """Timeout in milliseconds for tool discovery and tool calls.""" + item_url: str | None = None + """Catalogue item URL when supplied by the authority.""" - tools: list[str] | None = None - """Tools to include. Defaults to all tools when omitted.""" + publisher: str | None = None + """Catalogue publisher retained at install planning time.""" + + resource_id: str | None = None + """Authority resource identifier when supplied by the catalogue.""" + + trust_at_install: CatalogTrustSnapshot | None = None + """Catalogue trust observation retained at install planning time.""" + + version: str | None = None + """Catalogue version retained at install planning time.""" @staticmethod - def from_dict(obj: Any) -> 'ManagedMCPServerConfig': + def from_dict(obj: Any) -> 'InstallationCatalogueIdentity': assert isinstance(obj, dict) display_name = from_str(obj.get("displayName")) - url = from_str(obj.get("url")) - headers_refresh_ttl_ms = from_union([from_int, from_none], obj.get("headersRefreshTtlMs")) - timeout = from_union([from_int, from_none], obj.get("timeout")) - tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools")) - return ManagedMCPServerConfig(display_name, url, headers_refresh_ttl_ms, timeout, tools) + source = from_str(obj.get("source")) + description = from_union([from_str, from_none], obj.get("description")) + item_url = from_union([from_str, from_none], obj.get("itemUrl")) + publisher = from_union([from_str, from_none], obj.get("publisher")) + resource_id = from_union([from_str, from_none], obj.get("resourceId")) + trust_at_install = from_union([_load_CatalogTrustSnapshot, from_none], obj.get("trustAtInstall")) + version = from_union([from_str, from_none], obj.get("version")) + return InstallationCatalogueIdentity(display_name, source, description, item_url, publisher, resource_id, trust_at_install, version) def to_dict(self) -> dict: result: dict = {} result["displayName"] = from_str(self.display_name) - result["url"] = from_str(self.url) - if self.headers_refresh_ttl_ms is not None: - result["headersRefreshTtlMs"] = from_union([from_int, from_none], self.headers_refresh_ttl_ms) - if self.timeout is not None: - result["timeout"] = from_union([from_int, from_none], self.timeout) - if self.tools is not None: - result["tools"] = from_union([lambda x: from_list(from_str, x), from_none], self.tools) + result["source"] = from_str(self.source) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.item_url is not None: + result["itemUrl"] = from_union([from_str, from_none], self.item_url) + if self.publisher is not None: + result["publisher"] = from_union([from_str, from_none], self.publisher) + if self.resource_id is not None: + result["resourceId"] = from_union([from_str, from_none], self.resource_id) + if self.trust_at_install is not None: + result["trustAtInstall"] = from_union([lambda x: (x).to_dict(), from_none], self.trust_at_install) + if self.version is not None: + result["version"] = from_union([from_str, from_none], self.version) return result +class Resource(Enum): + MCP = "mcp" + SKILL = "skill" + +class MCPInstallationReviewAction(Enum): + INSTALL = "install" + UNINSTALL = "uninstall" + # 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 MCPPlanConfigurationOperation(Enum): + """Whether the change would create a new entry or modify an existing one. - error_message: str | None = None - """Discovery or validation error text when managed settings could not be read safely.""" + Whether a planned configuration change would create or modify an entry + """ + ADD = "add" + UPDATE = "update" - settings_json: Any = None - """Validated, canonical managed-settings JSON. Omitted when no managed settings were - discovered or when discovered settings failed validation. +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPPlanScope(Enum): + """Scope the change would be written to. + + Configuration scope an MCP install plan targets + + Configuration scope the plan targets. + + Configuration scope the plan targets. Defaults to user scope when omitted. """ + USER = "user" - @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) +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPPlanRemoteTransport(Enum): + """Transport in the effective persisted remote configuration. - 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 + Transport exposed by a remote endpoint + + Endpoint transport this remote choice would use. + """ + HTTP = "http" + SSE = "sse" + STREAMABLE_HTTP = "streamable-http" # 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. - """ - bypass_permissions_disabled: bool - """Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. - Deny-wins across layers, and forced on when `failClosed` is true. - """ - device_managed: bool - """Whether an actual device MDM/plist/registry/file managed-settings layer was present""" +class MCPPlanResourceIdentity: + """Identity from the retained plan, not caller display text. - fail_closed: 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. - """ - managed_keys: list[str] - """The setting keys under enterprise management in the effective managed settings (e.g. - `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. - """ - server_managed: bool - """Whether the server (account/org) managed-settings layer was present""" + Normalised identity of the MCP server a plan targets, independent of how the card spelled + it. - source: ManagedSettingsResolvedSource - """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. - """ - client_managed: bool | None = None - """Whether a session-local permissions layer injected by the SDK host was present""" + Identity from the installed receipt. - permissions_allow_intersected: bool | None = None - """Whether at least two managed sources supplied permission allowlists, so enforcement - intersects them and the flattened settings payload omits `permissions.allow`. - """ - policy_helper_managed: bool | None = None - """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. - """ - sandbox_enabled_by_undetermined_policy: bool | None = None - """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. - """ - settings: Any = None - """The effective (resolved) managed settings values, so clients can render exactly what is - enforced. Absent when no managed policy is in force. + Identity retained from the validated original plan. + + Normalised identity of the server the plan would install. """ + canonical_name: str + """Canonical, normalised name of the server, for example `io.github.owner/server`.""" + + server_name: str + """Local configuration key the server would be recorded under.""" + + registry_id: str | None = None + """Registry identifier of the server, when it came from a registry.""" + + version: str | None = None + """Version advertised by the card, when it declares one.""" @staticmethod - def from_dict(obj: Any) -> 'ManagedSettingsResolvedData': + def from_dict(obj: Any) -> 'MCPPlanResourceIdentity': assert isinstance(obj, dict) - bypass_permissions_disabled = from_bool(obj.get("bypassPermissionsDisabled")) - device_managed = from_bool(obj.get("deviceManaged")) - fail_closed = from_bool(obj.get("failClosed")) - managed_keys = from_list(from_str, obj.get("managedKeys")) - server_managed = from_bool(obj.get("serverManaged")) - source = ManagedSettingsResolvedSource(obj.get("source")) - client_managed = from_union([from_bool, from_none], obj.get("clientManaged")) - permissions_allow_intersected = from_union([from_bool, from_none], obj.get("permissionsAllowIntersected")) - policy_helper_managed = from_union([from_bool, from_none], obj.get("policyHelperManaged")) - sandbox_enabled_by_undetermined_policy = from_union([from_bool, from_none], obj.get("sandboxEnabledByUndeterminedPolicy")) - settings = obj.get("settings") - return ManagedSettingsResolvedData(bypass_permissions_disabled, device_managed, fail_closed, managed_keys, server_managed, source, client_managed, permissions_allow_intersected, policy_helper_managed, sandbox_enabled_by_undetermined_policy, settings) + canonical_name = from_str(obj.get("canonicalName")) + server_name = from_str(obj.get("serverName")) + registry_id = from_union([from_str, from_none], obj.get("registryId")) + version = from_union([from_str, from_none], obj.get("version")) + return MCPPlanResourceIdentity(canonical_name, server_name, registry_id, version) def to_dict(self) -> dict: result: dict = {} - result["bypassPermissionsDisabled"] = from_bool(self.bypass_permissions_disabled) - result["deviceManaged"] = from_bool(self.device_managed) - result["failClosed"] = from_bool(self.fail_closed) - result["managedKeys"] = from_list(from_str, self.managed_keys) - result["serverManaged"] = from_bool(self.server_managed) - result["source"] = to_enum(ManagedSettingsResolvedSource, self.source) - if self.client_managed is not None: - result["clientManaged"] = from_union([from_bool, from_none], self.client_managed) - if self.permissions_allow_intersected is not None: - result["permissionsAllowIntersected"] = from_union([from_bool, from_none], self.permissions_allow_intersected) - if self.policy_helper_managed is not None: - result["policyHelperManaged"] = from_union([from_bool, from_none], self.policy_helper_managed) - if self.sandbox_enabled_by_undetermined_policy is not None: - result["sandboxEnabledByUndeterminedPolicy"] = from_union([from_bool, from_none], self.sandbox_enabled_by_undetermined_policy) - if self.settings is not None: - result["settings"] = self.settings + result["canonicalName"] = from_str(self.canonical_name) + result["serverName"] = from_str(self.server_name) + if self.registry_id is not None: + result["registryId"] = from_union([from_str, from_none], self.registry_id) + 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. +class MCPPlanValueCategory(Enum): + """Exact category declared by the selected choice. + + Where a required value is applied when the planned server is launched + + Where the value is applied when the server is launched. + """ + ENVIRONMENT_VARIABLE = "environment-variable" + HEADER = "header" + PACKAGE_ARGUMENT = "package-argument" + RUNTIME_ARGUMENT = "runtime-argument" + URL_VARIABLE = "url-variable" + +# Experimental: this type is part of an experimental API and may change or be removed. +class SkillInstallationSessionState(Enum): + """Bound-session load observation. + + Bound-session observation after reconciling persisted enablement. + """ + LOADED_DISABLED = "loaded-disabled" + LOADED_ENABLED = "loaded-enabled" + NOT_LOADED = "not-loaded" + UNKNOWN = "unknown" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MarketplaceAddResult: - """Result of registering a new marketplace.""" +class SkillInstallationSource: + """Exact verified source identity. - name: str - """Final name of the marketplace as resolved from its manifest""" + Source identity retained from Agent Finder and the pinned GitHub descriptor. + + Exact retained verified source identity. + """ + bundle_digest: str + """Digest of the descriptor's bundle manifest.""" + + catalog_revision_id: str + """Agent Finder materialisation revision identifier.""" + + descriptor_digest: str + """Digest of the canonical materialisation descriptor.""" + + repository: str + """Repository full name, for example owner/name.""" + + repository_id: str + """GitHub repository database identifier.""" + + resource_id: str + """Agent Finder resource identifier.""" + + revision: str + """Pinned Git commit revision.""" + + root: str + """Root path within the pinned repository.""" @staticmethod - def from_dict(obj: Any) -> 'MarketplaceAddResult': + def from_dict(obj: Any) -> 'SkillInstallationSource': assert isinstance(obj, dict) - name = from_str(obj.get("name")) - return MarketplaceAddResult(name) + bundle_digest = from_str(obj.get("bundleDigest")) + catalog_revision_id = from_str(obj.get("catalogRevisionId")) + descriptor_digest = from_str(obj.get("descriptorDigest")) + repository = from_str(obj.get("repository")) + repository_id = from_str(obj.get("repositoryId")) + resource_id = from_str(obj.get("resourceId")) + revision = from_str(obj.get("revision")) + root = from_str(obj.get("root")) + return SkillInstallationSource(bundle_digest, catalog_revision_id, descriptor_digest, repository, repository_id, resource_id, revision, root) def to_dict(self) -> dict: result: dict = {} - result["name"] = from_str(self.name) + result["bundleDigest"] = from_str(self.bundle_digest) + result["catalogRevisionId"] = from_str(self.catalog_revision_id) + result["descriptorDigest"] = from_str(self.descriptor_digest) + result["repository"] = from_str(self.repository) + result["repositoryId"] = from_str(self.repository_id) + result["resourceId"] = from_str(self.resource_id) + result["revision"] = from_str(self.revision) + result["root"] = from_str(self.root) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class SkillInstallationScope(Enum): + """Installation scope. Agent Finder Skills are installed in the user's personal Copilot home.""" + + PERSONAL = "personal" + +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPPlanPolicyDecision(Enum): + """What policy decided for this server. + + What policy decided for a planned server + """ + ALLOWED = "allowed" + BLOCKED = "blocked" + REQUIRES_APPROVAL = "requires-approval" + +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPInstallationSecretStorage(Enum): + """Explicit reviewed backend selection; no backend is accessed when no secrets are + supplied. + + Explicit backend selection is part of the final review; failures never switch backends. + + The trusted host presents this choice alongside the exact secret placeholders. + """ + KEYCHAIN = "keychain" + PRIVATE_FILE = "private-file" + +class InstallMethod(Enum): + """Discriminator for a package-backed transport choice + + Discriminator for a remote-endpoint transport choice + """ + PACKAGE = "package" + REMOTE = "remote" + +class MCPPlanRequiredValueKind(Enum): + """Discriminator for a scalar required value + + Discriminator for an enumerated required value + """ + ENUM = "enum" + SCALAR = "scalar" + +class MCPPlanRequiredValueValueType(Enum): + """Scalar type the value must conform to. + + Scalar type a required value must conform to + + Discriminator for an enumerated required value + """ + BOOLEAN = "boolean" + ENUM = "enum" + NUMBER = "number" + PATH = "path" + STRING = "string" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPlanSecretPlaceholder: + """A secret a transport choice needs, referenced by placeholder. No secret value ever + appears in a plan, and the placeholder resolves against the keychain only when a plan is + applied. + """ + key: str + """Key the secret is supplied under. Inert untrusted data.""" + + placeholder: str + """The runtime-assigned `${secret:}` placeholder written into configuration in place of + the value. + """ + title: str | None = None + """Human-readable label from the card. Inert untrusted text.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPlanSecretPlaceholder': + assert isinstance(obj, dict) + key = from_str(obj.get("key")) + placeholder = from_str(obj.get("placeholder")) + title = from_union([from_str, from_none], obj.get("title")) + return MCPPlanSecretPlaceholder(key, placeholder, title) + + def to_dict(self) -> dict: + result: dict = {} + result["key"] = from_str(self.key) + result["placeholder"] = from_str(self.placeholder) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + return result + +class MCPPlanETransport(Enum): + """Local process transport this package choice would use. + + Transport exposed by a locally launched package + + Transport in the effective persisted remote configuration. + + Transport exposed by a remote endpoint + + Endpoint transport this remote choice would use. + """ + HTTP = "http" + SSE = "sse" + STDIO = "stdio" + STREAMABLE_HTTP = "streamable-http" + +class NScope(Enum): + """Scope the change would be written to. + + Configuration scope an MCP install plan targets + + Configuration scope the plan targets. + + Configuration scope the plan targets. Defaults to user scope when omitted. + + Installation scope. Agent Finder Skills are installed in the user's personal Copilot home. + """ + PERSONAL = "personal" + USER = "user" + +class InstallationDecision(Enum): + """Fresh explicit user decision. There is no default. + + Explicit user decisions, never inferred from a permission grant or model response. + """ + CANCEL = "cancel" + CONFIRM = "confirm" + DECLINE = "decline" + +class PurpleSource(Enum): + GITHUB = "github" + +class FluffySource(Enum): + LOCAL = "local" + +# Experimental: this type is part of an experimental API and may change or be removed. +class InstructionLocation(Enum): + """Which tier this target belongs to + + Where this source lives — used for UI grouping + """ + PLUGIN = "plugin" + REPOSITORY = "repository" + USER = "user" + WORKING_DIRECTORY = "working-directory" + +# Experimental: this type is part of an experimental API and may change or be removed. +class InstructionSourceType(Enum): + """Category of instruction source — used for merge logic""" + + CHILD_INSTRUCTIONS = "child-instructions" + HOME = "home" + MODEL = "model" + NESTED_AGENTS = "nested-agents" + PLUGIN = "plugin" + REPO = "repo" + VSCODE = "vscode" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class InstructionsDiscoverRequest: + """Optional project paths to include in instruction discovery.""" + + exclude_host_instructions: bool | None = None + """When true, omit the host's instruction sources (user/home-level files and plugin rules), + leaving only repository and working-directory sources. For multitenant deployments. + """ + project_paths: list[str] | None = None + """Optional list of project directory paths to scan for repository/working-directory + instruction sources. When omitted or empty, only user-level and plugin instruction + sources are returned (no project scan). + """ + + @staticmethod + def from_dict(obj: Any) -> 'InstructionsDiscoverRequest': + assert isinstance(obj, dict) + exclude_host_instructions = from_union([from_bool, from_none], obj.get("excludeHostInstructions")) + project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths")) + return InstructionsDiscoverRequest(exclude_host_instructions, project_paths) + + def to_dict(self) -> dict: + result: dict = {} + if self.exclude_host_instructions is not None: + result["excludeHostInstructions"] = from_union([from_bool, from_none], self.exclude_host_instructions) + if self.project_paths is not None: + result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class InstructionsGetDiscoveryPathsRequest: + """Optional project paths to include when enumerating instruction discovery targets.""" + + exclude_host_instructions: bool | None = None + """When true, omit the host's user-level instruction targets, leaving only repository + targets. For multitenant deployments (mirrors `discover`'s `excludeHostInstructions`). + """ + project_paths: list[str] | None = None + """Optional list of project directory paths. When omitted or empty, only the user-level + targets are returned. + """ + + @staticmethod + def from_dict(obj: Any) -> 'InstructionsGetDiscoveryPathsRequest': + assert isinstance(obj, dict) + exclude_host_instructions = from_union([from_bool, from_none], obj.get("excludeHostInstructions")) + project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths")) + return InstructionsGetDiscoveryPathsRequest(exclude_host_instructions, project_paths) + + def to_dict(self) -> dict: + result: dict = {} + if self.exclude_host_instructions is not None: + result["excludeHostInstructions"] = from_union([from_bool, from_none], self.exclude_host_instructions) + if self.project_paths is not None: + result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class InterruptMainTurnRequest: + """Parameters for interrupting the main agent turn.""" + + flush_queued: bool | None = None + """When true, the user's queued prompts are preserved and run as the next turn once the + interrupted turn unwinds; when false (the default), the queue is cleared like a plain + abort. + """ + + @staticmethod + def from_dict(obj: Any) -> 'InterruptMainTurnRequest': + assert isinstance(obj, dict) + flush_queued = from_union([from_bool, from_none], obj.get("flushQueued")) + return InterruptMainTurnRequest(flush_queued) + + def to_dict(self) -> dict: + result: dict = {} + if self.flush_queued is not None: + result["flushQueued"] = from_union([from_bool, from_none], self.flush_queued) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class InterruptMainTurnResult: + """Result of interrupting the main agent turn.""" + + interrupted: bool + """Whether an in-flight main agent turn was interrupted. False when the main loop was not + processing. + """ + + @staticmethod + def from_dict(obj: Any) -> 'InterruptMainTurnResult': + assert isinstance(obj, dict) + interrupted = from_bool(obj.get("interrupted")) + return InterruptMainTurnResult(interrupted) + + def to_dict(self) -> dict: + result: dict = {} + result["interrupted"] = from_bool(self.interrupted) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class JSONSchemaResponseFormat: + """A JSON Schema output contract. OpenAI receives the name, description, schema and strict + setting; Anthropic receives the schema in output_config.format and always uses its native + strict enforcement. + + JSON Schema and provider options for the turn's output. + """ + name: str + """Name of the output schema, subject to the provider's naming restrictions.""" + + 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 + and schema restrictions are determined by the provider. + """ + description: str | None = None + """Optional description passed to OpenAI providers.""" + + strict: bool | None = None + """Optional strict enforcement setting for OpenAI providers. Omitted uses the provider + default. Anthropic always enforces its supported schema subset. + """ + + @staticmethod + def from_dict(obj: Any) -> 'JSONSchemaResponseFormat': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + schema = obj.get("schema") + description = from_union([from_str, from_none], obj.get("description")) + strict = from_union([from_bool, from_none], obj.get("strict")) + return JSONSchemaResponseFormat(name, schema, description, strict) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["schema"] = self.schema + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.strict is not None: + result["strict"] = from_union([from_bool, from_none], self.strict) + return result + +@dataclass +class LlmInferenceHTTPRequestChunkRequest: + """A request body chunk or cancellation signal.""" + + data: str + """Body byte range. UTF-8 text when `binary` is absent or false; base64-encoded bytes when + `binary` is true. May be empty. + """ + request_id: str + """Matches the requestId from the originating httpRequestStart frame.""" + + agent_invocation_id: str | None = None + """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. + """ + binary: bool | None = None + """When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text.""" + + cancel: bool | None = None + """When true, the runtime is cancelling the in-flight request (e.g. upstream consumer + aborted). `data` is ignored. Implies end-of-request. + """ + cancel_reason: str | None = None + """Optional human-readable reason for the cancellation, propagated for logging.""" + + end: bool | None = None + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceHTTPRequestChunkRequest': + assert isinstance(obj, dict) + data = from_str(obj.get("data")) + request_id = from_str(obj.get("requestId")) + agent_invocation_id = from_union([from_str, from_none], obj.get("agentInvocationId")) + binary = from_union([from_bool, from_none], obj.get("binary")) + cancel = from_union([from_bool, from_none], obj.get("cancel")) + cancel_reason = from_union([from_str, from_none], obj.get("cancelReason")) + end = from_union([from_bool, from_none], obj.get("end")) + return LlmInferenceHTTPRequestChunkRequest(data, request_id, agent_invocation_id, binary, cancel, cancel_reason, end) + + def to_dict(self) -> dict: + result: dict = {} + result["data"] = from_str(self.data) + result["requestId"] = from_str(self.request_id) + if self.agent_invocation_id is not None: + result["agentInvocationId"] = from_union([from_str, from_none], self.agent_invocation_id) + if self.binary is not None: + result["binary"] = from_union([from_bool, from_none], self.binary) + if self.cancel is not None: + result["cancel"] = from_union([from_bool, from_none], self.cancel) + if self.cancel_reason is not None: + result["cancelReason"] = from_union([from_str, from_none], self.cancel_reason) + if self.end is not None: + result["end"] = from_union([from_bool, from_none], self.end) + return result + +@dataclass +class LlmInferenceHTTPRequestChunkResult: + """Acknowledgement. The SDK is free to ignore the ack and treat chunk delivery as + fire-and-forget. + """ + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceHTTPRequestChunkResult': + assert isinstance(obj, dict) + return LlmInferenceHTTPRequestChunkResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + +class LlmInferenceHTTPRequestStartTransport(Enum): + """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. + """ + HTTP = "http" + WEBSOCKET = "websocket" + +@dataclass +class LlmInferenceHTTPRequestStartResult: + """Acknowledgement. Returning successfully simply means the SDK accepted the start frame; it + does not imply the request will succeed. + """ + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceHTTPRequestStartResult': + assert isinstance(obj, dict) + return LlmInferenceHTTPRequestStartResult() + + 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 LlmInferenceHTTPResponseChunkError: + """Set to terminate the response with a transport-level failure. Implies end-of-stream; any + further chunks for this requestId are ignored. + """ + message: str + """Human-readable failure description.""" + + code: str | None = None + """Optional machine-readable error code.""" + + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseChunkError': + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + code = from_union([from_str, from_none], obj.get("code")) + return LlmInferenceHTTPResponseChunkError(message, code) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + if self.code is not None: + result["code"] = from_union([from_str, from_none], self.code) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class LlmInferenceHTTPResponseChunkResult: + """Whether the chunk was accepted.""" + + accepted: bool + """True when the chunk was matched to a pending request; false when unknown.""" + + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseChunkResult': + assert isinstance(obj, dict) + accepted = from_bool(obj.get("accepted")) + return LlmInferenceHTTPResponseChunkResult(accepted) + + def to_dict(self) -> dict: + result: dict = {} + result["accepted"] = from_bool(self.accepted) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class LlmInferenceHTTPResponseStartRequest: + """Response head.""" + + headers: dict[str, list[str]] + """HTTP response headers, preserving multiple values per name.""" + + request_id: str + """Matches the requestId from the originating httpRequestStart frame.""" + + status: int + """HTTP status code.""" + + status_text: str | None = None + """Optional HTTP status reason phrase.""" + + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseStartRequest': + assert isinstance(obj, dict) + headers = from_dict(lambda x: from_list(from_str, x), obj.get("headers")) + request_id = from_str(obj.get("requestId")) + status = from_int(obj.get("status")) + status_text = from_union([from_str, from_none], obj.get("statusText")) + return LlmInferenceHTTPResponseStartRequest(headers, request_id, status, status_text) + + def to_dict(self) -> dict: + result: dict = {} + result["headers"] = from_dict(lambda x: from_list(from_str, x), self.headers) + result["requestId"] = from_str(self.request_id) + result["status"] = from_int(self.status) + if self.status_text is not None: + result["statusText"] = from_union([from_str, from_none], self.status_text) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class LlmInferenceHTTPResponseStartResult: + """Whether the start frame was accepted.""" + + accepted: bool + """True when the response start was matched to a pending request; false when unknown.""" + + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceHTTPResponseStartResult': + assert isinstance(obj, dict) + accepted = from_bool(obj.get("accepted")) + return LlmInferenceHTTPResponseStartResult(accepted) + + def to_dict(self) -> dict: + result: dict = {} + result["accepted"] = from_bool(self.accepted) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class LlmInferenceSetProviderResult: + """Indicates whether the calling client was registered as the LLM inference provider.""" + + success: bool + """Whether the provider was set successfully""" + + @staticmethod + def from_dict(obj: Any) -> 'LlmInferenceSetProviderResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return LlmInferenceSetProviderResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class HostType(Enum): + """Repository host type + + Hosting platform type of the repository + + Repository host type, if known + + Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. + """ + ADO = "ado" + GITHUB = "github" + +# Experimental: this type is part of an experimental API and may change or be removed. +class SessionLogLevel(Enum): + """Log severity level. Determines how the message is displayed in the timeline. Defaults to + "info". + """ + ERROR = "error" + INFO = "info" + WARNING = "warning" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class LogResult: + """Identifier of the session event that was emitted for the log message.""" + + event_id: UUID + """The unique identifier of the emitted session event""" + + @staticmethod + def from_dict(obj: Any) -> 'LogResult': + assert isinstance(obj, dict) + event_id = UUID(obj.get("eventId")) + return LogResult(event_id) + + def to_dict(self) -> dict: + result: dict = {} + result["eventId"] = str(self.event_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class LspInitializeRequest: + """Parameters for (re)loading the merged LSP configuration set.""" + + force: bool | None = None + """Force re-initialization even when LSP configs were already loaded for the working + directory. + """ + git_root: str | None = None + """Git root used as the boundary when traversing for project-level LSP configs (supports + monorepos). + """ + working_directory: str | None = None + """Working directory used to load project-level LSP configs. Defaults to the session working + directory when omitted. + """ + + @staticmethod + def from_dict(obj: Any) -> 'LspInitializeRequest': + assert isinstance(obj, dict) + force = from_union([from_bool, from_none], obj.get("force")) + git_root = from_union([from_str, from_none], obj.get("gitRoot")) + working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) + return LspInitializeRequest(force, git_root, working_directory) + + def to_dict(self) -> dict: + result: dict = {} + if self.force is not None: + result["force"] = from_union([from_bool, from_none], self.force) + if self.git_root is not None: + result["gitRoot"] = from_union([from_str, from_none], self.git_root) + if self.working_directory is not None: + result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedMCPServerConfig: + """Non-secret host-managed HTTP MCP server configuration. The containing map key is the + stable managed identity; credentials are supplied dynamically by the host. + """ + display_name: str + """Human-readable catalog display name.""" + + url: str + """Hosted MCP streamable HTTP endpoint.""" + + headers_refresh_ttl_ms: int | None = None + """Maximum dynamic-header cache lifetime in milliseconds.""" + + timeout: int | None = None + """Timeout in milliseconds for tool discovery and tool calls.""" + + tools: list[str] | None = None + """Tools to include. Defaults to all tools when omitted.""" + + @staticmethod + def from_dict(obj: Any) -> 'ManagedMCPServerConfig': + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + url = from_str(obj.get("url")) + headers_refresh_ttl_ms = from_union([from_int, from_none], obj.get("headersRefreshTtlMs")) + timeout = from_union([from_int, from_none], obj.get("timeout")) + tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools")) + return ManagedMCPServerConfig(display_name, url, headers_refresh_ttl_ms, timeout, tools) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["url"] = from_str(self.url) + if self.headers_refresh_ttl_ms is not None: + result["headersRefreshTtlMs"] = from_union([from_int, from_none], self.headers_refresh_ttl_ms) + if self.timeout is not None: + result["timeout"] = from_union([from_int, from_none], self.timeout) + if self.tools is not None: + result["tools"] = from_union([lambda x: from_list(from_str, x), from_none], self.tools) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingMeta: + """Lock state and provenance of one managed setting. + + Lock state and provenance of `values.autoTier`. + + 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) -> 'ManagedSettingMeta': + assert isinstance(obj, dict) + overridable = from_bool(obj.get("overridable")) + source = from_str(obj.get("source")) + return ManagedSettingMeta(overridable, source) + + def to_dict(self) -> dict: + result: dict = {} + 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: + """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. + Deny-wins across layers, and forced on when `failClosed` is true. + """ + device_managed: bool + """Whether an actual device MDM/plist/registry/file managed-settings layer was present""" + + fail_closed: 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. + """ + managed_keys: list[str] + """The setting keys under enterprise management in the effective managed settings (e.g. + `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + """ + server_managed: bool + """Whether the server (account/org) managed-settings layer was present""" + + source: ManagedSettingsResolvedSource + """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. + """ + client_managed: bool | None = None + """Whether a session-local permissions layer injected by the SDK host was present""" + + permissions_allow_intersected: bool | None = None + """Whether at least two managed sources supplied permission allowlists, so enforcement + intersects them and the flattened settings payload omits `permissions.allow`. + """ + policy_helper_managed: bool | None = None + """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. + """ + sandbox_enabled_by_undetermined_policy: bool | None = None + """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. + """ + settings: Any = None + """The effective (resolved) managed settings values, so clients can render exactly what is + enforced. Absent when no managed policy is in force. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsResolvedData': + assert isinstance(obj, dict) + bypass_permissions_disabled = from_bool(obj.get("bypassPermissionsDisabled")) + device_managed = from_bool(obj.get("deviceManaged")) + fail_closed = from_bool(obj.get("failClosed")) + managed_keys = from_list(from_str, obj.get("managedKeys")) + server_managed = from_bool(obj.get("serverManaged")) + source = ManagedSettingsResolvedSource(obj.get("source")) + client_managed = from_union([from_bool, from_none], obj.get("clientManaged")) + permissions_allow_intersected = from_union([from_bool, from_none], obj.get("permissionsAllowIntersected")) + policy_helper_managed = from_union([from_bool, from_none], obj.get("policyHelperManaged")) + sandbox_enabled_by_undetermined_policy = from_union([from_bool, from_none], obj.get("sandboxEnabledByUndeterminedPolicy")) + settings = obj.get("settings") + return ManagedSettingsResolvedData(bypass_permissions_disabled, device_managed, fail_closed, managed_keys, server_managed, source, client_managed, permissions_allow_intersected, policy_helper_managed, sandbox_enabled_by_undetermined_policy, settings) + + def to_dict(self) -> dict: + result: dict = {} + result["bypassPermissionsDisabled"] = from_bool(self.bypass_permissions_disabled) + result["deviceManaged"] = from_bool(self.device_managed) + result["failClosed"] = from_bool(self.fail_closed) + result["managedKeys"] = from_list(from_str, self.managed_keys) + result["serverManaged"] = from_bool(self.server_managed) + result["source"] = to_enum(ManagedSettingsResolvedSource, self.source) + if self.client_managed is not None: + result["clientManaged"] = from_union([from_bool, from_none], self.client_managed) + if self.permissions_allow_intersected is not None: + result["permissionsAllowIntersected"] = from_union([from_bool, from_none], self.permissions_allow_intersected) + if self.policy_helper_managed is not None: + result["policyHelperManaged"] = from_union([from_bool, from_none], self.policy_helper_managed) + if self.sandbox_enabled_by_undetermined_policy is not None: + result["sandboxEnabledByUndeterminedPolicy"] = from_union([from_bool, from_none], self.sandbox_enabled_by_undetermined_policy) + 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 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: + """Result of registering a new marketplace.""" + + name: str + """Final name of the marketplace as resolved from its manifest""" + + @staticmethod + def from_dict(obj: Any) -> 'MarketplaceAddResult': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + return MarketplaceAddResult(name) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -5637,74 +6955,6 @@ def to_dict(self) -> dict: result["tools"] = from_list(lambda x: from_dict(lambda x: x, x), self.tools) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPAppsReadResourceRequest: - """MCP server and resource URI to fetch.""" - - server_name: str - """Name of the MCP server hosting the resource""" - - uri: str - """Resource URI (typically ui://...)""" - - @staticmethod - def from_dict(obj: Any) -> 'MCPAppsReadResourceRequest': - assert isinstance(obj, dict) - server_name = from_str(obj.get("serverName")) - uri = from_str(obj.get("uri")) - return MCPAppsReadResourceRequest(server_name, uri) - - def to_dict(self) -> dict: - result: dict = {} - result["serverName"] = from_str(self.server_name) - result["uri"] = from_str(self.uri) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPAppsResourceContent: - """MCP Apps resource content with URI, optional MIME type, text or base64 blob, and resource - metadata. - """ - uri: str - """The resource URI (typically ui://...)""" - - meta: dict[str, Any] | None = None - """Resource-level metadata (CSP, permissions, etc.)""" - - blob: str | None = None - """Base64-encoded binary content""" - - mime_type: str | None = None - """MIME type of the content""" - - text: str | None = None - """Text content (e.g. HTML)""" - - @staticmethod - def from_dict(obj: Any) -> 'MCPAppsResourceContent': - assert isinstance(obj, dict) - uri = from_str(obj.get("uri")) - meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) - blob = from_union([from_str, from_none], obj.get("blob")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - text = from_union([from_str, from_none], obj.get("text")) - return MCPAppsResourceContent(uri, meta, blob, mime_type, text) - - def to_dict(self) -> dict: - result: dict = {} - result["uri"] = from_str(self.uri) - if self.meta is not None: - result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) - if self.blob is not None: - result["blob"] = from_union([from_str, from_none], self.blob) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPCancelSamplingExecutionParams: @@ -5889,7 +7139,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). """ @@ -5924,6 +7174,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: @@ -5932,15 +7210,21 @@ class MCPDisableRequest: server_name: str """Name of the MCP server to disable""" + expected_installation_id: str | None = field(default=None, kw_only=True) + """Required for an owned installation; omission preserves only manual-server behaviour.""" + @staticmethod def from_dict(obj: Any) -> 'MCPDisableRequest': assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) - return MCPDisableRequest(server_name) + expected_installation_id = from_union([from_str, from_none], obj.get("expectedInstallationId")) + return MCPDisableRequest(server_name, expected_installation_id=expected_installation_id) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) + if self.expected_installation_id is not None: + result["expectedInstallationId"] = from_union([from_str, from_none], self.expected_installation_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -5986,15 +7270,21 @@ class MCPEnableRequest: server_name: str """Name of the MCP server to enable""" + expected_installation_id: str | None = field(default=None, kw_only=True) + """Exact receipt identity for explicit owned activation in this session.""" + @staticmethod def from_dict(obj: Any) -> 'MCPEnableRequest': assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) - return MCPEnableRequest(server_name) + expected_installation_id = from_union([from_str, from_none], obj.get("expectedInstallationId")) + return MCPEnableRequest(server_name, expected_installation_id=expected_installation_id) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) + if self.expected_installation_id is not None: + result["expectedInstallationId"] = from_union([from_str, from_none], self.expected_installation_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6128,163 +7418,108 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. -class MCPPlanConfigurationOperation(Enum): - """Whether the change would create a new entry or modify an existing one. - - Whether a planned configuration change would create or modify an entry - """ - ADD = "add" - UPDATE = "update" - -# Experimental: this type is part of an experimental API and may change or be removed. -class MCPPlanScope(Enum): - """Scope the change would be written to. - - Configuration scope an MCP install plan targets - - Configuration scope the plan targets. - - Configuration scope the plan targets. Defaults to user scope when omitted. - """ - USER = "user" - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPPlanResourceIdentity: - """Normalised identity of the server the plan would install. - - Normalised identity of the MCP server a plan targets, independent of how the card spelled - it. - """ - canonical_name: str - """Canonical, normalised name of the server, for example `io.github.owner/server`.""" - - server_name: str - """Local configuration key the server would be recorded under.""" - - registry_id: str | None = None - """Registry identifier of the server, when it came from a registry.""" - - version: str | None = None - """Version advertised by the card, when it declares one.""" - - @staticmethod - def from_dict(obj: Any) -> 'MCPPlanResourceIdentity': - assert isinstance(obj, dict) - canonical_name = from_str(obj.get("canonicalName")) - server_name = from_str(obj.get("serverName")) - registry_id = from_union([from_str, from_none], obj.get("registryId")) - version = from_union([from_str, from_none], obj.get("version")) - return MCPPlanResourceIdentity(canonical_name, server_name, registry_id, version) - - def to_dict(self) -> dict: - result: dict = {} - result["canonicalName"] = from_str(self.canonical_name) - result["serverName"] = from_str(self.server_name) - if self.registry_id is not None: - result["registryId"] = from_union([from_str, from_none], self.registry_id) - 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. -class MCPPlanPolicyDecision(Enum): - """What policy decided for this server. - - What policy decided for a planned server - """ - ALLOWED = "allowed" - BLOCKED = "blocked" - REQUIRES_APPROVAL = "requires-approval" - -class InstallMethod(Enum): - """Discriminator for a package-backed transport choice - - Discriminator for a remote-endpoint transport choice - """ - PACKAGE = "package" - REMOTE = "remote" - -# Experimental: this type is part of an experimental API and may change or be removed. -class MCPPlanValueCategory(Enum): - """Where the value is applied when the server is launched. +class MCPInstallationFailureReason(Enum): + """Bounded refusal categories, without echoing handles, credentials or configuration. - Where a required value is applied when the planned server is launched - """ - ENVIRONMENT_VARIABLE = "environment-variable" - HEADER = "header" - PACKAGE_ARGUMENT = "package-argument" - RUNTIME_ARGUMENT = "runtime-argument" - URL_VARIABLE = "url-variable" + Cause of the fully aborted or compensated operation. -class MCPPlanRequiredValueKind(Enum): - """Discriminator for a scalar required value + Specific bounded refusal, never a success-shaped fallback. - Discriminator for an enumerated required value + Specific bounded refusal. """ - ENUM = "enum" - SCALAR = "scalar" - -class MCPPlanRequiredValueValueType(Enum): - """Scalar type the value must conform to. - - Scalar type a required value must conform to + CANCELLED = "cancelled" + CAPABILITY_REQUIRED = "capability-required" + CONFIGURATION_CHANGED = "configuration-changed" + CONFIGURATION_MODIFIED = "configuration-modified" + CONFIRMATION_INVALID = "confirmation-invalid" + CONFIRMATION_UNAVAILABLE = "confirmation-unavailable" + FOREIGN_RUNTIME = "foreign-runtime" + INVALID_REQUEST = "invalid-request" + LIFECYCLE_UNAVAILABLE = "lifecycle-unavailable" + OPERATION_LIMIT = "operation-limit" + PLAN_EXPIRED = "plan-expired" + PLAN_REPLAYED = "plan-replayed" + POLICY_CHANGED = "policy-changed" + POLICY_CONTEXT_UNAVAILABLE = "policy-context-unavailable" + POLICY_DENIED = "policy-denied" + REGISTRY_UNAVAILABLE = "registry-unavailable" + REPLAN_REQUIRED = "replan-required" + RESOURCE_NOT_FOUND = "resource-not-found" + SECRET_STORE_UNAVAILABLE = "secret-store-unavailable" + SOURCE_CHANGED = "source-changed" + SOURCE_REVALIDATION_UNAVAILABLE = "source-revalidation-unavailable" + SOURCE_UNAVAILABLE = "source-unavailable" + WRITE_FAILED = "write-failed" + +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPInstallationState(Enum): + """Ownership or setup state, never inferred proof of tool usability. + + Configuration ownership and setup observations, distinct from tool permissions. + """ + ACTIVATION_FAILED = "activation-failed" + ACTIVE = "active" + AUTHENTICATION_REQUIRED = "authentication-required" + CONFIGURATION_MODIFIED = "configuration-modified" + NEEDS_SETUP = "needs-setup" + RECOVERY_REQUIRED = "recovery-required" + +class MCPInstallationManagementOutcomeKind(Enum): + INSTALL_PREPARED = "install-prepared" + LISTED = "listed" + OPERATION = "operation" + RECOVERED = "recovered" + RECOVERY_REQUIRED = "recovery-required" + REFUSED = "refused" + UNINSTALL_PLANNED = "uninstall-planned" + +class MCPInstallationOutcomeKind(Enum): + CANCELLED = "cancelled" + DECLINED = "declined" + INSTALLED = "installed" + RECOVERY_REQUIRED = "recovery-required" + REFUSED = "refused" + ROLLED_BACK = "rolled-back" + UNINSTALLED = "uninstalled" + +class Phase(Enum): + APPLYING = "applying" + AWAITING_CONFIRMATION = "awaiting-confirmation" + COMPLETED = "completed" + PREPARED = "prepared" + PREPARING = "preparing" + REVALIDATING = "revalidating" - Discriminator for an enumerated required value - """ - BOOLEAN = "boolean" - ENUM = "enum" - NUMBER = "number" - PATH = "path" - STRING = "string" +class MCPInstallationManagementResultKind(Enum): + INVALID_REQUEST = "invalid-request" + NEGOTIATION_REFUSED = "negotiation-refused" + OUTCOME = "outcome" # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPPlanSecretPlaceholder: - """A secret a transport choice needs, referenced by placeholder. No secret value ever - appears in a plan, and the placeholder resolves against the keychain only when a plan is - applied. +class MCPInstallationSecret: + """A request-local value for one exact reviewed placeholder. Never logged or persisted in a + plan. """ - key: str - """Key the secret is supplied under. Inert untrusted data.""" - placeholder: str - """The runtime-assigned `${secret:}` placeholder written into configuration in place of - the value. - """ - title: str | None = None - """Human-readable label from the card. Inert untrusted text.""" + """Exact placeholder from the selected choice, not a caller-chosen backend identifier.""" + + value: str + """Fresh explicit secret value. It is omitted from confirmation reviews and telemetry.""" @staticmethod - def from_dict(obj: Any) -> 'MCPPlanSecretPlaceholder': + def from_dict(obj: Any) -> 'MCPInstallationSecret': assert isinstance(obj, dict) - key = from_str(obj.get("key")) placeholder = from_str(obj.get("placeholder")) - title = from_union([from_str, from_none], obj.get("title")) - return MCPPlanSecretPlaceholder(key, placeholder, title) + value = from_str(obj.get("value")) + return MCPInstallationSecret(placeholder, value) def to_dict(self) -> dict: result: dict = {} - result["key"] = from_str(self.key) result["placeholder"] = from_str(self.placeholder) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) + result["value"] = from_str(self.value) return result -class MCPPlanETransport(Enum): - """Local process transport this package choice would use. - - Transport exposed by a locally launched package - - Endpoint transport this remote choice would use. - - Transport exposed by a remote endpoint - """ - HTTP = "http" - SSE = "sse" - STDIO = "stdio" - STREAMABLE_HTTP = "streamable-http" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPIsServerRunningRequest: @@ -6378,6 +7613,30 @@ def to_dict(self) -> dict: result["serverName"] = from_union([from_str, from_none], self.server_name) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPOauthCancelLoginRequest: + """Targets only the original prepared/applying owned login on this exact session requester.""" + + expected_installation_id: str + """The same authoritative installation identity supplied during preparation.""" + + login_id: str + """Runtime-issued login handle known before the effectful login request begins.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPOauthCancelLoginRequest': + assert isinstance(obj, dict) + expected_installation_id = from_str(obj.get("expectedInstallationId")) + login_id = from_str(obj.get("loginId")) + return MCPOauthCancelLoginRequest(expected_installation_id, login_id) + + def to_dict(self) -> dict: + result: dict = {} + result["expectedInstallationId"] = from_str(self.expected_installation_id) + result["loginId"] = from_str(self.login_id) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPOauthHandlePendingResult: @@ -6399,30 +7658,81 @@ def to_dict(self) -> dict: result["success"] = from_bool(self.success) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPOwnedOauthLoginStatus(Enum): + """Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. + + Outcome of starting the original prepared owned login. + """ + AWAITING_BROWSER = "awaiting-browser" + CONNECTED = "connected" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPOauthLoginResult: - """OAuth authorization URL the caller should open, or empty when cached tokens already - authenticated the server. +class MCPOauthPrepareLoginRequest: + """Effect-free preparation bound to the existing local session, requester and installation, + with frozen options. """ - 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. + expected_installation_id: str + """Exact installation identity from owned inventory, never a server-name alias.""" + + server_name: str + """Name recorded by the authoritative owned installation receipt.""" + + callback_success_message: str | None = None + """Text shown on the loopback callback page after successful authorisation.""" + + client_name: str | None = None + """Display name used by the incumbent OAuth client-registration flow.""" + + force_reauth: bool | None = None + """Request a new authorisation rather than accepting a usable cached grant.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPOauthPrepareLoginRequest': + assert isinstance(obj, dict) + expected_installation_id = from_str(obj.get("expectedInstallationId")) + server_name = from_str(obj.get("serverName")) + callback_success_message = from_union([from_str, from_none], obj.get("callbackSuccessMessage")) + client_name = from_union([from_str, from_none], obj.get("clientName")) + force_reauth = from_union([from_bool, from_none], obj.get("forceReauth")) + return MCPOauthPrepareLoginRequest(expected_installation_id, server_name, callback_success_message, client_name, force_reauth) + + def to_dict(self) -> dict: + result: dict = {} + result["expectedInstallationId"] = from_str(self.expected_installation_id) + result["serverName"] = from_str(self.server_name) + if self.callback_success_message is not None: + result["callbackSuccessMessage"] = from_union([from_str, from_none], self.callback_success_message) + if self.client_name is not None: + result["clientName"] = from_union([from_str, from_none], self.client_name) + if self.force_reauth is not None: + result["forceReauth"] = from_union([from_bool, from_none], self.force_reauth) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPOauthPrepareLoginResult: + """An inert runtime-issued login handle. Preparation alone performs no activation or OAuth + work. """ + expires_at: datetime + """Original expiry, not extended by consumption, retries or cancellation.""" + + login_id: str + """Retain with the original requester and use for one login or cancellation.""" @staticmethod - def from_dict(obj: Any) -> 'MCPOauthLoginResult': + def from_dict(obj: Any) -> 'MCPOauthPrepareLoginResult': assert isinstance(obj, dict) - authorization_url = from_union([from_str, from_none], obj.get("authorizationUrl")) - return MCPOauthLoginResult(authorization_url) + expires_at = from_datetime(obj.get("expiresAt")) + login_id = from_str(obj.get("loginId")) + return MCPOauthPrepareLoginResult(expires_at, login_id) def to_dict(self) -> dict: result: dict = {} - if self.authorization_url is not None: - result["authorizationUrl"] = from_union([from_str, from_none], self.authorization_url) + result["expiresAt"] = self.expires_at.isoformat() + result["loginId"] = from_str(self.login_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6443,15 +7753,21 @@ class MCPOauthProbeRequest: server_name: str """Name of the configured remote MCP server to probe.""" + expected_installation_id: str | None = field(default=None, kw_only=True) + """Exact owned receipt identity; probing never activates a dormant installation.""" + @staticmethod def from_dict(obj: Any) -> 'MCPOauthProbeRequest': assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) - return MCPOauthProbeRequest(server_name) + expected_installation_id = from_union([from_str, from_none], obj.get("expectedInstallationId")) + return MCPOauthProbeRequest(server_name, expected_installation_id=expected_installation_id) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) + if self.expected_installation_id is not None: + result["expectedInstallationId"] = from_union([from_str, from_none], self.expected_installation_id) return result class MCPOauthProbeResultStatus(Enum): @@ -6657,16 +7973,6 @@ class MCPPlanRemoteInstallMethod(Enum): REMOTE = "remote" -# Experimental: this type is part of an experimental API and may change or be removed. -class MCPPlanRemoteTransport(Enum): - """Endpoint transport this remote choice would use. - - Transport exposed by a remote endpoint - """ - HTTP = "http" - SSE = "sse" - STREAMABLE_HTTP = "streamable-http" - # Experimental: this type is part of an experimental API and may change or be removed. class MCPPlanRequiredValueScalarKind(Enum): """Discriminator for a scalar required value""" @@ -6684,6 +7990,139 @@ class MCPPlanScalarValueTypeEnum(Enum): PATH = "path" STRING = "string" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPreparedInstall: + """Known original operation identity, returned before callback or effects. + + Inert, runtime-owned admission. The operation ID is known before confirmation or effects. + """ + expires_at_epoch_ms: int + """Original plan expiry in Unix epoch milliseconds; preparation does not extend it.""" + + operation_id: str + """Original connection-owned operation, known before the first confirmation callback.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPreparedInstall': + assert isinstance(obj, dict) + expires_at_epoch_ms = from_int(obj.get("expiresAtEpochMs")) + operation_id = from_str(obj.get("operationId")) + return MCPPreparedInstall(expires_at_epoch_ms, operation_id) + + def to_dict(self) -> dict: + result: dict = {} + result["expiresAtEpochMs"] = from_int(self.expires_at_epoch_ms) + 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. @@ -6705,7 +8144,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). """ @@ -6744,50 +8183,6 @@ def to_dict(self) -> dict: result["removed"] = from_bool(self.removed) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPResourceContent: - """MCP resource content with URI, optional MIME type, text or base64 blob, and resource - metadata. - """ - uri: str - """The resource URI""" - - meta: dict[str, Any] | None = None - """Resource-level metadata (CSP, permissions, etc.)""" - - blob: str | None = None - """Base64-encoded binary content""" - - mime_type: str | None = None - """MIME type of the content""" - - text: str | None = None - """Text content (e.g. HTML)""" - - @staticmethod - def from_dict(obj: Any) -> 'MCPResourceContent': - assert isinstance(obj, dict) - uri = from_str(obj.get("uri")) - meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) - blob = from_union([from_str, from_none], obj.get("blob")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - text = from_union([from_str, from_none], obj.get("text")) - return MCPResourceContent(uri, meta, blob, mime_type, text) - - def to_dict(self) -> dict: - result: dict = {} - result["uri"] = from_str(self.uri) - if self.meta is not None: - result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) - if self.blob is not None: - result["blob"] = from_union([from_str, from_none], self.blob) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPResourcesListRequest: @@ -6839,39 +8234,38 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPResourcesReadRequest: - """MCP server and resource URI to fetch.""" +class MCPSamplingExecutionAction(Enum): + """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. + """ + CANCELLED = "cancelled" + FAILURE = "failure" + SUCCESS = "success" - server_name: str - """Name of the MCP server hosting the resource""" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPServerOwnership: + """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. - uri: str - """Resource URI""" + Owned installation that a listed MCP server's live configuration came from. + """ + installation_id: str + """Stable installation identifier from the owned installation receipt.""" @staticmethod - def from_dict(obj: Any) -> 'MCPResourcesReadRequest': + def from_dict(obj: Any) -> 'MCPServerOwnership': assert isinstance(obj, dict) - server_name = from_str(obj.get("serverName")) - uri = from_str(obj.get("uri")) - return MCPResourcesReadRequest(server_name, uri) + installation_id = from_str(obj.get("installationId")) + return MCPServerOwnership(installation_id) def to_dict(self) -> dict: result: dict = {} - result["serverName"] = from_str(self.server_name) - result["uri"] = from_str(self.uri) + result["installationId"] = from_str(self.installation_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. -class MCPSamplingExecutionAction(Enum): - """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. - """ - CANCELLED = "cancelled" - FAILURE = "failure" - SUCCESS = "success" - # Experimental: this type is part of an experimental API and may change or be removed. class MCPServerConfigHTTPType(Enum): """Remote transport type. Defaults to "http" when omitted.""" @@ -6921,15 +8315,21 @@ class MCPStopServerRequest: server_name: str """Name of the MCP server to stop""" + expected_installation_id: str | None = field(default=None, kw_only=True) + """Exact owned receipt identity. Stop also forgets this session's durable activation.""" + @staticmethod def from_dict(obj: Any) -> 'MCPStopServerRequest': assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) - return MCPStopServerRequest(server_name) + expected_installation_id = from_union([from_str, from_none], obj.get("expectedInstallationId")) + return MCPStopServerRequest(server_name, expected_installation_id=expected_installation_id) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) + if self.expected_installation_id is not None: + result["expectedInstallationId"] = from_union([from_str, from_none], self.expected_installation_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -7001,7 +8401,7 @@ class Categories: `totalTokens`. """ buffer: int - """Output reserve plus post-blocking-threshold buffer.""" + """Overlapping output reservation plus post-blocking-threshold buffer.""" custom_instructions: int """Custom-instructions tokens (0 when none are configured).""" @@ -7141,8 +8541,9 @@ class SessionContextInfo: """Token-usage breakdown for the session's current context window""" buffer_tokens: int - """Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%)""" - + """Output reservation overlapping the displayed prompt allowance plus tokens after the + effective input budget's buffer-exhaustion blocking threshold (default 95%). + """ compaction_threshold: int """Token count at which background compaction starts (configurable percentage of promptTokenLimit) @@ -7151,8 +8552,9 @@ class SessionContextInfo: """Tokens consumed by user/assistant/tool messages""" limit: int - """Prompt token limit plus the model's full output token limit.""" - + """Advertised prompt allowance for the selected context tier, without adding output tokens. + The denominator for context-usage displays. + """ mcp_tools_tokens: int """Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) @@ -7161,8 +8563,10 @@ class SessionContextInfo: """The model used for token counting""" prompt_token_limit: int - """Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified)""" - + """Effective input budget: the selected tier's prompt allowance bounded by the combined + context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when + limits are unspecified. + """ system_tokens: int """Tokens consumed by the system prompt""" @@ -7603,35 +9007,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: @@ -7735,6 +9110,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: @@ -7986,64 +9632,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: @@ -8139,11 +9727,11 @@ class ApprovalKind(Enum): EXTENSION_ENV_ACCESS = "extension-env-access" EXTENSION_MANAGEMENT = "extension-management" EXTENSION_PERMISSION_ACCESS = "extension-permission-access" - FACTORY = "factory" MCP = "mcp" MCP_SAMPLING = "mcp-sampling" MEMORY = "memory" READ = "read" + WORKFLOW = "workflow" WRITE = "write" class PermissionDecisionKind(Enum): @@ -8154,6 +9742,7 @@ class PermissionDecisionKind(Enum): APPROVE_FOR_SESSION = "approve-for-session" APPROVE_ONCE = "approve-once" APPROVE_PERMANENTLY = "approve-permanently" + APPROVE_READ_ONLY_FOR_SESSION = "approve-read-only-for-session" CANCELLED = "cancelled" DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" DENIED_BY_PERMISSION_REQUEST_HOOK = "denied-by-permission-request-hook" @@ -8181,15 +9770,15 @@ class PermissionDecisionApproveForLocationApprovalExtensionManagementKind(Enum): class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(Enum): EXTENSION_PERMISSION_ACCESS = "extension-permission-access" -class PermissionDecisionApproveForLocationApprovalFactoryKind(Enum): - FACTORY = "factory" - class PermissionDecisionApproveForLocationApprovalMCPSamplingKind(Enum): MCP_SAMPLING = "mcp-sampling" class PermissionDecisionApproveForLocationApprovalReadKind(Enum): READ = "read" +class PermissionDecisionApproveForLocationApprovalWorkflowKind(Enum): + WORKFLOW = "workflow" + class PermissionDecisionApproveForLocationApprovalWriteKind(Enum): WRITE = "write" @@ -8202,6 +9791,9 @@ class PermissionDecisionApproveOnceKind(Enum): class PermissionDecisionApprovePermanentlyKind(Enum): APPROVE_PERMANENTLY = "approve-permanently" +class PermissionDecisionApproveReadOnlyForSessionKind(Enum): + APPROVE_READ_ONLY_FOR_SESSION = "approve-read-only-for-session" + class PermissionDecisionApprovedKind(Enum): APPROVED = "approved" @@ -9370,16 +10962,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.""" @@ -9389,47 +10971,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: @@ -9574,6 +11115,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: @@ -10658,15 +12235,18 @@ class SandboxConfigAuth: `shell.credentials`; the two are additive. """ gh: bool | None = None - """Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the - OS keyring the sandbox blocks. Default: false (opt-in). + """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). """ git: bool | None = None - """Whether to inject git credentials as an `http..extraheader` so authenticated HTTPS - git works inside the sandbox without the shell-based credential helper the sandbox - blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, - GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's - own helper before the sandbox is applied. Default: false (opt-in). + """Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child + receives a fake `http..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). """ @staticmethod @@ -10684,6 +12264,28 @@ def to_dict(self) -> dict: result["git"] = from_union([from_bool, from_none], self.git) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxMaskedEnvVar: + """Destinations authorized to receive one masked environment credential.""" + + inject_hosts: list[str] + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SandboxMaskedEnvVar': + assert isinstance(obj, dict) + inject_hosts = from_list(from_str, obj.get("injectHosts")) + return SandboxMaskedEnvVar(inject_hosts) + + def to_dict(self) -> dict: + result: dict = {} + result["injectHosts"] = from_list(from_str, self.inject_hosts) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SandboxConfigUserPolicyExperimentalSeatbelt: @@ -10815,12 +12417,14 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. -class _SandboxConfigSource(Enum): - """Origin of the sandbox choice supplied by an internal client. +class SandboxConfigSource(Enum): + """Origin of the sandbox choice supplied by the host. This value describes preference or + session intent; it does not authorize bypassing managed policy. - Origin of the sandbox choice. The runtime uses this only for internal telemetry - provenance; managed policy is derived independently. + 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. """ NEVER_CONFIGURED = "never_configured" REPOSITORY_POLICY = "repository_policy" @@ -10884,11 +12488,34 @@ 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 SandboxGrantPathForRequestResult: + """Result of accepting a sandbox path grant.""" + + success: bool + """Whether this call resolved the pending request and added the path to the session's + sandbox policy. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SandboxGrantPathForRequestResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return SandboxGrantPathForRequestResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @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 @@ -10897,9 +12524,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.""" @@ -10925,6 +12554,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 @@ -11652,6 +13311,30 @@ def to_dict(self) -> dict: result["includeSkills"] = from_union([from_bool, from_none], self.include_skills) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionConnectedIDEInfo: + """The IDE a host is connected to, as reported to the session.""" + + ide_name: str + """Display name of the connected IDE, for example `VS Code`.""" + + workspace_folder: str + """Absolute path of the workspace folder the IDE has open.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionConnectedIDEInfo': + assert isinstance(obj, dict) + ide_name = from_str(obj.get("ideName")) + workspace_folder = from_str(obj.get("workspaceFolder")) + return SessionConnectedIDEInfo(ide_name, workspace_folder) + + def to_dict(self) -> dict: + result: dict = {} + result["ideName"] = from_str(self.ide_name) + result["workspaceFolder"] = from_str(self.workspace_folder) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSAppendFileRequest: @@ -11775,6 +13458,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: @@ -11923,14 +13630,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 @@ -12044,6 +13758,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: @@ -12267,6 +14017,12 @@ class SessionManagedPermissions: restrict something, so a mode this runtime cannot interpret fails closed to the most restrictive one it knows. Omit the key entirely to impose no restriction. """ + limit_to: list[str] | None = None + """Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or + `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected + because every network request must be enforceable at host-level egress. Multiple managed + sources intersect their lists; an empty list denies all hosts. + """ @staticmethod def from_dict(obj: Any) -> 'SessionManagedPermissions': @@ -12275,7 +14031,8 @@ def from_dict(obj: Any) -> 'SessionManagedPermissions': ask = from_union([lambda x: from_list(from_str, x), from_none], obj.get("ask")) deny = from_union([lambda x: from_list(from_str, x), from_none], obj.get("deny")) disable_bypass_permissions_mode = from_union([from_str, from_none], obj.get("disableBypassPermissionsMode")) - return SessionManagedPermissions(allow, ask, deny, disable_bypass_permissions_mode) + limit_to = from_union([lambda x: from_list(from_str, x), from_none], obj.get("limitTo")) + return SessionManagedPermissions(allow, ask, deny, disable_bypass_permissions_mode, limit_to) def to_dict(self) -> dict: result: dict = {} @@ -12287,6 +14044,100 @@ def to_dict(self) -> dict: result["deny"] = from_union([lambda x: from_list(from_str, x), from_none], self.deny) if self.disable_bypass_permissions_mode is not None: result["disableBypassPermissionsMode"] = from_union([from_str, from_none], self.disable_bypass_permissions_mode) + if self.limit_to is not None: + result["limitTo"] = from_union([lambda x: from_list(from_str, x), from_none], self.limit_to) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionMCPOauthCancelLoginRequest: + """Targets only the original prepared/applying owned login on this exact session requester.""" + + expected_installation_id: str + """The same authoritative installation identity supplied during preparation.""" + + login_id: str + """Runtime-issued login handle known before the effectful login request begins.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionMCPOauthCancelLoginRequest': + assert isinstance(obj, dict) + expected_installation_id = from_str(obj.get("expectedInstallationId")) + login_id = from_str(obj.get("loginId")) + return SessionMCPOauthCancelLoginRequest(expected_installation_id, login_id) + + def to_dict(self) -> dict: + result: dict = {} + result["expectedInstallationId"] = from_str(self.expected_installation_id) + result["loginId"] = from_str(self.login_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionMCPOauthPrepareLoginRequest: + """Effect-free preparation bound to the existing local session, requester and installation, + with frozen options. + """ + expected_installation_id: str + """Exact installation identity from owned inventory, never a server-name alias.""" + + server_name: str + """Name recorded by the authoritative owned installation receipt.""" + + callback_success_message: str | None = None + """Text shown on the loopback callback page after successful authorisation.""" + + client_name: str | None = None + """Display name used by the incumbent OAuth client-registration flow.""" + + force_reauth: bool | None = None + """Request a new authorisation rather than accepting a usable cached grant.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionMCPOauthPrepareLoginRequest': + assert isinstance(obj, dict) + expected_installation_id = from_str(obj.get("expectedInstallationId")) + server_name = from_str(obj.get("serverName")) + callback_success_message = from_union([from_str, from_none], obj.get("callbackSuccessMessage")) + client_name = from_union([from_str, from_none], obj.get("clientName")) + force_reauth = from_union([from_bool, from_none], obj.get("forceReauth")) + return SessionMCPOauthPrepareLoginRequest(expected_installation_id, server_name, callback_success_message, client_name, force_reauth) + + def to_dict(self) -> dict: + result: dict = {} + result["expectedInstallationId"] = from_str(self.expected_installation_id) + result["serverName"] = from_str(self.server_name) + if self.callback_success_message is not None: + result["callbackSuccessMessage"] = from_union([from_str, from_none], self.callback_success_message) + if self.client_name is not None: + result["clientName"] = from_union([from_str, from_none], self.client_name) + if self.force_reauth is not None: + result["forceReauth"] = from_union([from_bool, from_none], self.force_reauth) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionMCPOauthPrepareLoginResult: + """An inert runtime-issued login handle. Preparation alone performs no activation or OAuth + work. + """ + expires_at: datetime + """Original expiry, not extended by consumption, retries or cancellation.""" + + login_id: str + """Retain with the original requester and use for one login or cancellation.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionMCPOauthPrepareLoginResult': + assert isinstance(obj, dict) + expires_at = from_datetime(obj.get("expiresAt")) + login_id = from_str(obj.get("loginId")) + return SessionMCPOauthPrepareLoginResult(expires_at, login_id) + + def to_dict(self) -> dict: + result: dict = {} + result["expiresAt"] = self.expires_at.isoformat() + result["loginId"] = from_str(self.login_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -12559,6 +14410,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" @@ -12949,6 +14801,77 @@ class SessionVisibilityStatus(Enum): REPO = "repo" UNSHARED = "unshared" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionWorkingDirectoryContextWithClient: + """Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, + `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, + `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + + A working-directory context together with the client that produced it. + """ + cwd: str + """Current working directory path""" + + base_commit: str | None = None + """Merge-base commit SHA""" + + branch: str | None = None + """Current git branch name""" + + client_name: str | None = None + """Name of the client that created the session""" + + git_root: str | None = None + """Root directory of the git repository""" + + head_commit: str | None = None + """Head commit of the current git branch""" + + host_type: str | None = None + """Hosting platform type of the repository""" + + repository: str | None = None + """Repository identifier derived from the git remote URL""" + + repository_host: str | None = None + """Raw host string from the git remote URL""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionWorkingDirectoryContextWithClient': + assert isinstance(obj, dict) + cwd = from_str(obj.get("cwd")) + base_commit = from_union([from_str, from_none], obj.get("baseCommit")) + branch = from_union([from_str, from_none], obj.get("branch")) + client_name = from_union([from_str, from_none], obj.get("clientName")) + git_root = from_union([from_str, from_none], obj.get("gitRoot")) + head_commit = from_union([from_str, from_none], obj.get("headCommit")) + host_type = from_union([from_str, from_none], obj.get("hostType")) + repository = from_union([from_str, from_none], obj.get("repository")) + repository_host = from_union([from_str, from_none], obj.get("repositoryHost")) + return SessionWorkingDirectoryContextWithClient(cwd, base_commit, branch, client_name, git_root, head_commit, host_type, repository, repository_host) + + def to_dict(self) -> dict: + result: dict = {} + result["cwd"] = from_str(self.cwd) + if self.base_commit is not None: + result["baseCommit"] = from_union([from_str, from_none], self.base_commit) + if self.branch is not None: + result["branch"] = from_union([from_str, from_none], self.branch) + if self.client_name is not None: + result["clientName"] = from_union([from_str, from_none], self.client_name) + if self.git_root is not None: + result["gitRoot"] = from_union([from_str, from_none], self.git_root) + if self.head_commit is not None: + result["headCommit"] = from_union([from_str, from_none], self.head_commit) + if self.host_type is not None: + result["hostType"] = from_union([from_str, from_none], self.host_type) + if self.repository is not None: + result["repository"] = from_union([from_str, from_none], self.repository) + if self.repository_host is not None: + result["repositoryHost"] = from_union([from_str, from_none], self.repository_host) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionsBulkDeleteRequest: @@ -13050,6 +14973,25 @@ 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 SessionsCreateWorkspaceResult: + """The workspace record that was written.""" + + workspace_json: str + """The created workspace record, as JSON""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsCreateWorkspaceResult': + assert isinstance(obj, dict) + workspace_json = from_str(obj.get("workspaceJson")) + return SessionsCreateWorkspaceResult(workspace_json) + + def to_dict(self) -> dict: + result: dict = {} + result["workspaceJson"] = from_str(self.workspace_json) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionsDeleteRequest: @@ -13458,6 +15400,51 @@ 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 SessionsLoadWorkspaceRequest: + """Where the session's state lives, as a root directory and the session ID under it.""" + + session_id: str + """Session ID naming the state directory under the sessions home. Rejected when it is + absolute or contains a parent component, so it cannot escape the sessions home. + """ + sessions_home: str + """Root directory every session's state directory sits under""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsLoadWorkspaceRequest': + assert isinstance(obj, dict) + session_id = from_str(obj.get("sessionId")) + sessions_home = from_str(obj.get("sessionsHome")) + return SessionsLoadWorkspaceRequest(session_id, sessions_home) + + def to_dict(self) -> dict: + result: dict = {} + result["sessionId"] = from_str(self.session_id) + result["sessionsHome"] = from_str(self.sessions_home) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsLoadWorkspaceResult: + """The workspace record on disk, omitted when the session has none.""" + + workspace_json: str | None = None + """The workspace record, as JSON. Omitted when the record does not exist.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsLoadWorkspaceResult': + assert isinstance(obj, dict) + workspace_json = from_union([from_str, from_none], obj.get("workspaceJson")) + return SessionsLoadWorkspaceResult(workspace_json) + + def to_dict(self) -> dict: + result: dict = {} + if self.workspace_json is not None: + result["workspaceJson"] = from_union([from_str, from_none], self.workspace_json) + return result + class SessionsOpenAttachKind(Enum): ATTACH = "attach" @@ -13716,6 +15703,21 @@ def to_dict(self) -> dict: result["expectedFromSessionId"] = from_union([from_str, from_none], self.expected_from_session_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsUpdateWorkspaceFieldsResult: + """The merge completed. The record carries the supplied workspace-schema fields, but a + stored `fork_count` stays. + """ + @staticmethod + def from_dict(obj: Any) -> 'SessionsUpdateWorkspaceFieldsResult': + assert isinstance(obj, dict) + return SessionsUpdateWorkspaceFieldsResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + class SettableTokenAuthInfoType(Enum): TOKEN = "token" @@ -13738,45 +15740,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': @@ -13952,11 +15923,22 @@ class SkillDiscoveryScope(Enum): PERSONAL_COPILOT = "personal-copilot" PROJECT = "project" +class SkillInstallationManagementOutcomeKind(Enum): + ENABLED_CHANGED = "enabled-changed" + INSTALL_PLANNED = "install-planned" + LISTED = "listed" + OPERATION = "operation" + RECOVERED = "recovered" + RECOVERY_REQUIRED = "recovery-required" + REFUSED = "refused" + ROLLED_BACK = "rolled-back" + UNINSTALL_PLANNED = "uninstall-planned" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SkillProviderDescriptor: - """Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched - separately and lazily. + """Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is + fetched separately and lazily. """ description: str """Description used in skill catalogs without fetching content.""" @@ -14043,21 +16025,25 @@ def to_dict(self) -> dict: # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass class _SkillProviderReadResult: - """Complete text-only SKILL.md content returned by an SDK session's skill provider. Related - files and assets are not supported. + """Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter + is optional: fields it omits come from the catalog descriptor, fields it declares must + match the descriptor, and `allowed-tools` is read only from frontmatter. Related files + and assets are not supported. + """ + markdown: str | None = None + """SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill + with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. """ - markdown: str - """Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit.""" @staticmethod def from_dict(obj: Any) -> '_SkillProviderReadResult': assert isinstance(obj, dict) - markdown = from_str(obj.get("markdown")) + markdown = from_union([from_none, from_str], obj.get("markdown")) return _SkillProviderReadResult(markdown) def to_dict(self) -> dict: result: dict = {} - result["markdown"] = from_str(self.markdown) + result["markdown"] = from_union([from_none, from_str], self.markdown) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -15610,28 +17596,6 @@ 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 UserSettingsSetResult: - """Outcome of writing user settings.""" - - shadowed_keys: list[str] - """Top-level keys whose write landed in settings.json but is shadowed by a value still - present in the legacy config.json (config.json wins on read). The write does not take - effect until the legacy value is removed. - """ - - @staticmethod - def from_dict(obj: Any) -> 'UserSettingsSetResult': - assert isinstance(obj, dict) - shadowed_keys = from_list(from_str, obj.get("shadowedKeys")) - return UserSettingsSetResult(shadowed_keys) - - def to_dict(self) -> dict: - result: dict = {} - result["shadowedKeys"] = from_list(from_str, self.shadowed_keys) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkflowAbortRequest: @@ -15790,44 +17754,22 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class WorkflowDeclaredLimits: - """Declared or approved workflow resource ceilings. +class WorkflowDurableOperation(Enum): + """Execution-critical workflow storage operation. - Resource ceilings declared by the workflow. + Execution-critical durable operation that failed. """ - max_ai_credits: float | None = None - """Maximum AI credits consumed by subagents and descendants.""" - - max_concurrent_subagents: int | None = None - """Maximum concurrently active subagents.""" - - max_total_subagents: int | None = None - """Maximum total subagents spawned by the run.""" - - timeout_seconds: float | None = None - """Maximum accumulated active execution time in seconds.""" - - @staticmethod - def from_dict(obj: Any) -> 'WorkflowDeclaredLimits': - assert isinstance(obj, dict) - max_ai_credits = from_union([from_float, from_none], obj.get("maxAiCredits")) - max_concurrent_subagents = from_union([from_int, from_none], obj.get("maxConcurrentSubagents")) - max_total_subagents = from_union([from_int, from_none], obj.get("maxTotalSubagents")) - timeout_seconds = from_union([from_float, from_none], obj.get("timeoutSeconds")) - return WorkflowDeclaredLimits(max_ai_credits, max_concurrent_subagents, max_total_subagents, timeout_seconds) - - def to_dict(self) -> dict: - result: dict = {} - if self.max_ai_credits is not None: - result["maxAiCredits"] = from_union([to_float, from_none], self.max_ai_credits) - if self.max_concurrent_subagents is not None: - result["maxConcurrentSubagents"] = from_union([from_int, from_none], self.max_concurrent_subagents) - if self.max_total_subagents is not None: - result["maxTotalSubagents"] = from_union([from_int, from_none], self.max_total_subagents) - if self.timeout_seconds is not None: - result["timeoutSeconds"] = from_union([to_float, from_none], self.timeout_seconds) - return result + ADD_ELAPSED = "addElapsed" + CHARGE_CREDIT = "chargeCredit" + CREATE_RUN = "createRun" + FINISH_RUN = "finishRun" + JOURNAL_GET = "journalGet" + JOURNAL_PUT = "journalPut" + MARK_RUN_STARTED = "markRunStarted" + RECONCILE_CREDIT_TOTAL = "reconcileCreditTotal" + REFRESH_LEASE = "refreshLease" + RELEASE_AGENT = "releaseAgent" + RESERVE_AGENT = "reserveAgent" # Experimental: this type is part of an experimental API and may change or be removed. @dataclass @@ -16101,6 +18043,22 @@ def to_dict(self) -> dict: result["subagents"] = from_int(self.subagents) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class WorkflowRunStatus(Enum): + """Current workflow run status. + + Current or terminal state of a workflow run. + + Current or terminal workflow run status. + """ + CANCELLED = "cancelled" + COMPLETED = "completed" + ERROR = "error" + HALTED = "halted" + PAUSED = "paused" + PENDING = "pending" + RUNNING = "running" + class WorkflowRunFailureType(Enum): WORKFLOW_ACCOUNTING_INCOMPLETE = "workflow_accounting_incomplete" WORKFLOW_DURABLE_FAILURE = "workflow_durable_failure" @@ -16108,6 +18066,21 @@ class WorkflowRunFailureType(Enum): WORKFLOW_PROVIDER_DISCONNECTED = "workflow_provider_disconnected" WORKFLOW_RESUME_DECLINED = "workflow_resume_declined" +class PauseInfoType(Enum): + CHECKPOINT = "checkpoint" + USER = "user" + +# Experimental: this type is part of an experimental API and may change or be removed. +class WorkflowLogLineKind(Enum): + """Progress line kind. + + Kind of workflow progress line. + + Progress record kind. + """ + LOG = "log" + PHASE = "phase" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkflowPauseCheckpointRequest: @@ -16156,6 +18129,17 @@ def to_dict(self) -> dict: result["runId"] = from_str(self.run_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class WorkflowPhaseStatus(Enum): + """Derived lifecycle state of the phase. + + Derived lifecycle state of a workflow phase. + """ + ACTIVE = "active" + COMPLETED = "completed" + PENDING = "pending" + SKIPPED = "skipped" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkflowRunLimits: @@ -16707,6 +18691,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: @@ -16777,6 +18790,235 @@ def to_dict(self) -> dict: result["quotaSnapshots"] = from_dict(lambda x: to_class(AccountQuotaSnapshot, x), self.quota_snapshots) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AccountStatus: + """One signed-in account in the roster forest. + + The active account, or absent when not logged in. + """ + active: bool + """Whether this is the active account.""" + + host: str + """Authentication host URL.""" + + kind: AccountKind + """The provider kind of this account.""" + + login: str + """Authenticated login/username.""" + + selection_id: str + """Opaque selection id used to switch to, or log out, this account.""" + + derived_from: str | None = None + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'AccountStatus': + assert isinstance(obj, dict) + active = from_bool(obj.get("active")) + host = from_str(obj.get("host")) + kind = AccountKind(obj.get("kind")) + login = from_str(obj.get("login")) + selection_id = from_str(obj.get("selectionId")) + derived_from = from_union([from_str, from_none], obj.get("derivedFrom")) + return AccountStatus(active, host, kind, login, selection_id, derived_from) + + def to_dict(self) -> dict: + result: dict = {} + result["active"] = from_bool(self.active) + 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) + if self.derived_from is not None: + result["derivedFrom"] = from_union([from_str, from_none], self.derived_from) + return result + +@dataclass +class ItemElement: + """One signed-in account in the roster forest. + + The active account, or absent when not logged in. + + A provider offered for interactive login. + """ + kind: AccountKind + """The provider kind of this account. + + The neutral provider kind. + """ + active: bool | None = None + """Whether this is the active account.""" + + derived_from: str | None = None + """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. + """ + host: str | None = None + """Authentication host URL.""" + + login: str | None = None + """Authenticated login/username.""" + + selection_id: str | None = None + """Opaque selection id used to switch to, or log out, this account.""" + + available: bool | None = None + """Whether this provider is currently available to sign in with.""" + + label: str | None = None + """Human-readable menu label, owned by the runtime so every consumer renders identical text.""" + + @staticmethod + def from_dict(obj: Any) -> 'ItemElement': + assert isinstance(obj, dict) + kind = AccountKind(obj.get("kind")) + active = from_union([from_bool, from_none], obj.get("active")) + derived_from = from_union([from_str, from_none], obj.get("derivedFrom")) + host = from_union([from_str, from_none], obj.get("host")) + login = from_union([from_str, from_none], obj.get("login")) + selection_id = from_union([from_str, from_none], obj.get("selectionId")) + available = from_union([from_bool, from_none], obj.get("available")) + label = from_union([from_str, from_none], obj.get("label")) + return ItemElement(kind, active, derived_from, host, login, selection_id, available, label) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(AccountKind, self.kind) + if self.active is not None: + result["active"] = from_union([from_bool, from_none], self.active) + if self.derived_from is not None: + result["derivedFrom"] = from_union([from_str, from_none], self.derived_from) + 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) + if self.selection_id is not None: + result["selectionId"] = from_union([from_str, from_none], self.selection_id) + if self.available is not None: + result["available"] = from_union([from_bool, from_none], self.available) + if self.label is not None: + result["label"] = from_union([from_str, from_none], self.label) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderKind(Enum): + """The provider kind. + + 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. + + The neutral provider kind. + """ + COPILOT = "copilot" + LOKI = "loki" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthEnumerateQuery: + """Which typed accounts collection to enumerate. + + Selects which accounts collection to enumerate. A no-arg selector is the empty-payload + variant. + """ + kind: AuthEnumerateQueryKind + """Account-collection query variant discriminator.""" + + broker_available: bool | None = None + """Whether an interactive Entra broker is available on the host; gates Entra availability in + the returned list. + """ + + @staticmethod + def from_dict(obj: Any) -> 'AuthEnumerateQuery': + assert isinstance(obj, dict) + kind = AuthEnumerateQueryKind(obj.get("kind")) + broker_available = from_union([from_bool, from_none], obj.get("brokerAvailable")) + return AuthEnumerateQuery(kind, broker_available) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(AuthEnumerateQueryKind, self.kind) + if self.broker_available is not None: + result["brokerAvailable"] = from_union([from_bool, from_none], self.broker_available) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthReadQuery: + """Which typed accounts datum to read. + + Selects which typed accounts datum to read. + """ + kind: AuthReadQueryKind + """Account read-datum query variant discriminator.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthReadQuery': + assert isinstance(obj, dict) + kind = AuthReadQueryKind(obj.get("kind")) + return AuthReadQuery(kind) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(AuthReadQueryKind, self.kind) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthWrite: + """The non-interactive mutation command to apply. + + One non-interactive accounts mutation command (the selector is fused with its typed args). + """ + kind: AuthWriteKind + """Account mutation command variant discriminator.""" + + selection_id: str | None = None + """Opaque selection id of the account to make active. + + Opaque selection id of the account to log out; absent logs out the active account. + """ + host: str | None = None + """Authentication host URL.""" + + login: str | None = None + """Login/username for the credential.""" + + token: str | None = None + """GitHub authentication token to install.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthWrite': + assert isinstance(obj, dict) + kind = AuthWriteKind(obj.get("kind")) + selection_id = from_union([from_str, from_none], obj.get("selectionId")) + host = from_union([from_str, from_none], obj.get("host")) + login = from_union([from_str, from_none], obj.get("login")) + token = from_union([from_str, from_none], obj.get("token")) + return AuthWrite(kind, selection_id, host, login, token) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(AuthWriteKind, self.kind) + if self.selection_id is not None: + result["selectionId"] = from_union([from_str, from_none], self.selection_id) + 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) + 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 ModelCapabilitiesSupports: @@ -16791,6 +19033,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""" @@ -16802,9 +19048,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 = {} @@ -16812,6 +19059,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: @@ -17004,6 +19253,74 @@ def to_dict(self) -> dict: result["field"] = from_union([lambda x: to_enum(AgentRegistrySpawnValidationErrorField, x), from_none], self.field) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _AgentsGetAvailableBuiltinsResult: + """The shipped agents available under the requested flags.""" + + agents: list[_BuiltinAgentSummary] + """Available shipped agents, in the runtime's own order.""" + + @staticmethod + def from_dict(obj: Any) -> '_AgentsGetAvailableBuiltinsResult': + assert isinstance(obj, dict) + agents = from_list(_BuiltinAgentSummary.from_dict, obj.get("agents")) + return _AgentsGetAvailableBuiltinsResult(agents) + + def to_dict(self) -> dict: + result: dict = {} + result["agents"] = from_list(lambda x: to_class(_BuiltinAgentSummary, x), self.agents) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginBeginRequest: + """Begin an interactive login flow for a provider kind. Dispatch is kind-only.""" + + kind: LoginProviderKind + """The provider kind to sign in with.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginBeginRequest': + assert isinstance(obj, dict) + kind = LoginProviderKind(obj.get("kind")) + return AuthLoginBeginRequest(kind) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(LoginProviderKind, self.kind) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProviderDescriptor: + """A provider offered for interactive login.""" + + available: bool + """Whether this provider is currently available to sign in with.""" + + kind: LoginProviderKind + """The neutral provider kind.""" + + label: str + """Human-readable menu label, owned by the runtime so every consumer renders identical text.""" + + @staticmethod + def from_dict(obj: Any) -> 'ProviderDescriptor': + assert isinstance(obj, dict) + available = from_bool(obj.get("available")) + kind = LoginProviderKind(obj.get("kind")) + label = from_str(obj.get("label")) + return ProviderDescriptor(available, kind, label) + + def to_dict(self) -> dict: + result: dict = {} + result["available"] = from_bool(self.available) + result["kind"] = to_enum(LoginProviderKind, self.kind) + result["label"] = from_str(self.label) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AutopilotObjectiveState: @@ -17470,6 +19787,10 @@ class CatalogNegotiatedContract: catalog operation. Protocol version and capabilities the runtime honoured. + + Capabilities actually honoured for this request. + + Capabilities honoured for this request. """ granted_capabilities: list[CatalogCapability] """Wire features the runtime understood for this operation. Includes the five original @@ -17846,9 +20167,11 @@ class CatalogSearchPagination: """Page size bound to the search, equal to the effective request limit.""" token: str - """Opaque authority-issued pagination token. Only the runtime decodes it or changes - targetPage; SDK consumers must not decode, modify or log it. It has no runtime-created - expiry or cache. + """Opaque pagination token. Session-bound search returns an expiring runtime-owned reference + retaining the exact private authority token, original search and authority. Legacy + unbound search returns the authority token unchanged, without a runtime-created expiry. + Only the runtime unwraps tokens or changes targetPage; SDK consumers must not decode, + modify or log them. """ total_count: int """Backend-reported count for this response, not the number of returned candidates. Its @@ -17932,43 +20255,6 @@ 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 CatalogSelectionRequest: - """Terminates one retained catalog selection group through an opaque reference previously - returned by the model-safe search projection. - """ - contract: CatalogClientContract - """Protocol version and capabilities the caller requires.""" - - outcome: CatalogSelectionDecision - """The terminal outcome declared by the caller. Timed-out means the host's live interaction - deadline elapsed; a reference whose runtime TTL elapsed is rejected separately as stale. - """ - selection_ref: str - """Opaque runtime-instance scoped reference to one visible candidate. For a non-selected - outcome, any candidate reference from the same search closes that search's retained group. - """ - session_id: str - """Locally owned root session whose retained search state is being resolved.""" - - @staticmethod - def from_dict(obj: Any) -> 'CatalogSelectionRequest': - assert isinstance(obj, dict) - contract = CatalogClientContract.from_dict(obj.get("contract")) - outcome = CatalogSelectionDecision(obj.get("outcome")) - selection_ref = from_str(obj.get("selectionRef")) - session_id = from_str(obj.get("sessionId")) - return CatalogSelectionRequest(contract, outcome, selection_ref, session_id) - - def to_dict(self) -> dict: - result: dict = {} - result["contract"] = to_class(CatalogClientContract, self.contract) - result["outcome"] = to_enum(CatalogSelectionDecision, self.outcome) - result["selectionRef"] = from_str(self.selection_ref) - 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 CatalogSelectionDeclined: @@ -18653,6 +20939,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': @@ -18664,7 +20955,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 = {} @@ -18675,6 +20968,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. @@ -18697,6 +20994,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) @@ -18705,7 +21011,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 = {} @@ -18715,6 +21024,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. @@ -18798,6 +21113,69 @@ def to_dict(self) -> dict: result["totalTokens"] = from_int(self.total_tokens) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AccountAuthInfo: + """An interactive account whose model provider owns its credentials. It carries no GitHub + credential. + """ + host: str + """Host coordinate owned by the account's model provider.""" + + login: str + """Login identifying the provider-owned account.""" + + type: ClassVar[str] = "account" + """Provider-owned account authentication.""" + + @staticmethod + def from_dict(obj: Any) -> 'AccountAuthInfo': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + login = from_str(obj.get("login")) + return AccountAuthInfo(host, login) + + def to_dict(self) -> dict: + result: dict = {} + 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. @dataclass class CanvasHostContextCapabilities: @@ -18975,6 +21353,235 @@ 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 ConnectorDiscoveryAuthInfo: + """Account metadata.""" + + host: str + """Host.""" + + login: str + """Login.""" + + type: AuthInfoType + """Authentication type.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDiscoveryAuthInfo': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + login = from_str(obj.get("login")) + type = AuthInfoType(obj.get("type")) + return ConnectorDiscoveryAuthInfo(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 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 + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class GitHubRepositoryIdentity: + """Owner, name, and host of a GitHub repository, as resolved from a git remote URL.""" + + host: str + """Host the remote points at, for example `github.com` or a GitHub Enterprise hostname.""" + + name: str + """Repository name, without the owner prefix or the `.git` suffix.""" + + owner: str + """Repository owner login (user or organization).""" + + @staticmethod + def from_dict(obj: Any) -> 'GitHubRepositoryIdentity': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + name = from_str(obj.get("name")) + owner = from_str(obj.get("owner")) + return GitHubRepositoryIdentity(host, name, owner) + + def to_dict(self) -> dict: + result: dict = {} + result["host"] = from_str(self.host) + result["name"] = from_str(self.name) + result["owner"] = from_str(self.owner) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _LoggedInUser: + """Account used for the most recent sign-in. + + An account the host has signed in to, identified by the server it lives on and the login + it uses there. The same person can appear more than once when they use both github.com + and an Enterprise server. + """ + host: str + """Host the account belongs to, such as `github.com` or an Enterprise server.""" + + login: str + """Account login on that host.""" + + derived_from: str | None = None + """Source account this account was derived from, when one was recorded.""" + + kind: str | None = None + """Account kind, when the host recorded one. Consumers must tolerate new strings.""" + + @staticmethod + def from_dict(obj: Any) -> '_LoggedInUser': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + login = from_str(obj.get("login")) + derived_from = from_union([from_str, from_none], obj.get("derivedFrom")) + kind = from_union([from_str, from_none], obj.get("kind")) + return _LoggedInUser(host, login, derived_from, kind) + + def to_dict(self) -> dict: + result: dict = {} + result["host"] = from_str(self.host) + result["login"] = from_str(self.login) + if self.derived_from is not None: + result["derivedFrom"] = from_union([from_str, from_none], self.derived_from) + if self.kind is not None: + result["kind"] = from_union([from_str, from_none], self.kind) + return result + +@dataclass +class HostSessionCreateCallback: + """Application callback routed over its existing SDK connection.""" + + config: dict[str, Any] + """Host-selected SDK creation or resume settings, without executable callbacks or tools.""" + + handoff_id: str + """Unique identity of the session participation being requested.""" + + host_id: str + """Listener UUID identifying the owning application's host.""" + + resume: bool | None = None + """Resume an app-owned durable session instead of creating a new session.""" + + @staticmethod + def from_dict(obj: Any) -> 'HostSessionCreateCallback': + assert isinstance(obj, dict) + config = from_dict(lambda x: x, obj.get("config")) + handoff_id = from_str(obj.get("handoffId")) + host_id = from_str(obj.get("hostId")) + resume = from_union([from_bool, from_none], obj.get("resume")) + return HostSessionCreateCallback(config, handoff_id, host_id, resume) + + def to_dict(self) -> dict: + result: dict = {} + result["config"] = from_dict(lambda x: x, self.config) + result["handoffId"] = from_str(self.handoff_id) + result["hostId"] = from_str(self.host_id) + if self.resume is not None: + 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 HostSessionCreateRequest: + """One application-owned session handoff, requested by the supervised hosting participant.""" + + config: dict[str, Any] + """Host-selected SDK creation or resume settings, without executable callbacks or tools.""" + + handoff_id: str + """Unique identity for this participation, independent of the session lifetime.""" + + resume: bool | None = None + """Resume an app-owned durable session instead of creating a new session.""" + + @staticmethod + def from_dict(obj: Any) -> 'HostSessionCreateRequest': + assert isinstance(obj, dict) + config = from_dict(lambda x: x, obj.get("config")) + handoff_id = from_str(obj.get("handoffId")) + resume = from_union([from_bool, from_none], obj.get("resume")) + return HostSessionCreateRequest(config, handoff_id, resume) + + def to_dict(self) -> dict: + result: dict = {} + result["config"] = from_dict(lambda x: x, self.config) + result["handoffId"] = from_str(self.handoff_id) + if self.resume is not None: + 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: @@ -19001,6 +21608,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: @@ -19392,6 +22050,184 @@ def to_dict(self) -> dict: result["source"] = from_union([from_str, from_none], self.source) 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 ModelProviderProvenance: + """Attribution for the adapter that produced this model row. + + Attribution for the adapter that produced a provider row. + + Attribution for the adapter that produced this instance. + """ + 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.""" + + owner_display_name: str | None = None + """Human-readable contributor name, not the adapter display name.""" + + 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) -> 'ModelProviderProvenance': + assert isinstance(obj, dict) + 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["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 ModelProviderAttribution: + """Attribution for the adapter itself. + + Contributor attribution, independent of routing identity and authorization. + """ + source: ModelProviderProvenanceSource + """Kind of component that supplied the adapter. Attribution does not confer authority.""" + + owner_display_name: str | None = None + """Human-readable contributor name, not the adapter display name.""" + + 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) + + 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) -> 'ModelProviderOperationOutcome': + assert isinstance(obj, dict) + 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["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. @dataclass class SessionsReadPersistedEventsRequest: @@ -19736,6 +22572,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: @@ -19971,1040 +22850,984 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class FactoryAgentRequest: - """Parameters for one factory-scoped subagent call.""" - - execution_token: str - """Opaque token identifying the current factory execution attempt.""" - - factory_run_id: str - """Factory run identifier that owns the subagent.""" - - opts: FactoryAgentOptions - """Subagent execution options.""" - - prompt: str - """Prompt to send to the subagent.""" +class _GitHubOwnersListResult: + """Outcome of an owner listing. Exactly one of `owners` and `message` is present, except + that `throwError` reports a failure the caller is expected to raise rather than render. + """ + message: str | None = None + """Why no owners could be listed, phrased for a user. Present when the listing failed in a + way the caller should render rather than raise. + """ + owners: list[_GitHubOwnerOption] | None = None + """The owners, on success: the authenticated user first, then the organizations they belong + to. + """ + throw_error: str | None = None + """A malformed request or an unreadable credential, which the caller raises instead of + rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, + which the caller renders. + """ + warning: str | None = None + """A line the caller should log. Present only alongside `message`, and only for failures + worth recording. + """ @staticmethod - def from_dict(obj: Any) -> 'FactoryAgentRequest': + def from_dict(obj: Any) -> '_GitHubOwnersListResult': assert isinstance(obj, dict) - execution_token = from_str(obj.get("executionToken")) - factory_run_id = from_str(obj.get("factoryRunId")) - opts = FactoryAgentOptions.from_dict(obj.get("opts")) - prompt = from_str(obj.get("prompt")) - return FactoryAgentRequest(execution_token, factory_run_id, opts, prompt) + message = from_union([from_str, from_none], obj.get("message")) + owners = from_union([lambda x: from_list(_GitHubOwnerOption.from_dict, x), from_none], obj.get("owners")) + throw_error = from_union([from_str, from_none], obj.get("throwError")) + warning = from_union([from_str, from_none], obj.get("warning")) + return _GitHubOwnersListResult(message, owners, throw_error, warning) def to_dict(self) -> dict: result: dict = {} - result["executionToken"] = from_str(self.execution_token) - result["factoryRunId"] = from_str(self.factory_run_id) - result["opts"] = to_class(FactoryAgentOptions, self.opts) - result["prompt"] = from_str(self.prompt) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.owners is not None: + result["owners"] = from_union([lambda x: from_list(lambda x: to_class(_GitHubOwnerOption, x), x), from_none], self.owners) + if self.throw_error is not None: + result["throwError"] = from_union([from_str, from_none], self.throw_error) + if self.warning is not None: + result["warning"] = from_union([from_str, from_none], self.warning) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryRunFailure: - """Machine-readable terminal failure. - - Machine-readable factory run failure. - - Machine-readable failure details for a halted or errored run. - - The run stopped because its usage accounting could not be completed. - - The extension that owns the factory 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. - """ - run_id: str - """Factory run identifier. - - Factory run identifier whose changed limits were declined. - """ - type: FactoryRunFailureType - """Factory failure variant discriminator.""" - - kind: RunFailureKind | None = None - """Resource ceiling that stopped the run.""" - - suggested_value: float | None = None - """Suggested larger ceiling when the runtime can derive one safely.""" - - value: float | None = None - """Approved effective ceiling that was reached.""" +class GitHubTokenAcquireRequest: + """Asks the SDK client to acquire a GitHub access token from an opaque callback registration.""" - reason: str | None = None - """Human-readable reason the resume did not proceed.""" + host: str + """Effective GitHub host for which the callback must return a token.""" - code: str | None = None - """Stable failure code.""" + reason: GitHubTokenAcquireReason + """Why the runtime is requesting a GitHub credential.""" - operation: DurableOperation | None = None - """Execution-critical durable operation that failed.""" + registration_id: str + """Opaque identifier generated by the SDK for this callback registration.""" - drained_nano_aiu: int | None = None - """Confirmed usage in nano-AIU, representing the floor of what the run spent.""" + session_id: str | None = None + """Session receiving the token. Absent only before a cloud session has been assigned its id.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryRunFailure': + def from_dict(obj: Any) -> 'GitHubTokenAcquireRequest': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - type = FactoryRunFailureType(obj.get("type")) - kind = from_union([RunFailureKind, from_none], obj.get("kind")) - suggested_value = from_union([from_float, from_none], obj.get("suggestedValue")) - value = from_union([from_float, from_none], obj.get("value")) - reason = from_union([from_str, from_none], obj.get("reason")) - code = from_union([from_str, from_none], obj.get("code")) - operation = from_union([DurableOperation, from_none], obj.get("operation")) - drained_nano_aiu = from_union([from_int, from_none], obj.get("drainedNanoAiu")) - return FactoryRunFailure(run_id, type, kind, suggested_value, value, reason, code, operation, drained_nano_aiu) + host = from_str(obj.get("host")) + reason = GitHubTokenAcquireReason(obj.get("reason")) + registration_id = from_str(obj.get("registrationId")) + session_id = from_union([from_str, from_none], obj.get("sessionId")) + return GitHubTokenAcquireRequest(host, reason, registration_id, session_id) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) - result["type"] = to_enum(FactoryRunFailureType, self.type) - if self.kind is not None: - result["kind"] = from_union([lambda x: to_enum(RunFailureKind, x), from_none], self.kind) - if self.suggested_value is not None: - result["suggestedValue"] = from_union([to_float, from_none], self.suggested_value) - if self.value is not None: - result["value"] = from_union([to_float, from_none], self.value) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.code is not None: - result["code"] = from_union([from_str, from_none], self.code) - if self.operation is not None: - result["operation"] = from_union([lambda x: to_enum(DurableOperation, x), from_none], self.operation) - if self.drained_nano_aiu is not None: - result["drainedNanoAiu"] = from_union([from_int, from_none], self.drained_nano_aiu) + result["host"] = from_str(self.host) + result["reason"] = to_enum(GitHubTokenAcquireReason, self.reason) + result["registrationId"] = from_str(self.registration_id) + if self.session_id is not None: + result["sessionId"] = from_union([from_str, from_none], self.session_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PauseInfoClass: - type: PauseInfoType - """Factory pause initiator discriminator.""" +class GitHubTokenAcquireResult: + """SDK host response to a GitHub credential request.""" - key: str | None = None - """Stable author-defined checkpoint key that initiated the pause.""" + kind: GitHubTokenAcquireResultKind + """GitHub credential response variant discriminator.""" + + access_token: str | None = None + """GitHub access token acquired by the SDK host.""" + + expires_in: int | None = None + """Remaining token lifetime in seconds when callback execution completes. It must exceed the + one-hour preflight refresh threshold. + """ + token_type: str | None = None + """OAuth token type. Defaults to bearer when omitted.""" @staticmethod - def from_dict(obj: Any) -> 'PauseInfoClass': + def from_dict(obj: Any) -> 'GitHubTokenAcquireResult': assert isinstance(obj, dict) - type = PauseInfoType(obj.get("type")) - key = from_union([from_str, from_none], obj.get("key")) - return PauseInfoClass(type, key) + kind = GitHubTokenAcquireResultKind(obj.get("kind")) + access_token = from_union([from_str, from_none], obj.get("accessToken")) + expires_in = from_union([from_int, from_none], obj.get("expiresIn")) + token_type = from_union([from_str, from_none], obj.get("tokenType")) + return GitHubTokenAcquireResult(kind, access_token, expires_in, token_type) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(PauseInfoType, self.type) - if self.key is not None: - result["key"] = from_union([from_str, from_none], self.key) + result["kind"] = to_enum(GitHubTokenAcquireResultKind, self.kind) + if self.access_token is not None: + result["accessToken"] = from_union([from_str, from_none], self.access_token) + if self.expires_in is not None: + result["expiresIn"] = from_union([from_int, from_none], self.expires_in) + if self.token_type is not None: + result["tokenType"] = from_union([from_str, from_none], self.token_type) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryPauseInfo: - """Durable metadata describing who initiated a factory pause. +class MCPOauthPendingRequestResponse: + """Host response to the pending OAuth request.""" - Structured pause initiator metadata for a paused attempt. - """ - type: PauseInfoType - """Factory pause initiator discriminator.""" + kind: GitHubTokenAcquireResultKind + """OAuth response variant discriminator.""" - key: str | None = None - """Stable author-defined checkpoint key that initiated the pause.""" + access_token: str | None = None + """Access token acquired by the SDK host""" + + expires_in: int | None = None + """Token lifetime in seconds, if known.""" + + token_type: str | None = None + """OAuth token type. Defaults to bearer when omitted.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryPauseInfo': + def from_dict(obj: Any) -> 'MCPOauthPendingRequestResponse': assert isinstance(obj, dict) - type = PauseInfoType(obj.get("type")) - key = from_union([from_str, from_none], obj.get("key")) - return FactoryPauseInfo(type, key) + kind = GitHubTokenAcquireResultKind(obj.get("kind")) + access_token = from_union([from_str, from_none], obj.get("accessToken")) + expires_in = from_union([from_int, from_none], obj.get("expiresIn")) + token_type = from_union([from_str, from_none], obj.get("tokenType")) + return MCPOauthPendingRequestResponse(kind, access_token, expires_in, token_type) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(PauseInfoType, self.type) - if self.key is not None: - result["key"] = from_union([from_str, from_none], self.key) + result["kind"] = to_enum(GitHubTokenAcquireResultKind, self.kind) + if self.access_token is not None: + result["accessToken"] = from_union([from_str, from_none], self.access_token) + if self.expires_in is not None: + result["expiresIn"] = from_union([from_int, from_none], self.expires_in) + if self.token_type is not None: + result["tokenType"] = from_union([from_str, from_none], self.token_type) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class WorkflowRunTerminalPauseInfo: - type: PauseInfoType - """Workflow pause initiator discriminator.""" +class _GitReposFromRemotesResult: + """The GitHub repositories a working tree's remotes point at.""" - key: str | None = None - """Stable author-defined checkpoint key that initiated the pause.""" + repositories: list[_GitRemoteRepository] + """One entry per distinct GitHub repository, in the order git reports the first remote for + each repository. Empty when no remote points at a GitHub host, which a caller should read + as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + """ @staticmethod - def from_dict(obj: Any) -> 'WorkflowRunTerminalPauseInfo': + def from_dict(obj: Any) -> '_GitReposFromRemotesResult': assert isinstance(obj, dict) - type = PauseInfoType(obj.get("type")) - key = from_union([from_str, from_none], obj.get("key")) - return WorkflowRunTerminalPauseInfo(type, key) + repositories = from_list(_GitRemoteRepository.from_dict, obj.get("repositories")) + return _GitReposFromRemotesResult(repositories) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(PauseInfoType, self.type) - if self.key is not None: - result["key"] = from_union([from_str, from_none], self.key) + result["repositories"] = from_list(lambda x: to_class(_GitRemoteRepository, x), self.repositories) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class WorkflowPauseInfo: - """Durable metadata describing who initiated a workflow pause. +class InstalledPluginSource: + """Source descriptor for a direct GitHub plugin install, with `owner/repo`, optional ref or + full commit SHA, and optional subpath. - Structured pause initiator metadata for a paused attempt. + Source descriptor for a direct URL plugin install, with URL, optional ref or full commit + SHA, and optional subpath. + + Source descriptor for a direct local plugin install, with a local filesystem path. """ - type: PauseInfoType - """Workflow pause initiator discriminator.""" + source: InstalledPluginSourceURLSource + """Constant value. Always "github". - key: str | None = None - """Stable author-defined checkpoint key that initiated the pause.""" + Constant value. Always "url". - @staticmethod - def from_dict(obj: Any) -> 'WorkflowPauseInfo': - assert isinstance(obj, dict) - type = PauseInfoType(obj.get("type")) - key = from_union([from_str, from_none], obj.get("key")) - return WorkflowPauseInfo(type, key) + Constant value. Always "local". + """ + path: str | None = None + """Optional repository-relative path to the plugin. - def to_dict(self) -> dict: - result: dict = {} - result["type"] = to_enum(PauseInfoType, self.type) - if self.key is not None: - result["key"] = from_union([from_str, from_none], self.key) - return result + Optional source-relative path to the plugin. -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryLogLine: - """One ordered factory progress line.""" + Local filesystem path to the plugin. + """ + ref: str | None = None + """Optional Git ref to resolve.""" - kind: LogLineKind - """Progress line kind.""" + repo: str | None = None + """GitHub repository in `owner/repo` form.""" - seq: int - """Monotonic sequence number within the factory run.""" + sha: str | None = None + """Optional full 40-character hexadecimal commit SHA.""" - text: str - """Progress text.""" + url: str | None = None + """URL of the plugin source.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryLogLine': + def from_dict(obj: Any) -> 'InstalledPluginSource': assert isinstance(obj, dict) - kind = LogLineKind(obj.get("kind")) - seq = from_int(obj.get("seq")) - text = from_str(obj.get("text")) - return FactoryLogLine(kind, seq, text) + source = InstalledPluginSourceURLSource(obj.get("source")) + path = from_union([from_str, from_none], obj.get("path")) + ref = from_union([from_str, from_none], obj.get("ref")) + repo = from_union([from_str, from_none], obj.get("repo")) + sha = from_union([from_str, from_none], obj.get("sha")) + url = from_union([from_str, from_none], obj.get("url")) + return InstalledPluginSource(source, path, ref, repo, sha, url) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(LogLineKind, self.kind) - result["seq"] = from_int(self.seq) - result["text"] = from_str(self.text) + result["source"] = to_enum(InstalledPluginSourceURLSource, self.source) + if self.path is not None: + result["path"] = from_union([from_str, from_none], self.path) + if self.ref is not None: + result["ref"] = from_union([from_str, from_none], self.ref) + if self.repo is not None: + result["repo"] = from_union([from_str, from_none], self.repo) + if self.sha is not None: + result["sha"] = from_union([from_str, from_none], self.sha) + 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. @dataclass -class FactoryProgressLine: - """One durable factory progress record.""" +class SessionInstalledPluginSource: + """Source descriptor for a direct GitHub plugin install, with `owner/repo`, optional ref or + full commit SHA, and optional subpath. - attempt: int - """Resume attempt that emitted this record.""" + Source descriptor for a direct URL plugin install, with URL, optional ref or full commit + SHA, and optional subpath. - kind: LogLineKind - """Progress record kind.""" + Source descriptor for a direct local plugin install, with a local filesystem path. + """ + source: InstalledPluginSourceURLSource + """Constant value. Always "github". - recorded_at: int - """Epoch milliseconds when the record was persisted.""" + Constant value. Always "url". - seq: int - """Global monotonic sequence number within the run.""" + Constant value. Always "local". + """ + path: str | None = None + """Optional repository-relative path to the plugin. - text: str - """Prompt-safe progress text.""" + Optional source-relative path to the plugin. - phase_id: str | None = None - """Phase active when the record was emitted, or null before any phase.""" + Local filesystem path to the plugin. + """ + ref: str | None = None + """Optional Git ref to resolve.""" + + repo: str | None = None + """GitHub repository in `owner/repo` form.""" + + sha: str | None = None + """Optional full 40-character hexadecimal commit SHA.""" + + url: str | None = None + """URL of the plugin source.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryProgressLine': + def from_dict(obj: Any) -> 'SessionInstalledPluginSource': assert isinstance(obj, dict) - attempt = from_int(obj.get("attempt")) - kind = LogLineKind(obj.get("kind")) - recorded_at = from_int(obj.get("recordedAt")) - seq = from_int(obj.get("seq")) - text = from_str(obj.get("text")) - phase_id = from_union([from_none, from_str], obj.get("phaseId")) - return FactoryProgressLine(attempt, kind, recorded_at, seq, text, phase_id) + source = InstalledPluginSourceURLSource(obj.get("source")) + path = from_union([from_str, from_none], obj.get("path")) + ref = from_union([from_str, from_none], obj.get("ref")) + repo = from_union([from_str, from_none], obj.get("repo")) + sha = from_union([from_str, from_none], obj.get("sha")) + url = from_union([from_str, from_none], obj.get("url")) + return SessionInstalledPluginSource(source, path, ref, repo, sha, url) def to_dict(self) -> dict: result: dict = {} - result["attempt"] = from_int(self.attempt) - result["kind"] = to_enum(LogLineKind, self.kind) - result["recordedAt"] = from_int(self.recorded_at) - result["seq"] = from_int(self.seq) - result["text"] = from_str(self.text) - result["phaseId"] = from_union([from_none, from_str], self.phase_id) + result["source"] = to_enum(InstalledPluginSourceURLSource, self.source) + if self.path is not None: + result["path"] = from_union([from_str, from_none], self.path) + if self.ref is not None: + result["ref"] = from_union([from_str, from_none], self.ref) + if self.repo is not None: + result["repo"] = from_union([from_str, from_none], self.repo) + if self.sha is not None: + result["sha"] = from_union([from_str, from_none], self.sha) + 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. @dataclass -class WorkflowLogLine: - """One ordered workflow progress line.""" +class HistoryCompactResult: + """Compaction outcome with the number of tokens and messages removed, summary text, and the + resulting context window breakdown. + """ + messages_removed: int + """Number of messages removed during compaction""" - kind: LogLineKind - """Progress line kind.""" + success: bool + """Whether compaction completed successfully""" - seq: int - """Monotonic sequence number within the workflow run.""" + tokens_removed: int + """Number of tokens freed by compaction""" - text: str - """Progress text.""" + context_window: HistoryCompactContextWindow | None = None + """Post-compaction context window usage breakdown""" + + summary_content: str | None = None + """Summary text produced by compaction. Omitted when compaction did not produce a summary + (e.g. failure path). + """ @staticmethod - def from_dict(obj: Any) -> 'WorkflowLogLine': + def from_dict(obj: Any) -> 'HistoryCompactResult': assert isinstance(obj, dict) - kind = LogLineKind(obj.get("kind")) - seq = from_int(obj.get("seq")) - text = from_str(obj.get("text")) - return WorkflowLogLine(kind, seq, text) + messages_removed = from_int(obj.get("messagesRemoved")) + success = from_bool(obj.get("success")) + tokens_removed = from_int(obj.get("tokensRemoved")) + context_window = from_union([HistoryCompactContextWindow.from_dict, from_none], obj.get("contextWindow")) + summary_content = from_union([from_str, from_none], obj.get("summaryContent")) + return HistoryCompactResult(messages_removed, success, tokens_removed, context_window, summary_content) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(LogLineKind, self.kind) - result["seq"] = from_int(self.seq) - result["text"] = from_str(self.text) + result["messagesRemoved"] = from_int(self.messages_removed) + result["success"] = from_bool(self.success) + result["tokensRemoved"] = from_int(self.tokens_removed) + if self.context_window is not None: + result["contextWindow"] = from_union([lambda x: to_class(HistoryCompactContextWindow, x), from_none], self.context_window) + if self.summary_content is not None: + result["summaryContent"] = from_union([from_str, from_none], self.summary_content) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class WorkflowProgressLine: - """One durable workflow progress record.""" - - attempt: int - """Resume attempt that emitted this record.""" - - kind: LogLineKind - """Progress record kind.""" - - recorded_at: int - """Epoch milliseconds when the record was persisted.""" - - seq: int - """Global monotonic sequence number within the run.""" +class HistorySkippedFileRestore: + """A captured file that rewind intentionally left unchanged.""" - text: str - """Prompt-safe progress text.""" + path: str + """Absolute path of the skipped file.""" - phase_id: str | None = None - """Phase active when the record was emitted, or null before any phase.""" + reason: HistoryFileRestoreSkipReason + """Reason the file was not restored.""" @staticmethod - def from_dict(obj: Any) -> 'WorkflowProgressLine': + def from_dict(obj: Any) -> 'HistorySkippedFileRestore': assert isinstance(obj, dict) - attempt = from_int(obj.get("attempt")) - kind = LogLineKind(obj.get("kind")) - recorded_at = from_int(obj.get("recordedAt")) - seq = from_int(obj.get("seq")) - text = from_str(obj.get("text")) - phase_id = from_union([from_none, from_str], obj.get("phaseId")) - return WorkflowProgressLine(attempt, kind, recorded_at, seq, text, phase_id) + path = from_str(obj.get("path")) + reason = HistoryFileRestoreSkipReason(obj.get("reason")) + return HistorySkippedFileRestore(path, reason) def to_dict(self) -> dict: result: dict = {} - result["attempt"] = from_int(self.attempt) - result["kind"] = to_enum(LogLineKind, self.kind) - result["recordedAt"] = from_int(self.recorded_at) - result["seq"] = from_int(self.seq) - result["text"] = from_str(self.text) - result["phaseId"] = from_union([from_none, from_str], self.phase_id) + result["path"] = from_str(self.path) + result["reason"] = to_enum(HistoryFileRestoreSkipReason, self.reason) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryPhaseObservation: - """Durable lifecycle and timing for one factory phase.""" - - accumulated_active_ms: int - """Completed active time accumulated by this phase in milliseconds.""" - - current_active_ms: int - """Current live active time for this phase in milliseconds.""" - - entry_count: int - """Number of times execution entered this phase.""" - - id: str - """Phase identifier.""" - - last_entered_run_attempt: int - """Most recent run attempt that entered this phase, or `0` if the phase has never been - entered. - """ - live_agent_count: int - """Direct agents in this phase that are currently live.""" - - status: PhaseStatus - """Derived lifecycle state of the phase.""" - - title: str - """Human-readable phase title.""" - - total_agent_count: int - """Total direct agents associated with this phase.""" +class HistoryListRewindPointsResult: + """Rewind points and file-change-tracking availability for the session.""" - completed_at: int | None = None - """Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip - timestamp (equal to `startedAt`). - """ - detail: str | None = None - """Optional human-readable phase detail.""" + file_change_tracking_enabled: bool + """Whether this session captured file changes from its first turn.""" - ordinal: int | None = None - """Zero-based declared phase ordinal, or null for an undeclared phase.""" + points: list[HistoryRewindPoint] + """Root user turns in chronological order. Empty when `unavailableReason` is set.""" - started_at: int | None = None - """Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip - timestamp (equal to `completedAt`). + unavailable_reason: HistoryRewindUnavailableReason | None = None + """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`. """ @staticmethod - def from_dict(obj: Any) -> 'FactoryPhaseObservation': + def from_dict(obj: Any) -> 'HistoryListRewindPointsResult': assert isinstance(obj, dict) - accumulated_active_ms = from_int(obj.get("accumulatedActiveMs")) - current_active_ms = from_int(obj.get("currentActiveMs")) - entry_count = from_int(obj.get("entryCount")) - id = from_str(obj.get("id")) - last_entered_run_attempt = from_int(obj.get("lastEnteredRunAttempt")) - live_agent_count = from_int(obj.get("liveAgentCount")) - status = PhaseStatus(obj.get("status")) - title = from_str(obj.get("title")) - total_agent_count = from_int(obj.get("totalAgentCount")) - completed_at = from_union([from_int, from_none], obj.get("completedAt")) - detail = from_union([from_str, from_none], obj.get("detail")) - ordinal = from_union([from_none, from_int], obj.get("ordinal")) - started_at = from_union([from_int, from_none], obj.get("startedAt")) - return FactoryPhaseObservation(accumulated_active_ms, current_active_ms, entry_count, id, last_entered_run_attempt, live_agent_count, status, title, total_agent_count, completed_at, detail, ordinal, started_at) + file_change_tracking_enabled = from_bool(obj.get("fileChangeTrackingEnabled")) + points = from_list(HistoryRewindPoint.from_dict, obj.get("points")) + unavailable_reason = from_union([HistoryRewindUnavailableReason, from_none], obj.get("unavailableReason")) + return HistoryListRewindPointsResult(file_change_tracking_enabled, points, unavailable_reason) def to_dict(self) -> dict: result: dict = {} - result["accumulatedActiveMs"] = from_int(self.accumulated_active_ms) - result["currentActiveMs"] = from_int(self.current_active_ms) - result["entryCount"] = from_int(self.entry_count) - result["id"] = from_str(self.id) - result["lastEnteredRunAttempt"] = from_int(self.last_entered_run_attempt) - result["liveAgentCount"] = from_int(self.live_agent_count) - result["status"] = to_enum(PhaseStatus, self.status) - result["title"] = from_str(self.title) - result["totalAgentCount"] = from_int(self.total_agent_count) - if self.completed_at is not None: - result["completedAt"] = from_union([from_int, from_none], self.completed_at) - if self.detail is not None: - result["detail"] = from_union([from_str, from_none], self.detail) - result["ordinal"] = from_union([from_none, from_int], self.ordinal) - if self.started_at is not None: - result["startedAt"] = from_union([from_int, from_none], self.started_at) + result["fileChangeTrackingEnabled"] = from_bool(self.file_change_tracking_enabled) + result["points"] = from_list(lambda x: to_class(HistoryRewindPoint, x), self.points) + if self.unavailable_reason is not None: + result["unavailableReason"] = from_union([lambda x: to_enum(HistoryRewindUnavailableReason, x), from_none], self.unavailable_reason) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class WorkflowPhaseObservation: - """Durable lifecycle and timing for one workflow phase.""" - - accumulated_active_ms: int - """Completed active time accumulated by this phase in milliseconds.""" - - current_active_ms: int - """Current live active time for this phase in milliseconds.""" - - entry_count: int - """Number of times execution entered this phase.""" - - id: str - """Phase identifier.""" - - last_entered_run_attempt: int - """Most recent run attempt that entered this phase, or `0` if the phase has never been - entered. - """ - live_agent_count: int - """Direct agents in this phase that are currently live.""" - - status: PhaseStatus - """Derived lifecycle state of the phase.""" - - title: str - """Human-readable phase title.""" +class HistoryRewindFilePreview: + """A file that a conversation-and-files rewind would restore.""" - total_agent_count: int - """Total direct agents associated with this phase.""" + change_type: HistoryRewindChangeType + """Aggregate change made across the discarded turns.""" - completed_at: int | None = None - """Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip - timestamp (equal to `startedAt`). - """ - detail: str | None = None - """Optional human-readable phase detail.""" + lines_added: int + """Lines added across the discarded turns.""" - ordinal: int | None = None - """Zero-based declared phase ordinal, or null for an undeclared phase.""" + lines_removed: int + """Lines removed across the discarded turns.""" - started_at: int | None = None - """Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip - timestamp (equal to `completedAt`). - """ + path: str + """Absolute path of the captured file.""" @staticmethod - def from_dict(obj: Any) -> 'WorkflowPhaseObservation': + def from_dict(obj: Any) -> 'HistoryRewindFilePreview': assert isinstance(obj, dict) - accumulated_active_ms = from_int(obj.get("accumulatedActiveMs")) - current_active_ms = from_int(obj.get("currentActiveMs")) - entry_count = from_int(obj.get("entryCount")) - id = from_str(obj.get("id")) - last_entered_run_attempt = from_int(obj.get("lastEnteredRunAttempt")) - live_agent_count = from_int(obj.get("liveAgentCount")) - status = PhaseStatus(obj.get("status")) - title = from_str(obj.get("title")) - total_agent_count = from_int(obj.get("totalAgentCount")) - completed_at = from_union([from_int, from_none], obj.get("completedAt")) - detail = from_union([from_str, from_none], obj.get("detail")) - ordinal = from_union([from_none, from_int], obj.get("ordinal")) - started_at = from_union([from_int, from_none], obj.get("startedAt")) - return WorkflowPhaseObservation(accumulated_active_ms, current_active_ms, entry_count, id, last_entered_run_attempt, live_agent_count, status, title, total_agent_count, completed_at, detail, ordinal, started_at) + change_type = HistoryRewindChangeType(obj.get("changeType")) + lines_added = from_int(obj.get("linesAdded")) + lines_removed = from_int(obj.get("linesRemoved")) + path = from_str(obj.get("path")) + return HistoryRewindFilePreview(change_type, lines_added, lines_removed, path) def to_dict(self) -> dict: result: dict = {} - result["accumulatedActiveMs"] = from_int(self.accumulated_active_ms) - result["currentActiveMs"] = from_int(self.current_active_ms) - result["entryCount"] = from_int(self.entry_count) - result["id"] = from_str(self.id) - result["lastEnteredRunAttempt"] = from_int(self.last_entered_run_attempt) - result["liveAgentCount"] = from_int(self.live_agent_count) - result["status"] = to_enum(PhaseStatus, self.status) - result["title"] = from_str(self.title) - result["totalAgentCount"] = from_int(self.total_agent_count) - if self.completed_at is not None: - result["completedAt"] = from_union([from_int, from_none], self.completed_at) - if self.detail is not None: - result["detail"] = from_union([from_str, from_none], self.detail) - result["ordinal"] = from_union([from_none, from_int], self.ordinal) - if self.started_at is not None: - result["startedAt"] = from_union([from_int, from_none], self.started_at) + result["changeType"] = to_enum(HistoryRewindChangeType, self.change_type) + result["linesAdded"] = from_int(self.lines_added) + result["linesRemoved"] = from_int(self.lines_removed) + 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 FactoryResumeRequest: - """Parameters for resuming a factory run from its persisted identity.""" - - run_id: str - """Factory run identifier.""" - - limits: FactoryRunLimits | None = None - """Optional per-invocation resource ceiling overrides.""" +class HistoryRewindRequest: + """Boundary and mode for rewinding session history.""" - log_phase_names: bool | None = None - """Whether to emit factory phase names to the session transcript.""" + event_id: str + """ID of the user.message event that begins the discarded suffix.""" - notify_on_complete: bool | None = None - """Whether to notify the originating session when the factory completes.""" + mode: HistoryRewindMode + """Whether to rewind only conversation history or also restore captured files.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryResumeRequest': + def from_dict(obj: Any) -> 'HistoryRewindRequest': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - limits = from_union([FactoryRunLimits.from_dict, from_none], obj.get("limits")) - log_phase_names = from_union([from_bool, from_none], obj.get("logPhaseNames")) - notify_on_complete = from_union([from_bool, from_none], obj.get("notifyOnComplete")) - return FactoryResumeRequest(run_id, limits, log_phase_names, notify_on_complete) + event_id = from_str(obj.get("eventId")) + mode = HistoryRewindMode(obj.get("mode")) + return HistoryRewindRequest(event_id, mode) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) - if self.limits is not None: - result["limits"] = from_union([lambda x: to_class(FactoryRunLimits, x), from_none], self.limits) - if self.log_phase_names is not None: - result["logPhaseNames"] = from_union([from_bool, from_none], self.log_phase_names) - if self.notify_on_complete is not None: - result["notifyOnComplete"] = from_union([from_bool, from_none], self.notify_on_complete) + result["eventId"] = from_str(self.event_id) + 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 RunOptions: - """Factory invocation options. +class HostConfiguration: + """Normalized listener settings delivered only to the supervised hosting participant.""" - Options controlling factory invocation. - """ - limits: FactoryRunLimits | None = None - """Per-invocation resource ceiling overrides.""" + resume_factory: bool + """Whether app-owned durable sessions are resumed by the owning application.""" - log_phase_names: bool | None = None - """Whether to emit factory phase names to the session transcript.""" + session_factory: bool + """Whether session materialization is delegated to the owning application.""" - notify_on_complete: bool | None = None - """Whether to notify the originating session when the factory completes.""" + github_environment: HostGitHubEnvironmentOptions | None = None + """Requested GitHub Mission Control registration.""" - resume_from_run_id: str | None = None - """Run identifier whose journal and progress should seed this resumed run.""" + local_server: HostLocalServerConfiguration | None = None + """Normalized local listener settings, absent for relay-only hosts.""" @staticmethod - def from_dict(obj: Any) -> 'RunOptions': + def from_dict(obj: Any) -> 'HostConfiguration': assert isinstance(obj, dict) - limits = from_union([FactoryRunLimits.from_dict, from_none], obj.get("limits")) - log_phase_names = from_union([from_bool, from_none], obj.get("logPhaseNames")) - notify_on_complete = from_union([from_bool, from_none], obj.get("notifyOnComplete")) - resume_from_run_id = from_union([from_str, from_none], obj.get("resumeFromRunId")) - return RunOptions(limits, log_phase_names, notify_on_complete, resume_from_run_id) + 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 = {} - if self.limits is not None: - result["limits"] = from_union([lambda x: to_class(FactoryRunLimits, x), from_none], self.limits) - if self.log_phase_names is not None: - result["logPhaseNames"] = from_union([from_bool, from_none], self.log_phase_names) - if self.notify_on_complete is not None: - result["notifyOnComplete"] = from_union([from_bool, from_none], self.notify_on_complete) - if self.resume_from_run_id is not None: - result["resumeFromRunId"] = from_union([from_str, from_none], self.resume_from_run_id) + 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 -# Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class _FactoryToolResumeRequest: - """Internal parameters for resuming a factory run from a tool.""" +class HostExitedNotification: + """Reports a supervised listener's hosting-task termination and cleanup outcome.""" - run_id: str - """Factory run identifier.""" + host_id: str + """Listener UUID.""" - limits: FactoryRunLimits | None = None - """Optional per-invocation resource ceiling overrides.""" + reason: HostExitReason + """Cause of termination.""" - tool_call_id: str | None = None - """Opaque identifier of the originating tool call.""" + error: str | None = None + """Explicit startup or teardown failure, when present.""" + + exit_code: int | None = None + """Process exit status when available; absent for in-process listener tasks.""" @staticmethod - def from_dict(obj: Any) -> '_FactoryToolResumeRequest': + def from_dict(obj: Any) -> 'HostExitedNotification': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - limits = from_union([FactoryRunLimits.from_dict, from_none], obj.get("limits")) - tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) - return _FactoryToolResumeRequest(run_id, limits, tool_call_id) + host_id = from_str(obj.get("hostId")) + reason = HostExitReason(obj.get("reason")) + error = from_union([from_none, from_str], obj.get("error")) + exit_code = from_union([from_int, from_none], obj.get("exitCode")) + return HostExitedNotification(host_id, reason, error, exit_code) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) - if self.limits is not None: - result["limits"] = from_union([lambda x: to_class(FactoryRunLimits, x), from_none], self.limits) - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_str, from_none], self.tool_call_id) + result["hostId"] = from_str(self.host_id) + result["reason"] = to_enum(HostExitReason, self.reason) + if self.error is not None: + result["error"] = from_union([from_none, from_str], self.error) + if self.exit_code is not None: + 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 GitHubTokenAcquireRequest: - """Asks the SDK client to acquire a GitHub access token from an opaque callback registration.""" +class HostStartRequest: + """Starts a supervised AHP host with at least one explicitly selected transport.""" - host: str - """Effective GitHub host for which the callback must return a token.""" + host_id: str + """Caller-generated UUID identifying this connection-owned listener.""" - reason: GitHubTokenAcquireReason - """Why the runtime is requesting a GitHub credential.""" + github_environment: HostGitHubEnvironmentOptions | None = None + """Registers a GitHub Mission Control environment and enables its relay transport.""" - registration_id: str - """Opaque identifier generated by the SDK for this callback registration.""" + local_server: HostLocalServerOptions | None = None + """Enables a local WebSocket listener.""" - session_id: str | None = None - """Session receiving the token. Absent only before a cloud session has been assigned its id.""" + 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) -> 'GitHubTokenAcquireRequest': + def from_dict(obj: Any) -> 'HostStartRequest': assert isinstance(obj, dict) - host = from_str(obj.get("host")) - reason = GitHubTokenAcquireReason(obj.get("reason")) - registration_id = from_str(obj.get("registrationId")) - session_id = from_union([from_str, from_none], obj.get("sessionId")) - return GitHubTokenAcquireRequest(host, reason, registration_id, session_id) + 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["host"] = from_str(self.host) - result["reason"] = to_enum(GitHubTokenAcquireReason, self.reason) - result["registrationId"] = from_str(self.registration_id) - if self.session_id is not None: - result["sessionId"] = from_union([from_str, from_none], self.session_id) + 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 GitHubTokenAcquireResult: - """SDK host response to a GitHub credential request.""" - - kind: GitHubTokenAcquireResultKind - """GitHub credential response variant discriminator.""" +class MCPAppsReadResourceRequest: + """MCP server and resource URI to fetch.""" - access_token: str | None = None - """GitHub access token acquired by the SDK host.""" + server_name: str + """Name of the MCP server hosting the resource""" - expires_in: int | None = None - """Remaining token lifetime in seconds when callback execution completes. It must exceed the - one-hour preflight refresh threshold. - """ - token_type: str | None = None - """OAuth token type. Defaults to bearer when omitted.""" + uri: str + """Resource URI (typically ui://...)""" @staticmethod - def from_dict(obj: Any) -> 'GitHubTokenAcquireResult': + def from_dict(obj: Any) -> 'MCPAppsReadResourceRequest': assert isinstance(obj, dict) - kind = GitHubTokenAcquireResultKind(obj.get("kind")) - access_token = from_union([from_str, from_none], obj.get("accessToken")) - expires_in = from_union([from_int, from_none], obj.get("expiresIn")) - token_type = from_union([from_str, from_none], obj.get("tokenType")) - return GitHubTokenAcquireResult(kind, access_token, expires_in, token_type) + server_name = from_str(obj.get("serverName")) + uri = from_str(obj.get("uri")) + return MCPAppsReadResourceRequest(server_name, uri) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(GitHubTokenAcquireResultKind, self.kind) - if self.access_token is not None: - result["accessToken"] = from_union([from_str, from_none], self.access_token) - if self.expires_in is not None: - result["expiresIn"] = from_union([from_int, from_none], self.expires_in) - if self.token_type is not None: - result["tokenType"] = from_union([from_str, from_none], self.token_type) + result["serverName"] = from_str(self.server_name) + result["uri"] = from_str(self.uri) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPOauthPendingRequestResponse: - """Host response to the pending OAuth request.""" +class MCPAppsResourceContent: + """MCP Apps resource content with URI, optional MIME type, text or base64 blob, and resource + metadata. + """ + uri: str + """The resource URI (typically ui://...)""" - kind: GitHubTokenAcquireResultKind - """OAuth response variant discriminator.""" + meta: dict[str, Any] | None = None + """Resource-level metadata (CSP, permissions, etc.)""" - access_token: str | None = None - """Access token acquired by the SDK host""" + blob: str | None = None + """Base64-encoded binary content""" - expires_in: int | None = None - """Token lifetime in seconds, if known.""" + mime_type: str | None = None + """MIME type of the content""" - token_type: str | None = None - """OAuth token type. Defaults to bearer when omitted.""" + text: str | None = None + """Text content (e.g. HTML)""" @staticmethod - def from_dict(obj: Any) -> 'MCPOauthPendingRequestResponse': + def from_dict(obj: Any) -> 'MCPAppsResourceContent': assert isinstance(obj, dict) - kind = GitHubTokenAcquireResultKind(obj.get("kind")) - access_token = from_union([from_str, from_none], obj.get("accessToken")) - expires_in = from_union([from_int, from_none], obj.get("expiresIn")) - token_type = from_union([from_str, from_none], obj.get("tokenType")) - return MCPOauthPendingRequestResponse(kind, access_token, expires_in, token_type) + uri = from_str(obj.get("uri")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + blob = from_union([from_str, from_none], obj.get("blob")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + text = from_union([from_str, from_none], obj.get("text")) + return MCPAppsResourceContent(uri, meta, blob, mime_type, text) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(GitHubTokenAcquireResultKind, self.kind) - if self.access_token is not None: - result["accessToken"] = from_union([from_str, from_none], self.access_token) - if self.expires_in is not None: - result["expiresIn"] = from_union([from_int, from_none], self.expires_in) - if self.token_type is not None: - result["tokenType"] = from_union([from_str, from_none], self.token_type) + result["uri"] = from_str(self.uri) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.blob is not None: + result["blob"] = from_union([from_str, from_none], self.blob) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.text is not None: + result["text"] = from_union([from_str, from_none], self.text) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class HistoryCompactResult: - """Compaction outcome with the number of tokens and messages removed, summary text, and the - resulting context window breakdown. +class MCPResourceContent: + """MCP resource content with URI, optional MIME type, text or base64 blob, and resource + metadata. """ - messages_removed: int - """Number of messages removed during compaction""" + uri: str + """The resource URI""" - success: bool - """Whether compaction completed successfully""" + meta: dict[str, Any] | None = None + """Resource-level metadata (CSP, permissions, etc.)""" - tokens_removed: int - """Number of tokens freed by compaction""" + blob: str | None = None + """Base64-encoded binary content""" - context_window: HistoryCompactContextWindow | None = None - """Post-compaction context window usage breakdown""" + mime_type: str | None = None + """MIME type of the content""" - summary_content: str | None = None - """Summary text produced by compaction. Omitted when compaction did not produce a summary - (e.g. failure path). - """ + text: str | None = None + """Text content (e.g. HTML)""" @staticmethod - def from_dict(obj: Any) -> 'HistoryCompactResult': + def from_dict(obj: Any) -> 'MCPResourceContent': assert isinstance(obj, dict) - messages_removed = from_int(obj.get("messagesRemoved")) - success = from_bool(obj.get("success")) - tokens_removed = from_int(obj.get("tokensRemoved")) - context_window = from_union([HistoryCompactContextWindow.from_dict, from_none], obj.get("contextWindow")) - summary_content = from_union([from_str, from_none], obj.get("summaryContent")) - return HistoryCompactResult(messages_removed, success, tokens_removed, context_window, summary_content) + uri = from_str(obj.get("uri")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + blob = from_union([from_str, from_none], obj.get("blob")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + text = from_union([from_str, from_none], obj.get("text")) + return MCPResourceContent(uri, meta, blob, mime_type, text) def to_dict(self) -> dict: result: dict = {} - result["messagesRemoved"] = from_int(self.messages_removed) - result["success"] = from_bool(self.success) - result["tokensRemoved"] = from_int(self.tokens_removed) - if self.context_window is not None: - result["contextWindow"] = from_union([lambda x: to_class(HistoryCompactContextWindow, x), from_none], self.context_window) - if self.summary_content is not None: - result["summaryContent"] = from_union([from_str, from_none], self.summary_content) + result["uri"] = from_str(self.uri) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.blob is not None: + result["blob"] = from_union([from_str, from_none], self.blob) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.text is not None: + result["text"] = from_union([from_str, from_none], self.text) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class HistorySkippedFileRestore: - """A captured file that rewind intentionally left unchanged.""" +class MCPResourcesReadRequest: + """MCP server and resource URI to fetch.""" - path: str - """Absolute path of the skipped file.""" + server_name: str + """Name of the MCP server hosting the resource""" - reason: HistoryFileRestoreSkipReason - """Reason the file was not restored.""" + uri: str + """Resource URI""" @staticmethod - def from_dict(obj: Any) -> 'HistorySkippedFileRestore': + def from_dict(obj: Any) -> 'MCPResourcesReadRequest': assert isinstance(obj, dict) - path = from_str(obj.get("path")) - reason = HistoryFileRestoreSkipReason(obj.get("reason")) - return HistorySkippedFileRestore(path, reason) + server_name = from_str(obj.get("serverName")) + uri = from_str(obj.get("uri")) + return MCPResourcesReadRequest(server_name, uri) def to_dict(self) -> dict: result: dict = {} - result["path"] = from_str(self.path) - result["reason"] = to_enum(HistoryFileRestoreSkipReason, self.reason) + result["serverName"] = from_str(self.server_name) + result["uri"] = from_str(self.uri) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class HistoryListRewindPointsResult: - """Rewind points and file-change-tracking availability for the session.""" +class WorkflowDeclaredLimits: + """Declared or approved workflow resource ceilings. - file_change_tracking_enabled: bool - """Whether this session captured file changes from its first turn.""" + Resource ceilings declared by the workflow. + """ + max_ai_credits: float | None = None + """Maximum AI credits consumed by subagents and descendants.""" - points: list[HistoryRewindPoint] - """Root user turns in chronological order. Empty when `unavailableReason` is set.""" + max_concurrent_subagents: int | None = None + """Maximum concurrently active subagents.""" - unavailable_reason: HistoryRewindUnavailableReason | None = None - """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`. - """ + max_total_subagents: int | None = None + """Maximum total subagents spawned by the run.""" + + timeout_seconds: float | None = None + """Maximum accumulated active execution time in seconds.""" @staticmethod - def from_dict(obj: Any) -> 'HistoryListRewindPointsResult': + def from_dict(obj: Any) -> 'WorkflowDeclaredLimits': assert isinstance(obj, dict) - file_change_tracking_enabled = from_bool(obj.get("fileChangeTrackingEnabled")) - points = from_list(HistoryRewindPoint.from_dict, obj.get("points")) - unavailable_reason = from_union([HistoryRewindUnavailableReason, from_none], obj.get("unavailableReason")) - return HistoryListRewindPointsResult(file_change_tracking_enabled, points, unavailable_reason) + max_ai_credits = from_union([from_float, from_none], obj.get("maxAiCredits")) + max_concurrent_subagents = from_union([from_int, from_none], obj.get("maxConcurrentSubagents")) + max_total_subagents = from_union([from_int, from_none], obj.get("maxTotalSubagents")) + timeout_seconds = from_union([from_float, from_none], obj.get("timeoutSeconds")) + return WorkflowDeclaredLimits(max_ai_credits, max_concurrent_subagents, max_total_subagents, timeout_seconds) def to_dict(self) -> dict: result: dict = {} - result["fileChangeTrackingEnabled"] = from_bool(self.file_change_tracking_enabled) - result["points"] = from_list(lambda x: to_class(HistoryRewindPoint, x), self.points) - if self.unavailable_reason is not None: - result["unavailableReason"] = from_union([lambda x: to_enum(HistoryRewindUnavailableReason, x), from_none], self.unavailable_reason) + if self.max_ai_credits is not None: + result["maxAiCredits"] = from_union([to_float, from_none], self.max_ai_credits) + if self.max_concurrent_subagents is not None: + result["maxConcurrentSubagents"] = from_union([from_int, from_none], self.max_concurrent_subagents) + if self.max_total_subagents is not None: + result["maxTotalSubagents"] = from_union([from_int, from_none], self.max_total_subagents) + if self.timeout_seconds is not None: + result["timeoutSeconds"] = from_union([to_float, from_none], self.timeout_seconds) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class WorkflowRunFailureKind(Enum): + """Resource ceiling that stopped the run. + + Cumulative resource ceiling that stopped a workflow run. + """ + MAX_AI_CREDITS = "maxAiCredits" + MAX_TOTAL_SUBAGENTS = "maxTotalSubagents" + TIMEOUT_SECONDS = "timeoutSeconds" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class HistoryRewindFilePreview: - """A file that a conversation-and-files rewind would restore.""" +class MCPPlanConfigurationChange: + """The configuration change for the selected alternative only. - change_type: HistoryRewindChangeType - """Aggregate change made across the discarded turns.""" + The configuration-change alternative for the transportChoices entry at the same index. + Only the selected alternative is applied; entries are not cumulative. The payload stays + behind the runtime boundary. + """ + changed_fields: list[str] + """Names of the configuration fields the change would set, without their values.""" - lines_added: int - """Lines added across the discarded turns.""" + config_key: str + """Configuration key the change applies to.""" - lines_removed: int - """Lines removed across the discarded turns.""" + operation: MCPPlanConfigurationOperation + """Whether the change would create a new entry or modify an existing one.""" - path: str - """Absolute path of the captured file.""" + scope: MCPPlanScope + """Scope the change would be written to.""" + + secret_references: list[str] + """Secret placeholders the written configuration would reference. The constrained + placeholder type cannot carry a literal secret value. + """ @staticmethod - def from_dict(obj: Any) -> 'HistoryRewindFilePreview': + def from_dict(obj: Any) -> 'MCPPlanConfigurationChange': assert isinstance(obj, dict) - change_type = HistoryRewindChangeType(obj.get("changeType")) - lines_added = from_int(obj.get("linesAdded")) - lines_removed = from_int(obj.get("linesRemoved")) - path = from_str(obj.get("path")) - return HistoryRewindFilePreview(change_type, lines_added, lines_removed, path) + changed_fields = from_list(from_str, obj.get("changedFields")) + config_key = from_str(obj.get("configKey")) + operation = MCPPlanConfigurationOperation(obj.get("operation")) + scope = MCPPlanScope(obj.get("scope")) + secret_references = from_list(from_str, obj.get("secretReferences")) + return MCPPlanConfigurationChange(changed_fields, config_key, operation, scope, secret_references) def to_dict(self) -> dict: result: dict = {} - result["changeType"] = to_enum(HistoryRewindChangeType, self.change_type) - result["linesAdded"] = from_int(self.lines_added) - result["linesRemoved"] = from_int(self.lines_removed) - result["path"] = from_str(self.path) + result["changedFields"] = from_list(from_str, self.changed_fields) + result["configKey"] = from_str(self.config_key) + result["operation"] = to_enum(MCPPlanConfigurationOperation, self.operation) + result["scope"] = to_enum(MCPPlanScope, self.scope) + result["secretReferences"] = from_list(from_str, self.secret_references) return result -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class HistoryRewindRequest: - """Boundary and mode for rewinding session history.""" +class MCPPlanTargetClass: + """Exact reviewed user-scope destination. - event_id: str - """ID of the user.message event that begins the discarded suffix.""" + Where a plan would be written. - mode: HistoryRewindMode - """Whether to rewind only conversation history or also restore captured files.""" + Exact destination, checked for intervening changes before mutation. + + Configuration scope and key the plan would write to. + """ + config_key: str + """Configuration key the server would be recorded under within that scope.""" + + scope: MCPPlanScope + """Configuration scope the plan targets.""" @staticmethod - def from_dict(obj: Any) -> 'HistoryRewindRequest': + def from_dict(obj: Any) -> 'MCPPlanTargetClass': assert isinstance(obj, dict) - event_id = from_str(obj.get("eventId")) - mode = HistoryRewindMode(obj.get("mode")) - return HistoryRewindRequest(event_id, mode) + config_key = from_str(obj.get("configKey")) + scope = MCPPlanScope(obj.get("scope")) + return MCPPlanTargetClass(config_key, scope) def to_dict(self) -> dict: result: dict = {} - result["eventId"] = from_str(self.event_id) - result["mode"] = to_enum(HistoryRewindMode, self.mode) + result["configKey"] = from_str(self.config_key) + result["scope"] = to_enum(MCPPlanScope, self.scope) return result -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class InstalledPluginSource: - """Source descriptor for a direct GitHub plugin install, with `owner/repo`, optional ref or - full commit SHA, and optional subpath. - - Source descriptor for a direct URL plugin install, with URL, optional ref or full commit - SHA, and optional subpath. +class MCPInstallationRemoteConfiguration: + """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. - Source descriptor for a direct local plugin install, with a local filesystem path. + Final remote configuration, not a template. The producer refuses external-value + expansion before presenting this review. Receipt-owned secrets appear only as + `${installation-secret:}` references whose `` matches a reviewed + `${secret:}` placeholder; values are never included. """ - source: InstalledPluginSourceURLSource - """Constant value. Always "github". + headers: dict[str, str] + """Configured headers, excluding separately authorised OAuth tokens. Values may + contain owned secret references, never secret values. + """ + tools: list[str] + """Configured tool selection, not permission to invoke those tools.""" - Constant value. Always "url". + transport: MCPPlanRemoteTransport + """Transport in the effective persisted remote configuration.""" - Constant value. Always "local". - """ - path: str | None = None - """Optional repository-relative path to the plugin. + url: str + """Exact resolved endpoint, without templates or secret placeholders.""" - Optional source-relative path to the plugin. + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationRemoteConfiguration': + assert isinstance(obj, dict) + headers = from_dict(from_str, obj.get("headers")) + tools = from_list(from_str, obj.get("tools")) + transport = MCPPlanRemoteTransport(obj.get("transport")) + url = from_str(obj.get("url")) + return MCPInstallationRemoteConfiguration(headers, tools, transport, url) - Local filesystem path to the plugin. - """ - ref: str | None = None - """Optional Git ref to resolve.""" + def to_dict(self) -> dict: + result: dict = {} + result["headers"] = from_dict(from_str, self.headers) + result["tools"] = from_list(from_str, self.tools) + result["transport"] = to_enum(MCPPlanRemoteTransport, self.transport) + result["url"] = from_str(self.url) + return result - repo: str | None = None - """GitHub repository in `owner/repo` form.""" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPInstallationInput: + """One Registry string-valued configuration entry for the selected transport.""" - sha: str | None = None - """Optional full 40-character hexadecimal commit SHA.""" + category: MCPPlanValueCategory + """Exact category declared by the selected choice.""" - url: str | None = None - """URL of the plugin source.""" + key: str + """Exact key declared by the selected choice.""" + + value: str + """Explicit non-secret value. Secret placeholders use a separate input channel.""" @staticmethod - def from_dict(obj: Any) -> 'InstalledPluginSource': + def from_dict(obj: Any) -> 'MCPInstallationInput': assert isinstance(obj, dict) - source = InstalledPluginSourceURLSource(obj.get("source")) - path = from_union([from_str, from_none], obj.get("path")) - ref = from_union([from_str, from_none], obj.get("ref")) - repo = from_union([from_str, from_none], obj.get("repo")) - sha = from_union([from_str, from_none], obj.get("sha")) - url = from_union([from_str, from_none], obj.get("url")) - return InstalledPluginSource(source, path, ref, repo, sha, url) + category = MCPPlanValueCategory(obj.get("category")) + key = from_str(obj.get("key")) + value = from_str(obj.get("value")) + return MCPInstallationInput(category, key, value) def to_dict(self) -> dict: result: dict = {} - result["source"] = to_enum(InstalledPluginSourceURLSource, self.source) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.ref is not None: - result["ref"] = from_union([from_str, from_none], self.ref) - if self.repo is not None: - result["repo"] = from_union([from_str, from_none], self.repo) - if self.sha is not None: - result["sha"] = from_union([from_str, from_none], self.sha) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) + result["category"] = to_enum(MCPPlanValueCategory, self.category) + result["key"] = from_str(self.key) + result["value"] = from_str(self.value) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionInstalledPluginSource: - """Source descriptor for a direct GitHub plugin install, with `owner/repo`, optional ref or - full commit SHA, and optional subpath. +class MCPPlanPolicyResult: + """Policy decision bound to this plan. - Source descriptor for a direct URL plugin install, with URL, optional ref or full commit - SHA, and optional subpath. + Outcome of evaluating the planned server against registry and enterprise policy. + Evaluation is read-only. - Source descriptor for a direct local plugin install, with a local filesystem path. + Current removal policy, independent of permission to activate the server. + + Outcome of evaluating the server against registry and enterprise policy. """ - source: InstalledPluginSourceURLSource - """Constant value. Always "github". + decision: MCPPlanPolicyDecision + """What policy decided for this server.""" - Constant value. Always "url". + source: MCPPlanPolicySource + """Which authority produced the decision.""" - Constant value. Always "local". + reason: str | None = None + """Human-readable explanation, safe to surface. Never contains a query, URL, handle, or + secret. """ - path: str | None = None - """Optional repository-relative path to the plugin. - Optional source-relative path to the plugin. + @staticmethod + def from_dict(obj: Any) -> 'MCPPlanPolicyResult': + assert isinstance(obj, dict) + decision = MCPPlanPolicyDecision(obj.get("decision")) + source = MCPPlanPolicySource(obj.get("source")) + reason = from_union([from_str, from_none], obj.get("reason")) + return MCPPlanPolicyResult(decision, source, reason) - Local filesystem path to the plugin. - """ - ref: str | None = None - """Optional Git ref to resolve.""" + def to_dict(self) -> dict: + result: dict = {} + result["decision"] = to_enum(MCPPlanPolicyDecision, self.decision) + result["source"] = to_enum(MCPPlanPolicySource, self.source) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + return result - repo: str | None = None - """GitHub repository in `owner/repo` form.""" +@dataclass +class InstallationConfirmationResponse: + """A response is meaningful only on the connection and request that issued its challenge.""" - sha: str | None = None - """Optional full 40-character hexadecimal commit SHA.""" + confirmation_id: str + """Exact challenge from the request.""" - url: str | None = None - """URL of the plugin source.""" + decision: InstallationDecision + """Fresh explicit user decision. There is no default.""" + + review_fingerprint: str + """Exact review commitment from the request.""" @staticmethod - def from_dict(obj: Any) -> 'SessionInstalledPluginSource': + def from_dict(obj: Any) -> 'InstallationConfirmationResponse': assert isinstance(obj, dict) - source = InstalledPluginSourceURLSource(obj.get("source")) - path = from_union([from_str, from_none], obj.get("path")) - ref = from_union([from_str, from_none], obj.get("ref")) - repo = from_union([from_str, from_none], obj.get("repo")) - sha = from_union([from_str, from_none], obj.get("sha")) - url = from_union([from_str, from_none], obj.get("url")) - return SessionInstalledPluginSource(source, path, ref, repo, sha, url) + confirmation_id = from_str(obj.get("confirmationId")) + decision = InstallationDecision(obj.get("decision")) + review_fingerprint = from_str(obj.get("reviewFingerprint")) + return InstallationConfirmationResponse(confirmation_id, decision, review_fingerprint) def to_dict(self) -> dict: result: dict = {} - result["source"] = to_enum(InstalledPluginSourceURLSource, self.source) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.ref is not None: - result["ref"] = from_union([from_str, from_none], self.ref) - if self.repo is not None: - result["repo"] = from_union([from_str, from_none], self.repo) - if self.sha is not None: - result["sha"] = from_union([from_str, from_none], self.sha) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) + result["confirmationId"] = from_str(self.confirmation_id) + result["decision"] = to_enum(InstallationDecision, self.decision) + result["reviewFingerprint"] = from_str(self.review_fingerprint) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -21634,6 +24457,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: @@ -21754,84 +24666,65 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPAppsSetHostContextDetails: - """Host context advertised to MCP App guests""" - - available_display_modes: list[MCPAppsDisplayMode] | None = None - """Display modes the host supports""" - - display_mode: MCPAppsDisplayMode | None = None - """Current display mode (SEP-1865)""" - - locale: str | None = None - """BCP-47 locale, e.g. 'en-US'""" - - platform: MCPAppsHostContextDetailsPlatform | None = None - """Platform type for responsive design""" - - theme: Theme | None = None - """UI theme preference per SEP-1865""" - - time_zone: str | None = None - """IANA timezone, e.g. 'America/New_York'""" - - user_agent: str | None = None - """Host application identifier""" - - @staticmethod - def from_dict(obj: Any) -> 'MCPAppsSetHostContextDetails': - assert isinstance(obj, dict) - available_display_modes = from_union([lambda x: from_list(MCPAppsDisplayMode, x), from_none], obj.get("availableDisplayModes")) - display_mode = from_union([MCPAppsDisplayMode, from_none], obj.get("displayMode")) - locale = from_union([from_str, from_none], obj.get("locale")) - platform = from_union([MCPAppsHostContextDetailsPlatform, from_none], obj.get("platform")) - theme = from_union([Theme, from_none], obj.get("theme")) - time_zone = from_union([from_str, from_none], obj.get("timeZone")) - user_agent = from_union([from_str, from_none], obj.get("userAgent")) - return MCPAppsSetHostContextDetails(available_display_modes, display_mode, locale, platform, theme, time_zone, user_agent) - - def to_dict(self) -> dict: - result: dict = {} - if self.available_display_modes is not None: - result["availableDisplayModes"] = from_union([lambda x: from_list(lambda x: to_enum(MCPAppsDisplayMode, x), x), from_none], self.available_display_modes) - if self.display_mode is not None: - result["displayMode"] = from_union([lambda x: to_enum(MCPAppsDisplayMode, x), from_none], self.display_mode) - if self.locale is not None: - result["locale"] = from_union([from_str, from_none], self.locale) - if self.platform is not None: - result["platform"] = from_union([lambda x: to_enum(MCPAppsHostContextDetailsPlatform, x), from_none], self.platform) - if self.theme is not None: - result["theme"] = from_union([lambda x: to_enum(Theme, x), from_none], self.theme) - if self.time_zone is not None: - result["timeZone"] = from_union([from_str, from_none], self.time_zone) - if self.user_agent is not None: - result["userAgent"] = from_union([from_str, from_none], self.user_agent) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPAppsReadResourceResult: - """Resource contents returned by the MCP server.""" +class MCPAppsSetHostContextDetails: + """Host context advertised to MCP App guests""" - contents: list[MCPAppsResourceContent] - """Resource contents returned by the server""" + available_display_modes: list[MCPAppsDisplayMode] | None = None + """Display modes the host supports""" + + display_mode: MCPAppsDisplayMode | None = None + """Current display mode (SEP-1865)""" + + locale: str | None = None + """BCP-47 locale, e.g. 'en-US'""" + + platform: MCPAppsHostContextDetailsPlatform | None = None + """Platform type for responsive design""" + + theme: Theme | None = None + """UI theme preference per SEP-1865""" + + time_zone: str | None = None + """IANA timezone, e.g. 'America/New_York'""" + + user_agent: str | None = None + """Host application identifier""" @staticmethod - def from_dict(obj: Any) -> 'MCPAppsReadResourceResult': + def from_dict(obj: Any) -> 'MCPAppsSetHostContextDetails': assert isinstance(obj, dict) - contents = from_list(MCPAppsResourceContent.from_dict, obj.get("contents")) - return MCPAppsReadResourceResult(contents) + available_display_modes = from_union([lambda x: from_list(MCPAppsDisplayMode, x), from_none], obj.get("availableDisplayModes")) + display_mode = from_union([MCPAppsDisplayMode, from_none], obj.get("displayMode")) + locale = from_union([from_str, from_none], obj.get("locale")) + platform = from_union([MCPAppsHostContextDetailsPlatform, from_none], obj.get("platform")) + theme = from_union([Theme, from_none], obj.get("theme")) + time_zone = from_union([from_str, from_none], obj.get("timeZone")) + user_agent = from_union([from_str, from_none], obj.get("userAgent")) + return MCPAppsSetHostContextDetails(available_display_modes, display_mode, locale, platform, theme, time_zone, user_agent) def to_dict(self) -> dict: result: dict = {} - result["contents"] = from_list(lambda x: to_class(MCPAppsResourceContent, x), self.contents) + if self.available_display_modes is not None: + result["availableDisplayModes"] = from_union([lambda x: from_list(lambda x: to_enum(MCPAppsDisplayMode, x), x), from_none], self.available_display_modes) + if self.display_mode is not None: + result["displayMode"] = from_union([lambda x: to_enum(MCPAppsDisplayMode, x), from_none], self.display_mode) + if self.locale is not None: + result["locale"] = from_union([from_str, from_none], self.locale) + if self.platform is not None: + result["platform"] = from_union([lambda x: to_enum(MCPAppsHostContextDetailsPlatform, x), from_none], self.platform) + if self.theme is not None: + result["theme"] = from_union([lambda x: to_enum(Theme, x), from_none], self.theme) + if self.time_zone is not None: + result["timeZone"] = from_union([from_str, from_none], self.time_zone) + if self.user_agent is not None: + result["userAgent"] = from_union([from_str, from_none], self.user_agent) return result # Experimental: this type is part of an experimental API and may change or be removed. @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""" @@ -21857,6 +24750,9 @@ class MCPOauthLoginRequest: ephemeral host-owned secret, uses it for this authentication attempt and does not persist it. """ + expected_installation_id: str | None = field(default=None, kw_only=True) + """Exact owned receipt identity. Owned login never uses an implicit helper session.""" + force_reauth: bool | None = None """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 @@ -21866,11 +24762,22 @@ class MCPOauthLoginRequest: """Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. """ + login_id: str | None = field(default=None, kw_only=True) + """Required for owned login. Consumes the exact prepareLogin handle once. + Set forceReauth and display options during preparation, not consumption. + """ public_client: bool | None = None """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. """ + 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': @@ -21880,10 +24787,13 @@ def from_dict(obj: Any) -> 'MCPOauthLoginRequest': client_id = from_union([from_str, from_none], obj.get("clientId")) client_name = from_union([from_str, from_none], obj.get("clientName")) client_secret = from_union([from_str, from_none], obj.get("clientSecret")) + expected_installation_id = from_union([from_str, from_none], obj.get("expectedInstallationId")) force_reauth = from_union([from_bool, from_none], obj.get("forceReauth")) 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) + 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 = {} @@ -21896,12 +24806,18 @@ def to_dict(self) -> dict: result["clientName"] = from_union([from_str, from_none], self.client_name) if self.client_secret is not None: result["clientSecret"] = from_union([from_str, from_none], self.client_secret) + if self.expected_installation_id is not None: + result["expectedInstallationId"] = from_union([from_str, from_none], self.expected_installation_id) if self.force_reauth is not None: result["forceReauth"] = from_union([from_bool, from_none], self.force_reauth) if self.grant_type is not None: result["grantType"] = from_union([lambda x: to_enum(MCPGrantType, x), from_none], self.grant_type) + if self.login_id is not None: + 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. @@ -22025,138 +24941,103 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPPlanConfigurationChange: - """The configuration-change alternative for the transportChoices entry at the same index. - Only the selected alternative is applied; entries are not cumulative. The payload stays - behind the runtime boundary. - """ - changed_fields: list[str] - """Names of the configuration fields the change would set, without their values.""" - - config_key: str - """Configuration key the change applies to.""" +class MCPInstallationSummary: + """Durable configuration ownership is distinct from session-specific usability. - operation: MCPPlanConfigurationOperation - """Whether the change would create a new entry or modify an existing one.""" - - scope: MCPPlanScope - """Scope the change would be written to.""" + Original owned receipt being removed. - secret_references: list[str] - """Secret placeholders the written configuration would reference. The constrained - placeholder type cannot carry a literal secret value. + Receipt identity produced by the confirmed transaction. """ + choice_id: str + """Exact alternative retained in the installing receipt.""" - @staticmethod - def from_dict(obj: Any) -> 'MCPPlanConfigurationChange': - assert isinstance(obj, dict) - changed_fields = from_list(from_str, obj.get("changedFields")) - config_key = from_str(obj.get("configKey")) - operation = MCPPlanConfigurationOperation(obj.get("operation")) - scope = MCPPlanScope(obj.get("scope")) - secret_references = from_list(from_str, obj.get("secretReferences")) - return MCPPlanConfigurationChange(changed_fields, config_key, operation, scope, secret_references) + identity: MCPPlanResourceIdentity + """Identity retained from the validated original plan.""" - def to_dict(self) -> dict: - result: dict = {} - result["changedFields"] = from_list(from_str, self.changed_fields) - result["configKey"] = from_str(self.config_key) - result["operation"] = to_enum(MCPPlanConfigurationOperation, self.operation) - result["scope"] = to_enum(MCPPlanScope, self.scope) - result["secretReferences"] = from_list(from_str, self.secret_references) - return result + installation_id: str + """Exact durable installation receipt identity.""" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPPlanTarget: - """Configuration scope and key the plan would write to. + operation_id: str + """Original installing operation, not a fresh management operation.""" - Where a plan would be written. - """ - config_key: str - """Configuration key the server would be recorded under within that scope.""" + state: MCPInstallationState + """Ownership or setup state, never inferred proof of tool usability.""" - scope: MCPPlanScope - """Configuration scope the plan targets.""" + catalogue: InstallationCatalogueIdentity | None = None + """Catalogue identity retained from the installed plan when available.""" + + installed_at: str | None = None + """ISO 8601 wall-clock installation time when available.""" @staticmethod - def from_dict(obj: Any) -> 'MCPPlanTarget': + def from_dict(obj: Any) -> 'MCPInstallationSummary': assert isinstance(obj, dict) - config_key = from_str(obj.get("configKey")) - scope = MCPPlanScope(obj.get("scope")) - return MCPPlanTarget(config_key, scope) + choice_id = from_str(obj.get("choiceId")) + identity = MCPPlanResourceIdentity.from_dict(obj.get("identity")) + installation_id = from_str(obj.get("installationId")) + operation_id = from_str(obj.get("operationId")) + state = MCPInstallationState(obj.get("state")) + catalogue = from_union([InstallationCatalogueIdentity.from_dict, from_none], obj.get("catalogue")) + installed_at = from_union([from_str, from_none], obj.get("installedAt")) + return MCPInstallationSummary(choice_id, identity, installation_id, operation_id, state, catalogue, installed_at) def to_dict(self) -> dict: result: dict = {} - result["configKey"] = from_str(self.config_key) - result["scope"] = to_enum(MCPPlanScope, self.scope) + result["choiceId"] = from_str(self.choice_id) + result["identity"] = to_class(MCPPlanResourceIdentity, self.identity) + result["installationId"] = from_str(self.installation_id) + result["operationId"] = from_str(self.operation_id) + result["state"] = to_enum(MCPInstallationState, self.state) + if self.catalogue is not None: + result["catalogue"] = from_union([lambda x: to_class(InstallationCatalogueIdentity, x), from_none], self.catalogue) + if self.installed_at is not None: + result["installedAt"] = from_union([from_str, from_none], self.installed_at) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPPlanInstallRequest: - """A side-effect-free request for an MCP install plan. Computing a plan never writes - configuration, stores a secret, or reloads MCP servers. +class MCPOauthLoginResult: + """OAuth authorization URL the caller should open, or empty when cached tokens already + authenticated the server. """ - contract: CatalogClientContract - """Protocol version and capabilities the caller requires.""" - - source: MCPPlanInstallSource - """What to plan: either a candidate handle from a previous search, or a card supplied - directly. + 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. """ - scope: MCPPlanScope | None = None - """Configuration scope the plan targets. Defaults to user scope when omitted.""" - - @staticmethod - def from_dict(obj: Any) -> 'MCPPlanInstallRequest': - assert isinstance(obj, dict) - contract = CatalogClientContract.from_dict(obj.get("contract")) - source = _load_MCPPlanInstallSource(obj.get("source")) - scope = from_union([MCPPlanScope, from_none], obj.get("scope")) - return MCPPlanInstallRequest(contract, source, scope) - - def to_dict(self) -> dict: - result: dict = {} - result["contract"] = to_class(CatalogClientContract, self.contract) - result["source"] = (self.source).to_dict() - if self.scope is not None: - result["scope"] = from_union([lambda x: to_enum(MCPPlanScope, x), from_none], self.scope) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPPlanPolicyResult: - """Outcome of evaluating the server against registry and enterprise policy. - - Outcome of evaluating the planned server against registry and enterprise policy. - Evaluation is read-only. + 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. 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. """ - decision: MCPPlanPolicyDecision - """What policy decided for this server.""" - - source: MCPPlanPolicySource - """Which authority produced the decision.""" + login_id: str | None = None + """Runtime-issued owned flow identity; never a server name or installation operation ID.""" - reason: str | None = None - """Human-readable explanation, safe to surface. Never contains a query, URL, handle, or - secret. - """ + status: MCPOwnedOauthLoginStatus | None = None + """Explicit outcome for owned sign-in. Manual callers retain their legacy response shape.""" @staticmethod - def from_dict(obj: Any) -> 'MCPPlanPolicyResult': + def from_dict(obj: Any) -> 'MCPOauthLoginResult': assert isinstance(obj, dict) - decision = MCPPlanPolicyDecision(obj.get("decision")) - source = MCPPlanPolicySource(obj.get("source")) - reason = from_union([from_str, from_none], obj.get("reason")) - return MCPPlanPolicyResult(decision, source, reason) + 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_id, authorization_url, login_id, status) def to_dict(self) -> dict: result: dict = {} - result["decision"] = to_enum(MCPPlanPolicyDecision, self.decision) - result["source"] = to_enum(MCPPlanPolicySource, self.source) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) + 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: + result["loginId"] = from_union([from_str, from_none], self.login_id) + if self.status is not None: + result["status"] = from_union([lambda x: to_enum(MCPOwnedOauthLoginStatus, x), from_none], self.status) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -22445,21 +25326,39 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPResourcesReadResult: - """Resource contents returned by the MCP server.""" +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""" - contents: list[MCPResourceContent] - """Resource contents returned by the server""" + 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) -> 'MCPResourcesReadResult': + def from_dict(obj: Any) -> 'MCPPromptMessage': assert isinstance(obj, dict) - contents = from_list(MCPResourceContent.from_dict, obj.get("contents")) - return MCPResourcesReadResult(contents) + 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["contents"] = from_list(lambda x: to_class(MCPResourceContent, x), self.contents) + 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. @@ -22595,6 +25494,59 @@ def to_dict(self) -> dict: result["mode"] = to_enum(MCPSetEnvValueModeDetails, self.mode) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorDiscoveryCatalogEntry: + """Entry.""" + + display_name: str + """Display name.""" + + name: str + """Name.""" + + status: ConnectorCatalogStatus + """Status.""" + + description: str | None = None + """Description.""" + + logo: str | None = None + """Logo.""" + + release_tag: str | None = None + """Release tag.""" + + tier: str | None = None + """Tier.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDiscoveryCatalogEntry': + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + name = from_str(obj.get("name")) + status = ConnectorCatalogStatus(obj.get("status")) + description = from_union([from_str, from_none], obj.get("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 ConnectorDiscoveryCatalogEntry(display_name, name, status, description, logo, release_tag, tier) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["name"] = from_str(self.name) + 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. class DebugCollectLogsEntryKind(Enum): """Kind of source path to include. @@ -22615,7 +25567,8 @@ class SessionContextAttribution: individual messages are available separately via `metadata.getContextHeaviestMessages`. """ buffer_tokens: int - """Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors + """Output reservation overlapping the displayed prompt allowance plus the tokens past the + effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. """ categories: Categories @@ -22636,9 +25589,9 @@ class SessionContextAttribution: kinds generically. Nesting and rollups are expressed via `parentId`. """ limit: int - """Prompt limit plus the model's output reserve: the full context window - `categories.freeSpace` and `categories.buffer` are measured against. Mirrors - `SessionContextInfo.limit`. + """Advertised prompt allowance for the selected context tier: the denominator for + context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. + Mirrors `SessionContextInfo.limit`. """ model_id: str """The concrete model id the entire breakdown was tokenized against (feeds the per-model @@ -22652,8 +25605,8 @@ class SessionContextAttribution: model), `default` (a fallback before any model is known). """ prompt_token_limit: int - """Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` - context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + """Effective input budget after reserving requested output against the combined context + ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. """ total_tokens: int """Total token count of the current context window the entries are measured against (system @@ -23008,44 +25961,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: @@ -23135,55 +26050,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 @@ -23207,6 +26143,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.""" @@ -23226,10 +26166,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 = {} @@ -23245,6 +26186,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: @@ -23253,6 +26196,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: @@ -23562,84 +26590,6 @@ def to_dict(self) -> dict: result["operation"] = from_union([from_str, from_none], self.operation) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecisionApproveForLocationApprovalFactory: - """Location-scoped factory approval, optionally narrowed by approval key.""" - - kind: ClassVar[str] = "factory" - """Approval covering factory operations.""" - - approval_key: str | None = None - """Optional factory operation name or canonical approval key; when omitted, the approval - covers all factory operations. - """ - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalFactory': - assert isinstance(obj, dict) - approval_key = from_union([from_str, from_none], obj.get("approvalKey")) - return PermissionDecisionApproveForLocationApprovalFactory(approval_key) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = self.kind - if self.approval_key is not None: - result["approvalKey"] = from_union([from_str, from_none], self.approval_key) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecisionApproveForSessionApprovalFactory: - """Session-scoped factory approval, optionally narrowed by approval key.""" - - kind: ClassVar[str] = "factory" - """Approval covering factory operations.""" - - approval_key: str | None = None - """Optional factory operation name or canonical approval key; when omitted, the approval - covers all factory operations. - """ - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalFactory': - assert isinstance(obj, dict) - approval_key = from_union([from_str, from_none], obj.get("approvalKey")) - return PermissionDecisionApproveForSessionApprovalFactory(approval_key) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = self.kind - if self.approval_key is not None: - result["approvalKey"] = from_union([from_str, from_none], self.approval_key) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionsLocationsAddToolApprovalDetailsFactory: - """Location-persisted factory approval, optionally narrowed by approval key.""" - - kind: ClassVar[str] = "factory" - """Approval covering factory operations.""" - - approval_key: str | None = None - """Optional factory operation name or canonical approval key; when omitted, the approval - covers all factory operations. - """ - - @staticmethod - def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsFactory': - assert isinstance(obj, dict) - approval_key = from_union([from_str, from_none], obj.get("approvalKey")) - return PermissionsLocationsAddToolApprovalDetailsFactory(approval_key) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = self.kind - if self.approval_key is not None: - result["approvalKey"] = from_union([from_str, from_none], self.approval_key) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionDecisionApproveForLocationApprovalMCPSampling: @@ -23763,6 +26713,84 @@ def to_dict(self) -> dict: result["kind"] = self.kind return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionDecisionApproveForLocationApprovalWorkflow: + """Location-scoped workflow approval, optionally narrowed by approval key.""" + + kind: ClassVar[str] = "workflow" + """Approval covering workflow operations.""" + + approval_key: str | None = None + """Optional workflow operation name or canonical approval key; when omitted, the approval + covers all workflow operations. + """ + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalWorkflow': + assert isinstance(obj, dict) + approval_key = from_union([from_str, from_none], obj.get("approvalKey")) + return PermissionDecisionApproveForLocationApprovalWorkflow(approval_key) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.approval_key is not None: + result["approvalKey"] = from_union([from_str, from_none], self.approval_key) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionDecisionApproveForSessionApprovalWorkflow: + """Session-scoped workflow approval, optionally narrowed by approval key.""" + + kind: ClassVar[str] = "workflow" + """Approval covering workflow operations.""" + + approval_key: str | None = None + """Optional workflow operation name or canonical approval key; when omitted, the approval + covers all workflow operations. + """ + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalWorkflow': + assert isinstance(obj, dict) + approval_key = from_union([from_str, from_none], obj.get("approvalKey")) + return PermissionDecisionApproveForSessionApprovalWorkflow(approval_key) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.approval_key is not None: + result["approvalKey"] = from_union([from_str, from_none], self.approval_key) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionsLocationsAddToolApprovalDetailsWorkflow: + """Location-persisted workflow approval, optionally narrowed by approval key.""" + + kind: ClassVar[str] = "workflow" + """Approval covering workflow operations.""" + + approval_key: str | None = None + """Optional workflow operation name or canonical approval key; when omitted, the approval + covers all workflow operations. + """ + + @staticmethod + def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsWorkflow': + assert isinstance(obj, dict) + approval_key = from_union([from_str, from_none], obj.get("approvalKey")) + return PermissionsLocationsAddToolApprovalDetailsWorkflow(approval_key) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.approval_key is not None: + result["approvalKey"] = from_union([from_str, from_none], self.approval_key) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionDecisionApproveForLocationApprovalWrite: @@ -23895,6 +26923,30 @@ def to_dict(self) -> dict: result["kind"] = self.kind return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionDecisionApproveReadOnlyForSession: + """Approve file-tool read access to specific directories for this logical session, including + continuation or resume. + """ + directories: list[str] + """Canonical directories covered by the read-only grant""" + + kind: ClassVar[str] = "approve-read-only-for-session" + """Approve read-only file-tool directory access for this session""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApproveReadOnlyForSession': + assert isinstance(obj, dict) + directories = from_list(from_str, obj.get("directories")) + return PermissionDecisionApproveReadOnlyForSession(directories) + + def to_dict(self) -> dict: + result: dict = {} + result["directories"] = from_list(from_str, self.directories) + result["kind"] = self.kind + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionDecisionApproved: @@ -24615,6 +27667,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: @@ -24633,6 +27742,11 @@ class MCPServer: error: str | None = None """Error message if the server failed to connect""" + owned: MCPServerOwnership | None = field(default=None, kw_only=True) + """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. + """ server_metadata: McpServerMetadata | None = None """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 @@ -24647,6 +27761,11 @@ class MCPServer: source_plugin_version: str | None = None """Plugin version that provided this server, when source is plugin.""" + url: str | None = field(default=None, kw_only=True) + """Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for + local and in-memory servers. + """ + @staticmethod def from_dict(obj: Any) -> 'MCPServer': assert isinstance(obj, dict) @@ -24654,11 +27773,13 @@ def from_dict(obj: Any) -> 'MCPServer': status = McpServerStatus(obj.get("status")) display_name = from_union([from_str, from_none], obj.get("displayName")) error = from_union([from_str, from_none], obj.get("error")) + owned = from_union([MCPServerOwnership.from_dict, from_none], obj.get("owned")) server_metadata = from_union([McpServerMetadata.from_dict, from_none], obj.get("serverMetadata")) 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 MCPServer(name, status, display_name, error, server_metadata, source, source_plugin, source_plugin_version) + url = from_union([from_str, from_none], obj.get("url")) + return MCPServer(name, status, display_name, error, server_metadata, source, source_plugin, source_plugin_version, url=url, owned=owned) def to_dict(self) -> dict: result: dict = {} @@ -24668,6 +27789,8 @@ def to_dict(self) -> dict: result["displayName"] = from_union([from_str, from_none], self.display_name) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) + if self.owned is not None: + result["owned"] = from_union([lambda x: to_class(MCPServerOwnership, x), from_none], self.owned) if self.server_metadata is not None: result["serverMetadata"] = from_union([lambda x: to_class(McpServerMetadata, x), from_none], self.server_metadata) if self.source is not None: @@ -24676,6 +27799,8 @@ def to_dict(self) -> dict: 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) + 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. @@ -25047,17 +28172,6 @@ def to_dict(self) -> dict: result["input"] = self.input return result -# Experimental: this type is part of an experimental API and may change or be removed. -class PauseCheckpointAction(Enum): - """Action the runtime selected for a durable factory pause checkpoint. - - Whether this execution attempt must pause or may continue. - - Action the runtime selected for a durable workflow pause checkpoint. - """ - CONTINUE = "continue" - PAUSE = "pause" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModeSetRequest: @@ -25185,6 +28299,15 @@ def to_dict(self) -> dict: result["action"] = from_str(self.action) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class WorkflowPauseCheckpointAction(Enum): + """Whether this execution attempt must pause or may continue. + + Action the runtime selected for a durable workflow pause checkpoint. + """ + CONTINUE = "continue" + PAUSE = "pause" + # Experimental: this type is part of an experimental API and may change or be removed. class UISessionLimitsExhaustedResponseAction(Enum): """Action selected by the user. @@ -25198,27 +28321,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. @@ -25244,34 +28370,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: @@ -26223,6 +29321,34 @@ def to_dict(self) -> dict: result["type"] = to_enum(ResponseFormatType, self.type) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxCredentialsConfig: + """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. + + Whole-value environment credential masking for sandboxed children. + """ + env_vars: dict[str, SandboxMaskedEnvVar] + """Environment variable names and their HTTPS injection destinations. Absent variables stay + absent. No real values or sentinels are stored in this map. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SandboxCredentialsConfig': + assert isinstance(obj, dict) + env_vars = from_dict(SandboxMaskedEnvVar.from_dict, obj.get("envVars")) + return SandboxCredentialsConfig(env_vars) + + def to_dict(self) -> dict: + result: dict = {} + result["envVars"] = from_dict(lambda x: to_class(SandboxMaskedEnvVar, x), self.env_vars) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SandboxConfigUserPolicyExperimental: @@ -26337,6 +29463,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: @@ -26480,6 +29641,28 @@ def to_dict(self) -> dict: result["errors"] = from_union([lambda x: from_list(from_str, x), from_none], self.errors) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionMCPSetConnectedIDEInfoParams: + """Records which IDE the host is connected to, or clears it.""" + + ide: SessionConnectedIDEInfo | None = None + """The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports + that it is disconnected. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionMCPSetConnectedIDEInfoParams': + assert isinstance(obj, dict) + ide = from_union([SessionConnectedIDEInfo.from_dict, from_none], obj.get("ide")) + return SessionMCPSetConnectedIDEInfoParams(ide) + + def to_dict(self) -> dict: + result: dict = {} + if self.ide is not None: + result["ide"] = from_union([lambda x: to_class(SessionConnectedIDEInfo, x), from_none], self.ide) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSError: @@ -26491,18 +29674,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. @@ -27045,6 +30236,52 @@ def to_dict(self) -> dict: result["status"] = from_union([lambda x: to_enum(SessionVisibilityStatus, x), from_none], self.status) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsCreateWorkspaceRequest: + """Identity, state location and starting context for a workspace record.""" + + convention: str + """`windows` (any letter case) selects Windows path rules. Any other value selects POSIX + path rules. + """ + session_id: str + """Session ID the workspace record belongs to""" + + session_state_path: str + """Directory the session's state is written under when no session filesystem provider is + configured. Ignored when a provider is configured; the provider's session state path is + used instead. + """ + context: SessionWorkingDirectoryContextWithClient | None = None + """Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, + `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, + `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + """ + name: str | None = None + """User-supplied display name for the workspace""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsCreateWorkspaceRequest': + assert isinstance(obj, dict) + convention = from_str(obj.get("convention")) + session_id = from_str(obj.get("sessionId")) + session_state_path = from_str(obj.get("sessionStatePath")) + context = from_union([SessionWorkingDirectoryContextWithClient.from_dict, from_none], obj.get("context")) + name = from_union([from_str, from_none], obj.get("name")) + return SessionsCreateWorkspaceRequest(convention, session_id, session_state_path, context, name) + + def to_dict(self) -> dict: + result: dict = {} + result["convention"] = from_str(self.convention) + result["sessionId"] = from_str(self.session_id) + result["sessionStatePath"] = from_str(self.session_state_path) + if self.context is not None: + result["context"] = from_union([lambda x: to_class(SessionWorkingDirectoryContextWithClient, x), from_none], self.context) + if self.name is not None: + 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 SessionsClientMetadataEntry: @@ -27261,6 +30498,239 @@ def to_dict(self) -> dict: result["userInvocable"] = from_union([from_bool, from_none], self.user_invocable) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CatalogClientContract: + """The protocol version and capability set a caller requires, supplied on every catalog + request so negotiation cannot be skipped by omission. + + Protocol version and capabilities the caller requires. + + Capabilities required by the original prepared operation. + + Required authenticated bound installation capabilities. + + Original installation wire capability; new-work authentication is not reacquired. + + Required authenticated bound installation contract. + + Required bound catalogue and confirmed remote installation capabilities. + + Required authenticated bound catalogue and Skill installation capabilities. + + Required authenticated bound Skill installation capability. + """ + protocol_version: int + """SDK protocol version the caller was generated against. A caller below the runtime's + minimum supported version is refused rather than served a partial result. + """ + required_capabilities: list[str] + """Wire features the caller requires the runtime to understand. Identifiers are bounded but + extensible so a newer caller can negotiate with an older runtime. Requiring an unknown + feature yields a typed refusal listing what is understood, never a partial grant. A grant + does not promise that a deployment has enabled the operation; typed unavailable results + report that separately. + """ + + @staticmethod + def from_dict(obj: Any) -> 'CatalogClientContract': + assert isinstance(obj, dict) + protocol_version = from_int(obj.get("protocolVersion")) + required_capabilities = from_list(from_str, obj.get("requiredCapabilities")) + return CatalogClientContract(protocol_version, required_capabilities) + + def to_dict(self) -> dict: + result: dict = {} + result["protocolVersion"] = from_int(self.protocol_version) + result["requiredCapabilities"] = from_list(from_str, self.required_capabilities) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationFileReview: + """One reviewed Skill file.""" + + digest: str + """SHA-256 digest of the exact file bytes.""" + + executable: bool + """Whether the file is installed with executable permissions.""" + + media_type: str + """Declared media type for the file.""" + + path: str + """Relative file path within the Skill root.""" + + size_bytes: int + """Exact reviewed file size in bytes.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationFileReview': + assert isinstance(obj, dict) + digest = from_str(obj.get("digest")) + executable = from_bool(obj.get("executable")) + media_type = from_str(obj.get("mediaType")) + path = from_str(obj.get("path")) + size_bytes = from_int(obj.get("sizeBytes")) + return SkillInstallationFileReview(digest, executable, media_type, path, size_bytes) + + def to_dict(self) -> dict: + result: dict = {} + result["digest"] = from_str(self.digest) + result["executable"] = from_bool(self.executable) + result["mediaType"] = from_str(self.media_type) + result["path"] = from_str(self.path) + result["sizeBytes"] = from_int(self.size_bytes) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class SkillInstallationOwnershipState(Enum): + """Ownership state observed from files and receipts. + + Owned Skill state observed from files and receipts. + """ + INTACT = "intact" + MODIFIED = "modified" + RECOVERY_REQUIRED = "recovery-required" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationLocation: + """Exact user-scope target location without an absolute host path. + + A user-facing personal Skill installation location without absolute host paths. + + User-facing installation location without an absolute host path. + """ + display_label: str + """Safe display label, for example ~/.copilot/skills/run-checks.""" + + relative_path: str + """Path relative to the Copilot home.""" + + scope: SkillInstallationScope + """Installation scope. Agent Finder Skills are installed in the user's personal Copilot home.""" + + diagnostics_absolute_path: str | None = None + """Diagnostics-only absolute host path. Hosts must not display it by default.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationLocation': + assert isinstance(obj, dict) + display_label = from_str(obj.get("displayLabel")) + relative_path = from_str(obj.get("relativePath")) + scope = SkillInstallationScope(obj.get("scope")) + diagnostics_absolute_path = from_union([from_str, from_none], obj.get("diagnosticsAbsolutePath")) + return SkillInstallationLocation(display_label, relative_path, scope, diagnostics_absolute_path) + + def to_dict(self) -> dict: + result: dict = {} + result["displayLabel"] = from_str(self.display_label) + result["relativePath"] = from_str(self.relative_path) + result["scope"] = to_enum(SkillInstallationScope, self.scope) + if self.diagnostics_absolute_path is not None: + result["diagnosticsAbsolutePath"] = from_union([from_str, from_none], self.diagnostics_absolute_path) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPlanTarget: + """Exact reviewed user-scope destination. + + Where a plan would be written. + + Exact destination, checked for intervening changes before mutation. + + Configuration scope and key the plan would write to. + + Exact user-scope target location without an absolute host path. + + A user-facing personal Skill installation location without absolute host paths. + + User-facing installation location without an absolute host path. + """ + scope: NScope + """Configuration scope the plan targets. + + Installation scope. Agent Finder Skills are installed in the user's personal Copilot home. + """ + config_key: str | None = None + """Configuration key the server would be recorded under within that scope.""" + + diagnostics_absolute_path: str | None = None + """Diagnostics-only absolute host path. Hosts must not display it by default.""" + + display_label: str | None = None + """Safe display label, for example ~/.copilot/skills/run-checks.""" + + relative_path: str | None = None + """Path relative to the Copilot home.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPlanTarget': + assert isinstance(obj, dict) + scope = NScope(obj.get("scope")) + config_key = from_union([from_str, from_none], obj.get("configKey")) + diagnostics_absolute_path = from_union([from_str, from_none], obj.get("diagnosticsAbsolutePath")) + display_label = from_union([from_str, from_none], obj.get("displayLabel")) + relative_path = from_union([from_str, from_none], obj.get("relativePath")) + return MCPPlanTarget(scope, config_key, diagnostics_absolute_path, display_label, relative_path) + + def to_dict(self) -> dict: + result: dict = {} + result["scope"] = to_enum(NScope, self.scope) + if self.config_key is not None: + result["configKey"] = from_union([from_str, from_none], self.config_key) + if self.diagnostics_absolute_path is not None: + result["diagnosticsAbsolutePath"] = from_union([from_str, from_none], self.diagnostics_absolute_path) + if self.display_label is not None: + result["displayLabel"] = from_union([from_str, from_none], self.display_label) + if self.relative_path is not None: + result["relativePath"] = from_union([from_str, from_none], self.relative_path) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class SkillInstallationFailureReason(Enum): + """Bounded refusal categories for verified Skill installation management. + + Cause of the compensation. + + Cause of the fully aborted or compensated operation. + + Bounded refusal reason. + """ + ALREADY_INSTALLED = "already-installed" + BUSY = "busy" + CANCELLED = "cancelled" + CONFIGURATION_MODIFIED = "configuration-modified" + CONFIRMATION_INVALID = "confirmation-invalid" + CONFIRMATION_UNAVAILABLE = "confirmation-unavailable" + DESCRIPTOR_INVALID = "descriptor-invalid" + DESCRIPTOR_UNAVAILABLE = "descriptor-unavailable" + ENTRYPOINT_UNAVAILABLE = "entrypoint-unavailable" + EXPIRED = "expired" + FEATURE_DISABLED = "feature-disabled" + FOREIGN_RUNTIME = "foreign-runtime" + INVALID_CANDIDATE = "invalid-candidate" + INVALID_REQUEST = "invalid-request" + INVALID_SKILL = "invalid-skill" + LIFECYCLE_UNAVAILABLE = "lifecycle-unavailable" + OPERATION_LIMIT = "operation-limit" + PAYLOAD_MISMATCH = "payload-mismatch" + PAYLOAD_UNAVAILABLE = "payload-unavailable" + PLAN_EXPIRED = "plan-expired" + PLAN_REPLAYED = "plan-replayed" + POLICY_CHANGED = "policy-changed" + POLICY_CONTEXT_UNAVAILABLE = "policy-context-unavailable" + RECOVERY_REQUIRED = "recovery-required" + RESOURCE_NOT_FOUND = "resource-not-found" + REVIEW_TOO_LARGE = "review-too-large" + SEARCH_MISMATCH = "search-mismatch" + SOURCE_CHANGED = "source-changed" + WRITE_FAILED = "write-failed" + WRONG_KIND = "wrong-kind" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SkillList: @@ -28553,9 +32023,8 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class UserSettingsGetResult: - """Per-key metadata for every known user setting (settings.json overlaid with the legacy - config.json, config.json wins), including settings left at their default. Excludes - repository- and enterprise-managed overrides. + """Per-key metadata for every known user setting in settings.json, including settings left + at their default. Excludes repository- and enterprise-managed overrides. """ settings: dict[str, UserSettingMetadata] """Every known user setting keyed by setting name, each with its effective value, default, @@ -28607,82 +32076,212 @@ def to_dict(self) -> dict: result["workflowRunId"] = from_str(self.workflow_run_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class WorkflowRunFailure: - """Machine-readable terminal failure. +class PauseInfoClass: + type: PauseInfoType + """Workflow pause initiator discriminator.""" - Machine-readable workflow run failure. + key: str | None = None + """Stable author-defined checkpoint key that initiated the pause.""" - Machine-readable failure details for a halted or errored run. + @staticmethod + def from_dict(obj: Any) -> 'PauseInfoClass': + assert isinstance(obj, dict) + type = PauseInfoType(obj.get("type")) + key = from_union([from_str, from_none], obj.get("key")) + return PauseInfoClass(type, key) - The run stopped because its usage accounting could not be completed. + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(PauseInfoType, self.type) + if self.key is not None: + result["key"] = from_union([from_str, from_none], self.key) + return result - 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. - """ - run_id: str - """Workflow run identifier. +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class WorkflowPauseInfo: + """Durable metadata describing who initiated a workflow pause. - Workflow run identifier whose changed limits were declined. + Structured pause initiator metadata for a paused attempt. """ - type: WorkflowRunFailureType - """Workflow failure variant discriminator.""" + type: PauseInfoType + """Workflow pause initiator discriminator.""" - kind: RunFailureKind | None = None - """Resource ceiling that stopped the run.""" + key: str | None = None + """Stable author-defined checkpoint key that initiated the pause.""" - suggested_value: float | None = None - """Suggested larger ceiling when the runtime can derive one safely.""" + @staticmethod + def from_dict(obj: Any) -> 'WorkflowPauseInfo': + assert isinstance(obj, dict) + type = PauseInfoType(obj.get("type")) + key = from_union([from_str, from_none], obj.get("key")) + return WorkflowPauseInfo(type, key) - value: float | None = None - """Approved effective ceiling that was reached.""" + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(PauseInfoType, self.type) + if self.key is not None: + result["key"] = from_union([from_str, from_none], self.key) + return result - reason: str | None = None - """Human-readable reason the resume did not proceed.""" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class WorkflowLogLine: + """One ordered workflow progress line.""" - code: str | None = None - """Stable failure code.""" + kind: WorkflowLogLineKind + """Progress line kind.""" - operation: DurableOperation | None = None - """Execution-critical durable operation that failed.""" + seq: int + """Monotonic sequence number within the workflow run.""" - drained_nano_aiu: int | None = None - """Confirmed usage in nano-AIU, representing the floor of what the run spent.""" + text: str + """Progress text.""" @staticmethod - def from_dict(obj: Any) -> 'WorkflowRunFailure': + def from_dict(obj: Any) -> 'WorkflowLogLine': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - type = WorkflowRunFailureType(obj.get("type")) - kind = from_union([RunFailureKind, from_none], obj.get("kind")) - suggested_value = from_union([from_float, from_none], obj.get("suggestedValue")) - value = from_union([from_float, from_none], obj.get("value")) - reason = from_union([from_str, from_none], obj.get("reason")) - code = from_union([from_str, from_none], obj.get("code")) - operation = from_union([DurableOperation, from_none], obj.get("operation")) - drained_nano_aiu = from_union([from_int, from_none], obj.get("drainedNanoAiu")) - return WorkflowRunFailure(run_id, type, kind, suggested_value, value, reason, code, operation, drained_nano_aiu) + kind = WorkflowLogLineKind(obj.get("kind")) + seq = from_int(obj.get("seq")) + text = from_str(obj.get("text")) + return WorkflowLogLine(kind, seq, text) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) - result["type"] = to_enum(WorkflowRunFailureType, self.type) - if self.kind is not None: - result["kind"] = from_union([lambda x: to_enum(RunFailureKind, x), from_none], self.kind) - if self.suggested_value is not None: - result["suggestedValue"] = from_union([to_float, from_none], self.suggested_value) - if self.value is not None: - result["value"] = from_union([to_float, from_none], self.value) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.code is not None: - result["code"] = from_union([from_str, from_none], self.code) - if self.operation is not None: - result["operation"] = from_union([lambda x: to_enum(DurableOperation, x), from_none], self.operation) - if self.drained_nano_aiu is not None: - result["drainedNanoAiu"] = from_union([from_int, from_none], self.drained_nano_aiu) + result["kind"] = to_enum(WorkflowLogLineKind, self.kind) + result["seq"] = from_int(self.seq) + result["text"] = from_str(self.text) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class WorkflowProgressLine: + """One durable workflow progress record.""" + + attempt: int + """Resume attempt that emitted this record.""" + + kind: WorkflowLogLineKind + """Progress record kind.""" + + recorded_at: int + """Epoch milliseconds when the record was persisted.""" + + seq: int + """Global monotonic sequence number within the run.""" + + text: str + """Prompt-safe progress text.""" + + phase_id: str | None = None + """Phase active when the record was emitted, or null before any phase.""" + + @staticmethod + def from_dict(obj: Any) -> 'WorkflowProgressLine': + assert isinstance(obj, dict) + attempt = from_int(obj.get("attempt")) + kind = WorkflowLogLineKind(obj.get("kind")) + recorded_at = from_int(obj.get("recordedAt")) + seq = from_int(obj.get("seq")) + text = from_str(obj.get("text")) + phase_id = from_union([from_none, from_str], obj.get("phaseId")) + return WorkflowProgressLine(attempt, kind, recorded_at, seq, text, phase_id) + + def to_dict(self) -> dict: + result: dict = {} + result["attempt"] = from_int(self.attempt) + result["kind"] = to_enum(WorkflowLogLineKind, self.kind) + result["recordedAt"] = from_int(self.recorded_at) + result["seq"] = from_int(self.seq) + result["text"] = from_str(self.text) + result["phaseId"] = from_union([from_none, from_str], self.phase_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class WorkflowPhaseObservation: + """Durable lifecycle and timing for one workflow phase.""" + + accumulated_active_ms: int + """Completed active time accumulated by this phase in milliseconds.""" + + current_active_ms: int + """Current live active time for this phase in milliseconds.""" + + entry_count: int + """Number of times execution entered this phase.""" + + id: str + """Phase identifier.""" + + last_entered_run_attempt: int + """Most recent run attempt that entered this phase, or `0` if the phase has never been + entered. + """ + live_agent_count: int + """Direct agents in this phase that are currently live.""" + + status: WorkflowPhaseStatus + """Derived lifecycle state of the phase.""" + + title: str + """Human-readable phase title.""" + + total_agent_count: int + """Total direct agents associated with this phase.""" + + completed_at: int | None = None + """Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip + timestamp (equal to `startedAt`). + """ + detail: str | None = None + """Optional human-readable phase detail.""" + + ordinal: int | None = None + """Zero-based declared phase ordinal, or null for an undeclared phase.""" + + started_at: int | None = None + """Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip + timestamp (equal to `completedAt`). + """ + + @staticmethod + def from_dict(obj: Any) -> 'WorkflowPhaseObservation': + assert isinstance(obj, dict) + accumulated_active_ms = from_int(obj.get("accumulatedActiveMs")) + current_active_ms = from_int(obj.get("currentActiveMs")) + entry_count = from_int(obj.get("entryCount")) + id = from_str(obj.get("id")) + last_entered_run_attempt = from_int(obj.get("lastEnteredRunAttempt")) + live_agent_count = from_int(obj.get("liveAgentCount")) + status = WorkflowPhaseStatus(obj.get("status")) + title = from_str(obj.get("title")) + total_agent_count = from_int(obj.get("totalAgentCount")) + completed_at = from_union([from_int, from_none], obj.get("completedAt")) + detail = from_union([from_str, from_none], obj.get("detail")) + ordinal = from_union([from_none, from_int], obj.get("ordinal")) + started_at = from_union([from_int, from_none], obj.get("startedAt")) + return WorkflowPhaseObservation(accumulated_active_ms, current_active_ms, entry_count, id, last_entered_run_attempt, live_agent_count, status, title, total_agent_count, completed_at, detail, ordinal, started_at) + + def to_dict(self) -> dict: + result: dict = {} + result["accumulatedActiveMs"] = from_int(self.accumulated_active_ms) + result["currentActiveMs"] = from_int(self.current_active_ms) + result["entryCount"] = from_int(self.entry_count) + result["id"] = from_str(self.id) + result["lastEnteredRunAttempt"] = from_int(self.last_entered_run_attempt) + result["liveAgentCount"] = from_int(self.live_agent_count) + result["status"] = to_enum(WorkflowPhaseStatus, self.status) + result["title"] = from_str(self.title) + result["totalAgentCount"] = from_int(self.total_agent_count) + if self.completed_at is not None: + result["completedAt"] = from_union([from_int, from_none], self.completed_at) + if self.detail is not None: + result["detail"] = from_union([from_str, from_none], self.detail) + result["ordinal"] = from_union([from_none, from_int], self.ordinal) + if self.started_at is not None: + result["startedAt"] = from_union([from_int, from_none], self.started_at) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -28883,6 +32482,113 @@ 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 AuthEnumerateValue: + """The enumerated collection, keyed by the same selector as the query.""" + + items: list[ItemElement] + """The signed-in account forest; empty when not logged in. + + The providers offered for interactive login. + """ + kind: AuthEnumerateQueryKind + """Enumerated account-collection variant discriminator.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthEnumerateValue': + assert isinstance(obj, dict) + items = from_list(ItemElement.from_dict, obj.get("items")) + kind = AuthEnumerateQueryKind(obj.get("kind")) + return AuthEnumerateValue(items, kind) + + def to_dict(self) -> dict: + result: dict = {} + result["items"] = from_list(lambda x: to_class(ItemElement, x), self.items) + result["kind"] = to_enum(AuthEnumerateQueryKind, self.kind) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AccountsEnumerateRequest: + """Enumerate request carrying the typed collection query.""" + + query: AuthEnumerateQuery + """Which typed accounts collection to enumerate.""" + + @staticmethod + def from_dict(obj: Any) -> 'AccountsEnumerateRequest': + assert isinstance(obj, dict) + query = AuthEnumerateQuery.from_dict(obj.get("query")) + return AccountsEnumerateRequest(query) + + def to_dict(self) -> dict: + result: dict = {} + result["query"] = to_class(AuthEnumerateQuery, self.query) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AccountsGetRequest: + """Read request carrying the typed datum query.""" + + query: AuthReadQuery + """Which typed accounts datum to read.""" + + @staticmethod + def from_dict(obj: Any) -> 'AccountsGetRequest': + assert isinstance(obj, dict) + query = AuthReadQuery.from_dict(obj.get("query")) + return AccountsGetRequest(query) + + def to_dict(self) -> dict: + result: dict = {} + result["query"] = to_class(AuthReadQuery, self.query) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AccountsSetRequest: + """Mutation request carrying the typed write command.""" + + command: AuthWrite + """The non-interactive mutation command to apply.""" + + @staticmethod + def from_dict(obj: Any) -> 'AccountsSetRequest': + assert isinstance(obj, dict) + command = AuthWrite.from_dict(obj.get("command")) + return AccountsSetRequest(command) + + def to_dict(self) -> dict: + result: dict = {} + result["command"] = to_class(AuthWrite, self.command) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AgentDiscoveryPathList: @@ -28934,50 +32640,57 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AutopilotObjectiveGetStateResult: - """Canonical runtime state for the session's current autopilot objective.""" +class ModelProviderDescriptor: + """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. + """ + id: str + """Opaque, stable provider id, stamped onto every model this provider returns.""" - state: AutopilotObjectiveState | None = None - """Current objective state, or `null` when the session has no objective.""" + kind: ModelProviderKind + """The neutral provider kind.""" + + label: str + """Human-readable menu label, owned by the runtime so every consumer renders identical text.""" + + ordering: int + """Stable ordering key for presenting providers in a deterministic sequence.""" @staticmethod - def from_dict(obj: Any) -> 'AutopilotObjectiveGetStateResult': + def from_dict(obj: Any) -> 'ModelProviderDescriptor': assert isinstance(obj, dict) - state = from_union([AutopilotObjectiveState.from_dict, from_none], obj.get("state")) - return AutopilotObjectiveGetStateResult(state) + id = from_str(obj.get("id")) + kind = ModelProviderKind(obj.get("kind")) + label = from_str(obj.get("label")) + ordering = from_int(obj.get("ordering")) + return ModelProviderDescriptor(id, kind, label, ordering) def to_dict(self) -> dict: result: dict = {} - result["state"] = from_union([lambda x: to_class(AutopilotObjectiveState, x), from_none], self.state) + result["id"] = from_str(self.id) + result["kind"] = to_enum(ModelProviderKind, self.kind) + result["label"] = from_str(self.label) + result["ordering"] = from_int(self.ordering) return result # Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class _FactoryToolRunOptions: - """Options for an internal tool-originated factory invocation. - - Tool-originated factory invocation options. - """ - limits: FactoryRunLimits | None = None - """Per-invocation resource ceiling overrides.""" +class AutopilotObjectiveGetStateResult: + """Canonical runtime state for the session's current autopilot objective.""" - resume_from_run_id: str | None = None - """Run identifier whose journal and progress should seed this resumed run.""" + state: AutopilotObjectiveState | None = None + """Current objective state, or `null` when the session has no objective.""" @staticmethod - def from_dict(obj: Any) -> '_FactoryToolRunOptions': + def from_dict(obj: Any) -> 'AutopilotObjectiveGetStateResult': assert isinstance(obj, dict) - limits = from_union([FactoryRunLimits.from_dict, from_none], obj.get("limits")) - resume_from_run_id = from_union([from_str, from_none], obj.get("resumeFromRunId")) - return _FactoryToolRunOptions(limits, resume_from_run_id) + state = from_union([AutopilotObjectiveState.from_dict, from_none], obj.get("state")) + return AutopilotObjectiveGetStateResult(state) def to_dict(self) -> dict: result: dict = {} - if self.limits is not None: - result["limits"] = from_union([lambda x: to_class(FactoryRunLimits, x), from_none], self.limits) - if self.resume_from_run_id is not None: - result["resumeFromRunId"] = from_union([from_str, from_none], self.resume_from_run_id) + result["state"] = from_union([lambda x: to_class(AutopilotObjectiveState, x), from_none], self.state) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -29383,11 +33096,15 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPPlanProvenance: - """Origin and semantic digest of the exact validated JSON MCP card content bound to this - plan. + """Original source identity and content commitment. Provenance of the exact validated JSON MCP card content bound privately to a completed plan and its opaque handle. + + Source identity and content commitment retained by the installed receipt. + + Origin and semantic digest of the exact validated JSON MCP card content bound to this + plan. """ authority: str """Authority associated with the validated card, without path, query, or credentials. Inert @@ -29792,6 +33509,45 @@ def to_dict(self) -> dict: result["revision"] = from_int(self.revision) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginResultDto: + """Login result. When status is needs-plaintext-consent or needs-account-selection, advance + with the user's decision to continue. + + Result of an interactive login flow. Pending consent or account selection is not terminal. + """ + 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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginResultDto': + assert isinstance(obj, dict) + 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["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. @dataclass class CanvasHostContext: @@ -29834,8 +33590,9 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CatalogAISkillCandidate: - """An inert AI skill catalog result. AI skills are discovery-only and cannot be represented - as installable through this surface. + """An inert AI skill catalog result. Verified Skill candidates may be installable only when + the runtime reports installability and the selected session is permitted to plan + installation. """ display_name: str """Display name taken verbatim from the card. Inert untrusted text.""" @@ -29849,8 +33606,9 @@ class CatalogAISkillCandidate: """ISO 8601 timestamp after which the handle is stale and will be rejected.""" installability: CatalogAISkillInstallability - """AI skills are discovery-only and cannot be installed through this surface""" - + """Whether this AI skill candidate can be planned for verified installation in the selected + session. + """ kind: CatalogAISkillCandidateKind """Discriminator: this candidate describes an AI skill""" @@ -29928,7 +33686,7 @@ class CatalogMCPServerCandidate: handle_expires_at: str """ISO 8601 timestamp after which the handle is stale and will be rejected.""" - installability: CatalogMCPServerInstallabilityEnum + installability: CatalogMCPServerInstallability """Whether this MCP server can be planned for installation, and if policy prevents it.""" kind: CatalogMCPServerCandidateKind @@ -29962,7 +33720,7 @@ def from_dict(obj: Any) -> 'CatalogMCPServerCandidate': display_name = from_str(obj.get("displayName")) handle = from_str(obj.get("handle")) handle_expires_at = from_str(obj.get("handleExpiresAt")) - installability = CatalogMCPServerInstallabilityEnum(obj.get("installability")) + installability = CatalogMCPServerInstallability(obj.get("installability")) kind = CatalogMCPServerCandidateKind(obj.get("kind")) media_type = MCPServerCardMediaType(obj.get("mediaType")) provenance = CatalogMCPServerCandidateProvenance.from_dict(obj.get("provenance")) @@ -29977,7 +33735,7 @@ def to_dict(self) -> dict: result["displayName"] = from_str(self.display_name) result["handle"] = from_str(self.handle) result["handleExpiresAt"] = from_str(self.handle_expires_at) - result["installability"] = to_enum(CatalogMCPServerInstallabilityEnum, self.installability) + result["installability"] = to_enum(CatalogMCPServerInstallability, self.installability) result["kind"] = to_enum(CatalogMCPServerCandidateKind, self.kind) result["mediaType"] = to_enum(MCPServerCardMediaType, self.media_type) result["provenance"] = to_class(CatalogMCPServerCandidateProvenance, self.provenance) @@ -29990,6 +33748,156 @@ 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 ConnectorDiscoveryAccount: + """Eligible account.""" + + account_id: str + """Opaque account ID.""" + + auth_info: ConnectorDiscoveryAuthInfo + """Account metadata.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDiscoveryAccount': + assert isinstance(obj, dict) + account_id = from_str(obj.get("accountId")) + auth_info = ConnectorDiscoveryAuthInfo.from_dict(obj.get("authInfo")) + return ConnectorDiscoveryAccount(account_id, auth_info) + + def to_dict(self) -> dict: + result: dict = {} + result["accountId"] = from_str(self.account_id) + result["authInfo"] = to_class(ConnectorDiscoveryAuthInfo, self.auth_info) + 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. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _GitHubRepositoryAtPathResult: + """The GitHub repository that owns the requested path, when the selected remote (`origin`, + else the first) is on a GitHub host. + """ + repository: GitHubRepositoryIdentity | None = None + """Resolved repository identity, or null when the selected remote resolves to no GitHub host.""" + + @staticmethod + def from_dict(obj: Any) -> '_GitHubRepositoryAtPathResult': + assert isinstance(obj, dict) + repository = from_union([GitHubRepositoryIdentity.from_dict, from_none], obj.get("repository")) + return _GitHubRepositoryAtPathResult(repository) + + def to_dict(self) -> dict: + result: dict = {} + if self.repository is not None: + result["repository"] = from_union([lambda x: to_class(GitHubRepositoryIdentity, x), from_none], self.repository) + 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: @@ -30171,6 +34079,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: @@ -30594,318 +34560,228 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryRunTerminal: - """Prompt-safe terminal factory outcome.""" - - error: str | None = None - """Human-readable terminal error.""" - - failure: FactoryRunFailure | None = None - """Machine-readable terminal failure.""" - - pause_info: PauseInfoClass | None = None - """Pause initiator metadata, or null when the run did not pause.""" - - reason: str | None = None - """Human-readable terminal reason.""" - - result_preview: str | None = None - """Prompt-safe preview of the completed result.""" - - @staticmethod - def from_dict(obj: Any) -> 'FactoryRunTerminal': - assert isinstance(obj, dict) - error = from_union([from_str, from_none], obj.get("error")) - failure = from_union([FactoryRunFailure.from_dict, from_none], obj.get("failure")) - pause_info = from_union([PauseInfoClass.from_dict, from_none], obj.get("pauseInfo")) - reason = from_union([from_str, from_none], obj.get("reason")) - result_preview = from_union([from_str, from_none], obj.get("resultPreview")) - return FactoryRunTerminal(error, failure, pause_info, reason, result_preview) - - def to_dict(self) -> dict: - result: dict = {} - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.failure is not None: - result["failure"] = from_union([lambda x: to_class(FactoryRunFailure, x), from_none], self.failure) - result["pauseInfo"] = from_union([lambda x: to_class(PauseInfoClass, x), from_none], self.pause_info) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.result_preview is not None: - result["resultPreview"] = from_union([from_str, from_none], self.result_preview) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryRunResult: - """Terminal resumed run envelope. - - Complete current or terminal factory run envelope. +class MCPPrompt: + """An MCP prompt descriptor. Server-provided non-standard fields are exposed under + `additionalProperties`. """ - run_id: str - """Factory run identifier.""" - - status: RunStatus - """Current or terminal factory run status.""" + name: str + """The programmatic name of the prompt""" - attempt: int | None = None - """One-based execution attempt represented by this envelope. Absent before the first attempt - starts or when returned by an older runtime. - """ - error: str | None = None - """Error message for an errored run.""" + meta: dict[str, Any] | None = None + """Prompt-level metadata""" - failure: FactoryRunFailure | None = None - """Machine-readable failure details for a halted or errored run.""" + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard descriptor fields""" - pause_info: FactoryPauseInfo | None = None - """Structured pause initiator metadata for a paused attempt.""" + arguments: list[MCPPromptArgument] | None = None + """Arguments accepted by the prompt""" - reason: str | None = None - """Reason for a halted or cancelled run.""" + description: str | None = None + """Description of what this prompt provides""" - result: Any = None - """Completed factory result.""" + icons: list[MCPPromptIcon] | None = None + """Icons associated with this prompt""" - snapshot: Any = None - """Partial journal and progress snapshot for a halted, cancelled, or errored run.""" + title: str | None = None + """Human-readable display title""" @staticmethod - def from_dict(obj: Any) -> 'FactoryRunResult': + def from_dict(obj: Any) -> 'MCPPrompt': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - status = RunStatus(obj.get("status")) - attempt = from_union([from_int, from_none], obj.get("attempt")) - error = from_union([from_str, from_none], obj.get("error")) - failure = from_union([FactoryRunFailure.from_dict, from_none], obj.get("failure")) - pause_info = from_union([FactoryPauseInfo.from_dict, from_none], obj.get("pauseInfo")) - reason = from_union([from_str, from_none], obj.get("reason")) - result = obj.get("result") - snapshot = obj.get("snapshot") - return FactoryRunResult(run_id, status, attempt, error, failure, pause_info, reason, result, snapshot) + 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["runId"] = from_str(self.run_id) - result["status"] = to_enum(RunStatus, self.status) - if self.attempt is not None: - result["attempt"] = from_union([from_int, from_none], self.attempt) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.failure is not None: - result["failure"] = from_union([lambda x: to_class(FactoryRunFailure, x), from_none], self.failure) - if self.pause_info is not None: - result["pauseInfo"] = from_union([lambda x: to_class(FactoryPauseInfo, x), from_none], self.pause_info) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.result is not None: - result["result"] = self.result - if self.snapshot is not None: - result["snapshot"] = self.snapshot + 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 FactoryLogRequest: - """Parameters for recording factory progress.""" - - execution_token: str - """Opaque token identifying the current factory execution attempt.""" +class MCPOauthHandlePendingRequest: + """Pending MCP OAuth request ID and host-provided token or cancellation response.""" - lines: list[FactoryLogLine] - """Ordered progress lines to append.""" + request_id: str + """OAuth request identifier from the mcp.oauth_required event""" - run_id: str - """Factory run identifier.""" + result: MCPOauthPendingRequestResponse + """Host response to the pending OAuth request.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryLogRequest': + def from_dict(obj: Any) -> 'MCPOauthHandlePendingRequest': assert isinstance(obj, dict) - execution_token = from_str(obj.get("executionToken")) - lines = from_list(FactoryLogLine.from_dict, obj.get("lines")) - run_id = from_str(obj.get("runId")) - return FactoryLogRequest(execution_token, lines, run_id) + request_id = from_str(obj.get("requestId")) + result = MCPOauthPendingRequestResponse.from_dict(obj.get("result")) + return MCPOauthHandlePendingRequest(request_id, result) def to_dict(self) -> dict: result: dict = {} - result["executionToken"] = from_str(self.execution_token) - result["lines"] = from_list(lambda x: to_class(FactoryLogLine, x), self.lines) - result["runId"] = from_str(self.run_id) + result["requestId"] = from_str(self.request_id) + result["result"] = to_class(MCPOauthPendingRequestResponse, self.result) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryProgressPage: - """A bidirectional page of factory progress. - - Bidirectional page of durable factory progress. +class InstalledPlugin: + """Installed plugin record from global state, with marketplace, version, install time, + enabled state, cache path, and source. """ - has_more_newer: bool - """Whether progress records newer than this page exist.""" - - has_more_older: bool - """Whether progress records older than this page exist.""" - - records: list[FactoryProgressLine] - """Progress records in sequence order.""" - - revision: int - """Run revision reflected by this page.""" - - newest_seq: int | None = None - """Newest sequence number in this page, or null when empty.""" - - oldest_seq: int | None = None - """Oldest sequence number in this page, or null when empty.""" + enabled: bool + """Whether the plugin is currently enabled""" - @staticmethod - def from_dict(obj: Any) -> 'FactoryProgressPage': - assert isinstance(obj, dict) - has_more_newer = from_bool(obj.get("hasMoreNewer")) - has_more_older = from_bool(obj.get("hasMoreOlder")) - records = from_list(FactoryProgressLine.from_dict, obj.get("records")) - revision = from_int(obj.get("revision")) - newest_seq = from_union([from_int, from_none], obj.get("newestSeq")) - oldest_seq = from_union([from_int, from_none], obj.get("oldestSeq")) - return FactoryProgressPage(has_more_newer, has_more_older, records, revision, newest_seq, oldest_seq) + installed_at: str + """Installation timestamp""" - def to_dict(self) -> dict: - result: dict = {} - result["hasMoreNewer"] = from_bool(self.has_more_newer) - result["hasMoreOlder"] = from_bool(self.has_more_older) - result["records"] = from_list(lambda x: to_class(FactoryProgressLine, x), self.records) - result["revision"] = from_int(self.revision) - result["newestSeq"] = from_union([from_int, from_none], self.newest_seq) - result["oldestSeq"] = from_union([from_int, from_none], self.oldest_seq) - return result + marketplace: str + """Marketplace the plugin came from (empty string for direct repo installs)""" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class WorkflowLogRequest: - """Parameters for recording workflow progress.""" + name: str + """Plugin name""" - execution_token: str - """Opaque token identifying the current workflow execution attempt.""" + cache_path: str | None = None + """Path where the plugin is cached locally""" - lines: list[WorkflowLogLine] - """Ordered progress lines to append.""" + installed_from: str | None = None + """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. + """ + source: InstalledPluginSource | str | None = None + """Source for direct repo installs (when marketplace is empty)""" - run_id: str - """Workflow run identifier.""" + source_sha: str | None = None + """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. + """ + version: str | None = None + """Version installed (if available)""" @staticmethod - def from_dict(obj: Any) -> 'WorkflowLogRequest': + def from_dict(obj: Any) -> 'InstalledPlugin': assert isinstance(obj, dict) - execution_token = from_str(obj.get("executionToken")) - lines = from_list(WorkflowLogLine.from_dict, obj.get("lines")) - run_id = from_str(obj.get("runId")) - return WorkflowLogRequest(execution_token, lines, run_id) + enabled = from_bool(obj.get("enabled")) + installed_at = from_str(obj.get("installed_at")) + marketplace = from_str(obj.get("marketplace")) + name = from_str(obj.get("name")) + cache_path = from_union([from_str, from_none], obj.get("cache_path")) + installed_from = from_union([from_str, from_none], obj.get("installed_from")) + source = from_union([InstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) + source_sha = from_union([from_str, from_none], obj.get("source_sha")) + version = from_union([from_str, from_none], obj.get("version")) + return InstalledPlugin(enabled, installed_at, marketplace, name, cache_path, installed_from, source, source_sha, version) def to_dict(self) -> dict: result: dict = {} - result["executionToken"] = from_str(self.execution_token) - result["lines"] = from_list(lambda x: to_class(WorkflowLogLine, x), self.lines) - result["runId"] = from_str(self.run_id) + result["enabled"] = from_bool(self.enabled) + result["installed_at"] = from_str(self.installed_at) + result["marketplace"] = from_str(self.marketplace) + result["name"] = from_str(self.name) + if self.cache_path is not None: + result["cache_path"] = from_union([from_str, from_none], self.cache_path) + if self.installed_from is not None: + result["installed_from"] = from_union([from_str, from_none], self.installed_from) + if self.source is not None: + result["source"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str, from_none], self.source) + if self.source_sha is not None: + result["source_sha"] = from_union([from_str, from_none], self.source_sha) + 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 WorkflowProgressPage: - """A bidirectional page of workflow progress. - - Bidirectional page of durable workflow progress. +class SessionInstalledPlugin: + """Installed plugin record for a session, with marketplace, version, install time, enabled + state, cache path, and source. """ - has_more_newer: bool - """Whether progress records newer than this page exist.""" - - has_more_older: bool - """Whether progress records older than this page exist.""" - - records: list[WorkflowProgressLine] - """Progress records in sequence order.""" - - revision: int - """Run revision reflected by this page.""" - - newest_seq: int | None = None - """Newest sequence number in this page, or null when empty.""" - - oldest_seq: int | None = None - """Oldest sequence number in this page, or null when empty.""" + enabled: bool + """Whether the plugin is currently enabled""" - @staticmethod - def from_dict(obj: Any) -> 'WorkflowProgressPage': - assert isinstance(obj, dict) - has_more_newer = from_bool(obj.get("hasMoreNewer")) - has_more_older = from_bool(obj.get("hasMoreOlder")) - records = from_list(WorkflowProgressLine.from_dict, obj.get("records")) - revision = from_int(obj.get("revision")) - newest_seq = from_union([from_int, from_none], obj.get("newestSeq")) - oldest_seq = from_union([from_int, from_none], obj.get("oldestSeq")) - return WorkflowProgressPage(has_more_newer, has_more_older, records, revision, newest_seq, oldest_seq) + installed_at: str + """Installation timestamp (ISO-8601)""" - def to_dict(self) -> dict: - result: dict = {} - result["hasMoreNewer"] = from_bool(self.has_more_newer) - result["hasMoreOlder"] = from_bool(self.has_more_older) - result["records"] = from_list(lambda x: to_class(WorkflowProgressLine, x), self.records) - result["revision"] = from_int(self.revision) - result["newestSeq"] = from_union([from_int, from_none], self.newest_seq) - result["oldestSeq"] = from_union([from_int, from_none], self.oldest_seq) - return result + marketplace: str + """Marketplace the plugin came from (empty string for direct repo installs)""" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryRunRequest: - """Parameters for invoking a registered factory.""" + name: str + """Plugin name""" - args: Any - """Factory input value.""" + cache_path: str | None = None + """Path where the plugin is cached locally""" - name: str - """Registered factory name.""" + installed_from: str | None = None + """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. + """ + source: SessionInstalledPluginSource | str | None = None + """Source descriptor for direct repo installs (when marketplace is empty)""" - options: RunOptions | None = None - """Factory invocation options.""" + source_sha: str | None = None + """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. + """ + version: str | None = None + """Installed version, if known""" @staticmethod - def from_dict(obj: Any) -> 'FactoryRunRequest': + def from_dict(obj: Any) -> 'SessionInstalledPlugin': assert isinstance(obj, dict) - args = obj.get("args") + enabled = from_bool(obj.get("enabled")) + installed_at = from_str(obj.get("installed_at")) + marketplace = from_str(obj.get("marketplace")) name = from_str(obj.get("name")) - options = from_union([RunOptions.from_dict, from_none], obj.get("options")) - return FactoryRunRequest(args, name, options) + cache_path = from_union([from_str, from_none], obj.get("cache_path")) + installed_from = from_union([from_str, from_none], obj.get("installed_from")) + source = from_union([SessionInstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) + source_sha = from_union([from_str, from_none], obj.get("source_sha")) + version = from_union([from_str, from_none], obj.get("version")) + return SessionInstalledPlugin(enabled, installed_at, marketplace, name, cache_path, installed_from, source, source_sha, version) def to_dict(self) -> dict: result: dict = {} - result["args"] = self.args + result["enabled"] = from_bool(self.enabled) + result["installed_at"] = from_str(self.installed_at) + result["marketplace"] = from_str(self.marketplace) result["name"] = from_str(self.name) - if self.options is not None: - result["options"] = from_union([lambda x: to_class(RunOptions, x), from_none], self.options) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class MCPOauthHandlePendingRequest: - """Pending MCP OAuth request ID and host-provided token or cancellation response.""" - - request_id: str - """OAuth request identifier from the mcp.oauth_required event""" - - result: MCPOauthPendingRequestResponse - """Host response to the pending OAuth request.""" - - @staticmethod - def from_dict(obj: Any) -> 'MCPOauthHandlePendingRequest': - assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = MCPOauthPendingRequestResponse.from_dict(obj.get("result")) - return MCPOauthHandlePendingRequest(request_id, result) - - def to_dict(self) -> dict: - result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(MCPOauthPendingRequestResponse, self.result) + if self.cache_path is not None: + result["cache_path"] = from_union([from_str, from_none], self.cache_path) + if self.installed_from is not None: + result["installed_from"] = from_union([from_str, from_none], self.installed_from) + if self.source is not None: + result["source"] = from_union([lambda x: to_class(SessionInstalledPluginSource, x), from_str, from_none], self.source) + if self.source_sha is not None: + result["source_sha"] = from_union([from_str, from_none], self.source_sha) + 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. @@ -31007,148 +34883,118 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class InstalledPlugin: - """Installed plugin record from global state, with marketplace, version, install time, - enabled state, cache path, and source. - """ - enabled: bool - """Whether the plugin is currently enabled""" - - installed_at: str - """Installation timestamp""" +class MCPAppsReadResourceResult: + """Resource contents returned by the MCP server.""" - marketplace: str - """Marketplace the plugin came from (empty string for direct repo installs)""" + contents: list[MCPAppsResourceContent] + """Resource contents returned by the server""" - name: str - """Plugin name""" + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsReadResourceResult': + assert isinstance(obj, dict) + contents = from_list(MCPAppsResourceContent.from_dict, obj.get("contents")) + return MCPAppsReadResourceResult(contents) - cache_path: str | None = None - """Path where the plugin is cached locally""" + def to_dict(self) -> dict: + result: dict = {} + result["contents"] = from_list(lambda x: to_class(MCPAppsResourceContent, x), self.contents) + return result - installed_from: str | None = None - """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. - """ - source: InstalledPluginSource | str | None = None - """Source for direct repo installs (when marketplace is empty)""" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourcesReadResult: + """Resource contents returned by the MCP server.""" - source_sha: str | None = None - """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. - """ - version: str | None = None - """Version installed (if available)""" + contents: list[MCPResourceContent] + """Resource contents returned by the server""" @staticmethod - def from_dict(obj: Any) -> 'InstalledPlugin': + def from_dict(obj: Any) -> 'MCPResourcesReadResult': assert isinstance(obj, dict) - enabled = from_bool(obj.get("enabled")) - installed_at = from_str(obj.get("installed_at")) - marketplace = from_str(obj.get("marketplace")) - name = from_str(obj.get("name")) - cache_path = from_union([from_str, from_none], obj.get("cache_path")) - installed_from = from_union([from_str, from_none], obj.get("installed_from")) - source = from_union([InstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) - source_sha = from_union([from_str, from_none], obj.get("source_sha")) - version = from_union([from_str, from_none], obj.get("version")) - return InstalledPlugin(enabled, installed_at, marketplace, name, cache_path, installed_from, source, source_sha, version) + contents = from_list(MCPResourceContent.from_dict, obj.get("contents")) + return MCPResourcesReadResult(contents) def to_dict(self) -> dict: result: dict = {} - result["enabled"] = from_bool(self.enabled) - result["installed_at"] = from_str(self.installed_at) - result["marketplace"] = from_str(self.marketplace) - result["name"] = from_str(self.name) - if self.cache_path is not None: - result["cache_path"] = from_union([from_str, from_none], self.cache_path) - if self.installed_from is not None: - result["installed_from"] = from_union([from_str, from_none], self.installed_from) - if self.source is not None: - result["source"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str, from_none], self.source) - if self.source_sha is not None: - result["source_sha"] = from_union([from_str, from_none], self.source_sha) - if self.version is not None: - result["version"] = from_union([from_str, from_none], self.version) + result["contents"] = from_list(lambda x: to_class(MCPResourceContent, x), self.contents) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionInstalledPlugin: - """Installed plugin record for a session, with marketplace, version, install time, enabled - state, cache path, and source. - """ - enabled: bool - """Whether the plugin is currently enabled""" - - installed_at: str - """Installation timestamp (ISO-8601)""" +class WorkflowRunFailure: + """Machine-readable terminal failure. - marketplace: str - """Marketplace the plugin came from (empty string for direct repo installs)""" + Machine-readable workflow run failure. - name: str - """Plugin name""" + Machine-readable failure details for a halted or errored run. - cache_path: str | None = None - """Path where the plugin is cached locally""" + The run stopped because its usage accounting could not be completed. - installed_from: str | None = None - """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. + 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. """ - source: SessionInstalledPluginSource | str | None = None - """Source descriptor for direct repo installs (when marketplace is empty)""" + run_id: str + """Workflow run identifier. - source_sha: str | None = None - """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. + Workflow run identifier whose changed limits were declined. """ - version: str | None = None - """Installed version, if known""" + type: WorkflowRunFailureType + """Workflow failure variant discriminator.""" + + kind: WorkflowRunFailureKind | None = None + """Resource ceiling that stopped the run.""" + + suggested_value: float | None = None + """Suggested larger ceiling when the runtime can derive one safely.""" + + value: float | None = None + """Approved effective ceiling that was reached.""" + + reason: str | None = None + """Human-readable reason the resume did not proceed.""" + + code: str | None = None + """Stable failure code.""" + + operation: WorkflowDurableOperation | None = None + """Execution-critical durable operation that failed.""" + + drained_nano_aiu: int | None = None + """Confirmed usage in nano-AIU, representing the floor of what the run spent.""" @staticmethod - def from_dict(obj: Any) -> 'SessionInstalledPlugin': + def from_dict(obj: Any) -> 'WorkflowRunFailure': assert isinstance(obj, dict) - enabled = from_bool(obj.get("enabled")) - installed_at = from_str(obj.get("installed_at")) - marketplace = from_str(obj.get("marketplace")) - name = from_str(obj.get("name")) - cache_path = from_union([from_str, from_none], obj.get("cache_path")) - installed_from = from_union([from_str, from_none], obj.get("installed_from")) - source = from_union([SessionInstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) - source_sha = from_union([from_str, from_none], obj.get("source_sha")) - version = from_union([from_str, from_none], obj.get("version")) - return SessionInstalledPlugin(enabled, installed_at, marketplace, name, cache_path, installed_from, source, source_sha, version) + run_id = from_str(obj.get("runId")) + type = WorkflowRunFailureType(obj.get("type")) + kind = from_union([WorkflowRunFailureKind, from_none], obj.get("kind")) + suggested_value = from_union([from_float, from_none], obj.get("suggestedValue")) + value = from_union([from_float, from_none], obj.get("value")) + reason = from_union([from_str, from_none], obj.get("reason")) + code = from_union([from_str, from_none], obj.get("code")) + operation = from_union([WorkflowDurableOperation, from_none], obj.get("operation")) + drained_nano_aiu = from_union([from_int, from_none], obj.get("drainedNanoAiu")) + return WorkflowRunFailure(run_id, type, kind, suggested_value, value, reason, code, operation, drained_nano_aiu) def to_dict(self) -> dict: result: dict = {} - result["enabled"] = from_bool(self.enabled) - result["installed_at"] = from_str(self.installed_at) - result["marketplace"] = from_str(self.marketplace) - result["name"] = from_str(self.name) - if self.cache_path is not None: - result["cache_path"] = from_union([from_str, from_none], self.cache_path) - if self.installed_from is not None: - result["installed_from"] = from_union([from_str, from_none], self.installed_from) - if self.source is not None: - result["source"] = from_union([lambda x: to_class(SessionInstalledPluginSource, x), from_str, from_none], self.source) - if self.source_sha is not None: - result["source_sha"] = from_union([from_str, from_none], self.source_sha) - if self.version is not None: - result["version"] = from_union([from_str, from_none], self.version) + result["runId"] = from_str(self.run_id) + result["type"] = to_enum(WorkflowRunFailureType, self.type) + if self.kind is not None: + result["kind"] = from_union([lambda x: to_enum(WorkflowRunFailureKind, x), from_none], self.kind) + if self.suggested_value is not None: + result["suggestedValue"] = from_union([to_float, from_none], self.suggested_value) + if self.value is not None: + result["value"] = from_union([to_float, from_none], self.value) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + if self.code is not None: + result["code"] = from_union([from_str, from_none], self.code) + if self.operation is not None: + result["operation"] = from_union([lambda x: to_enum(WorkflowDurableOperation, x), from_none], self.operation) + if self.drained_nano_aiu is not None: + result["drainedNanoAiu"] = from_union([from_int, from_none], self.drained_nano_aiu) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -31542,6 +35388,40 @@ def to_dict(self) -> dict: result["user_named"] = from_union([from_bool, from_none], self.user_named) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsUpdateWorkspaceFieldsRequest: + """Where the session's state lives, plus workspace-schema fields to merge into its workspace + record. Stored keys outside the schema are not preserved, and a stored `fork_count` is + never replaced. + """ + fields_json: str + """Workspace-schema fields to merge into the record, as a JSON object. Fields the object + omits keep their stored values, except stored keys outside the schema are not preserved + and a stored `fork_count` is never replaced. + """ + session_id: str + """Session ID naming the state directory under the sessions home. Rejected when it is + absolute or contains a parent component, so it cannot escape the sessions home. + """ + sessions_home: str + """Root directory every session's state directory sits under""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsUpdateWorkspaceFieldsRequest': + assert isinstance(obj, dict) + fields_json = from_str(obj.get("fieldsJson")) + session_id = from_str(obj.get("sessionId")) + sessions_home = from_str(obj.get("sessionsHome")) + return SessionsUpdateWorkspaceFieldsRequest(fields_json, session_id, sessions_home) + + def to_dict(self) -> dict: + result: dict = {} + result["fieldsJson"] = from_str(self.fields_json) + result["sessionId"] = from_str(self.session_id) + result["sessionsHome"] = from_str(self.sessions_home) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkspacesCheckpoints: @@ -31625,6 +35505,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: @@ -31687,6 +35728,218 @@ def to_dict(self) -> dict: result["result"] = to_class(MCPHeadersHandlePendingHeadersRefreshRequest, self.result) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPInstallationOutcome: + """Immutable terminal receipt. + + Terminal mutation result. Uncertainty is not approval, rollback or permission to replay. + + Terminal result of the original operation. + + The durable transaction was aborted or fully compensated. + + A write may have completed. Recover and inspect durable state before retrying. + """ + kind: MCPInstallationOutcomeKind + """Terminal installation outcome discriminator.""" + + cleanup_pending: bool | None = None + """Durable installation succeeded but final transaction cleanup remains. + + Durable removal succeeded but final cleanup remains. + """ + installation: MCPInstallationSummary | None = None + """Receipt identity produced by the confirmed transaction.""" + + installation_id: str | None = None + """Exact removed receipt identity.""" + + operation_id: str | None = None + """Original removal operation. + + Original connection-owned operation. + + Operation whose durable result must be recovered and inspected. + + Original operation explicitly declined by the user. + + Original operation cancelled before a terminal application result. + + Present once an operation has been allocated; never a plan handle. + """ + preserved_shared_authentication: bool | None = None + """Any grants in the incumbent shared OAuth store remain unowned and retained.""" + + removed_owned_secrets: int | None = None + """Exact owned input slots removed, excluding shared OAuth credentials.""" + + restored_previous_configuration: bool | None = None + """Whether protected pre-install configuration was restored.""" + + reason: MCPInstallationFailureReason | None = None + """Cause of the fully aborted or compensated operation. + + Specific bounded refusal, never a success-shaped fallback. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationOutcome': + assert isinstance(obj, dict) + kind = MCPInstallationOutcomeKind(obj.get("kind")) + cleanup_pending = from_union([from_bool, from_none], obj.get("cleanupPending")) + installation = from_union([MCPInstallationSummary.from_dict, from_none], obj.get("installation")) + installation_id = from_union([from_str, from_none], obj.get("installationId")) + operation_id = from_union([from_str, from_none], obj.get("operationId")) + preserved_shared_authentication = from_union([from_bool, from_none], obj.get("preservedSharedAuthentication")) + removed_owned_secrets = from_union([from_int, from_none], obj.get("removedOwnedSecrets")) + restored_previous_configuration = from_union([from_bool, from_none], obj.get("restoredPreviousConfiguration")) + reason = from_union([MCPInstallationFailureReason, from_none], obj.get("reason")) + return MCPInstallationOutcome(kind, cleanup_pending, installation, installation_id, operation_id, preserved_shared_authentication, removed_owned_secrets, restored_previous_configuration, reason) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPInstallationOutcomeKind, self.kind) + if self.cleanup_pending is not None: + result["cleanupPending"] = from_union([from_bool, from_none], self.cleanup_pending) + if self.installation is not None: + result["installation"] = from_union([lambda x: to_class(MCPInstallationSummary, x), from_none], self.installation) + if self.installation_id is not None: + result["installationId"] = from_union([from_str, from_none], self.installation_id) + if self.operation_id is not None: + result["operationId"] = from_union([from_str, from_none], self.operation_id) + if self.preserved_shared_authentication is not None: + result["preservedSharedAuthentication"] = from_union([from_bool, from_none], self.preserved_shared_authentication) + if self.removed_owned_secrets is not None: + result["removedOwnedSecrets"] = from_union([from_int, from_none], self.removed_owned_secrets) + if self.restored_previous_configuration is not None: + result["restoredPreviousConfiguration"] = from_union([from_bool, from_none], self.restored_previous_configuration) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(MCPInstallationFailureReason, x), from_none], self.reason) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPUninstallPlan: + """Original owned removal plan and operation. + + Exact inert removal plan. No configuration or credentials have changed. + """ + expires_at_epoch_ms: int + """Original wall-clock expiry in milliseconds. Applying never renews it.""" + + installation: MCPInstallationSummary + """Original owned receipt being removed.""" + + operation_id: str + """The original operation, inspectable and cancellable on this same connection.""" + + owned_secret_count: int + """Exact configured input slots owned by this installation, never shared OAuth tokens.""" + + plan_handle: str + """One-use original connection and authority-bound plan handle.""" + + preserves_shared_authentication: bool + """Shared authentication is deliberately retained; revocation is a separate action.""" + + restores_previous_configuration: bool + """Whether removal restores a protected earlier configuration.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPUninstallPlan': + assert isinstance(obj, dict) + expires_at_epoch_ms = from_int(obj.get("expiresAtEpochMs")) + installation = MCPInstallationSummary.from_dict(obj.get("installation")) + operation_id = from_str(obj.get("operationId")) + owned_secret_count = from_int(obj.get("ownedSecretCount")) + plan_handle = from_str(obj.get("planHandle")) + preserves_shared_authentication = from_bool(obj.get("preservesSharedAuthentication")) + restores_previous_configuration = from_bool(obj.get("restoresPreviousConfiguration")) + return MCPUninstallPlan(expires_at_epoch_ms, installation, operation_id, owned_secret_count, plan_handle, preserves_shared_authentication, restores_previous_configuration) + + def to_dict(self) -> dict: + result: dict = {} + result["expiresAtEpochMs"] = from_int(self.expires_at_epoch_ms) + result["installation"] = to_class(MCPInstallationSummary, self.installation) + result["operationId"] = from_str(self.operation_id) + result["ownedSecretCount"] = from_int(self.owned_secret_count) + result["planHandle"] = from_str(self.plan_handle) + result["preservesSharedAuthentication"] = from_bool(self.preserves_shared_authentication) + 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 ConnectorDiscoveryCatalogResult: + """Entries for the selected account.""" + + account_id: str + """Opaque account ID.""" + + connectors: list[ConnectorDiscoveryCatalogEntry] + """Entries.""" + + refreshed_at_ms: int + """Refresh time in Unix epoch milliseconds.""" + + revision: int + """Revision.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDiscoveryCatalogResult': + assert isinstance(obj, dict) + account_id = from_str(obj.get("accountId")) + connectors = from_list(ConnectorDiscoveryCatalogEntry.from_dict, obj.get("connectors")) + refreshed_at_ms = from_int(obj.get("refreshedAtMs")) + revision = from_int(obj.get("revision")) + return ConnectorDiscoveryCatalogResult(account_id, connectors, refreshed_at_ms, revision) + + def to_dict(self) -> dict: + result: dict = {} + result["accountId"] = from_str(self.account_id) + result["connectors"] = from_list(lambda x: to_class(ConnectorDiscoveryCatalogEntry, x), self.connectors) + result["refreshedAtMs"] = from_int(self.refreshed_at_ms) + result["revision"] = from_int(self.revision) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsEntry: @@ -31863,40 +36116,6 @@ def to_dict(self) -> dict: result["tokenPrices"] = from_union([lambda x: to_class(ModelBillingTokenPrices, x), from_none], self.token_prices) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionModelList: - """The list of models available to this session.""" - - list: list[Any] - """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`). - """ - model_price_categories: list[SessionModelPriceCategory] | None = None - """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. - """ - quota_snapshots: dict[str, Any] | None = None - """Per-quota snapshots returned alongside the model list, keyed by quota type.""" - - @staticmethod - def from_dict(obj: Any) -> 'SessionModelList': - assert isinstance(obj, dict) - list = from_list(lambda x: x, obj.get("list")) - model_price_categories = from_union([lambda x: from_list(SessionModelPriceCategory.from_dict, x), from_none], obj.get("modelPriceCategories")) - quota_snapshots = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("quotaSnapshots")) - return SessionModelList(list, model_price_categories, quota_snapshots) - - def to_dict(self) -> dict: - result: dict = {} - result["list"] = from_list(lambda x: x, self.list) - if self.model_price_categories is not None: - result["modelPriceCategories"] = from_union([lambda x: from_list(lambda x: to_class(SessionModelPriceCategory, x), x), from_none], self.model_price_categories) - if self.quota_snapshots is not None: - result["quotaSnapshots"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.quota_snapshots) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelCapabilitiesOverride: @@ -31931,6 +36150,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: @@ -32020,6 +36366,33 @@ def to_dict(self) -> dict: result["decisionContext"] = from_union([lambda x: to_class(PermissionDecisionContext, x), from_none], self.decision_context) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxGrantPathForRequestRequest: + """Request to accept the sandbox path grant offered on an active sandbox escalation + permission prompt. + """ + request_id: str + """Identifier of the exact pending sandbox escalation permission request whose + sandboxPathGrant to accept. + """ + decision_context: PermissionDecisionContext | None = None + """Optional attribution for the permission decision.""" + + @staticmethod + def from_dict(obj: Any) -> 'SandboxGrantPathForRequestRequest': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + decision_context = from_union([PermissionDecisionContext.from_dict, from_none], obj.get("decisionContext")) + return SandboxGrantPathForRequestRequest(request_id, decision_context) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + if self.decision_context is not None: + result["decisionContext"] = from_union([lambda x: to_class(PermissionDecisionContext, x), from_none], self.decision_context) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionsConfigureAdditionalContentExclusionPolicy: @@ -32088,56 +36461,6 @@ def to_dict(self) -> dict: result["observedAt"] = from_str(self.observed_at) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class CatalogSearchRequest: - """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. - """ - contract: CatalogClientContract - """Protocol version and capabilities the caller requires.""" - - query: str - """Free-text search query. Persisted as tool input for session continuity, but omitted from - telemetry. - """ - kinds: list[CatalogCandidateKind] | None = None - """Restrict results to these candidate kinds. Agent Plugins are opt-in and require the - `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant - cannot receive an unknown result; when omitted, the backwards-compatible MCP server and - AI skill kinds are searched. - """ - limit: int | None = None - """Maximum number of candidates to return. Defaults to 10 when omitted.""" - - page: CatalogSearchPage | None = None - """Numbered navigation using metadata from an earlier response. Requires - catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh - first-page search. - """ - - @staticmethod - def from_dict(obj: Any) -> 'CatalogSearchRequest': - assert isinstance(obj, dict) - contract = CatalogClientContract.from_dict(obj.get("contract")) - query = from_str(obj.get("query")) - kinds = from_union([lambda x: from_list(CatalogCandidateKind, x), from_none], obj.get("kinds")) - limit = from_union([from_int, from_none], obj.get("limit")) - page = from_union([CatalogSearchPage.from_dict, from_none], obj.get("page")) - return CatalogSearchRequest(contract, query, kinds, limit, page) - - def to_dict(self) -> dict: - result: dict = {} - result["contract"] = to_class(CatalogClientContract, self.contract) - result["query"] = from_str(self.query) - if self.kinds is not None: - result["kinds"] = from_union([lambda x: from_list(lambda x: to_enum(CatalogCandidateKind, x), x), from_none], self.kinds) - if self.limit is not None: - result["limit"] = from_union([from_int, from_none], self.limit) - if self.page is not None: - result["page"] = from_union([lambda x: to_class(CatalogSearchPage, x), from_none], self.page) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CatalogUnsupportedKindError: @@ -32382,6 +36705,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: @@ -32484,72 +36827,38 @@ def to_dict(self) -> dict: result["inputSchema"] = self.input_schema return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryPauseCheckpointResult: - action: PauseCheckpointAction - """Whether this execution attempt must pause or may continue.""" - - @staticmethod - def from_dict(obj: Any) -> 'FactoryPauseCheckpointResult': - assert isinstance(obj, dict) - action = PauseCheckpointAction(obj.get("action")) - return FactoryPauseCheckpointResult(action) - - def to_dict(self) -> dict: - result: dict = {} - result["action"] = to_enum(PauseCheckpointAction, self.action) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionFactoryPauseAtCheckpointResult: - action: PauseCheckpointAction - """Whether this execution attempt must pause or may continue.""" - - @staticmethod - def from_dict(obj: Any) -> 'SessionFactoryPauseAtCheckpointResult': - assert isinstance(obj, dict) - action = PauseCheckpointAction(obj.get("action")) - return SessionFactoryPauseAtCheckpointResult(action) - - def to_dict(self) -> dict: - result: dict = {} - result["action"] = to_enum(PauseCheckpointAction, self.action) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionWorkflowPauseAtCheckpointResult: - action: PauseCheckpointAction + action: WorkflowPauseCheckpointAction """Whether this execution attempt must pause or may continue.""" @staticmethod def from_dict(obj: Any) -> 'SessionWorkflowPauseAtCheckpointResult': assert isinstance(obj, dict) - action = PauseCheckpointAction(obj.get("action")) + action = WorkflowPauseCheckpointAction(obj.get("action")) return SessionWorkflowPauseAtCheckpointResult(action) def to_dict(self) -> dict: result: dict = {} - result["action"] = to_enum(PauseCheckpointAction, self.action) + result["action"] = to_enum(WorkflowPauseCheckpointAction, self.action) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkflowPauseCheckpointResult: - action: PauseCheckpointAction + action: WorkflowPauseCheckpointAction """Whether this execution attempt must pause or may continue.""" @staticmethod def from_dict(obj: Any) -> 'WorkflowPauseCheckpointResult': assert isinstance(obj, dict) - action = PauseCheckpointAction(obj.get("action")) + action = WorkflowPauseCheckpointAction(obj.get("action")) return WorkflowPauseCheckpointResult(action) def to_dict(self) -> dict: result: dict = {} - result["action"] = to_enum(PauseCheckpointAction, self.action) + result["action"] = to_enum(WorkflowPauseCheckpointAction, self.action) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -32585,86 +36894,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: @@ -33155,6 +37384,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: @@ -33531,6 +37785,599 @@ def to_dict(self) -> dict: result["includePrompt"] = from_union([from_bool, from_none], self.include_prompt) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CatalogSearchRequest: + """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. + """ + contract: CatalogClientContract + """Protocol version and capabilities the caller requires.""" + + query: str + """Free-text search query. Persisted as tool input for session continuity, but omitted from + telemetry. + """ + kinds: list[CatalogCandidateKind] | None = None + """Restrict results to these candidate kinds. Agent Plugins are opt-in and require the + `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant + cannot receive an unknown result; when omitted, the backwards-compatible MCP server and + AI skill kinds are searched. + """ + limit: int | None = None + """Maximum number of candidates to return. Defaults to 10 when omitted.""" + + page: CatalogSearchPage | None = None + """Numbered navigation using metadata from an earlier response. Requires + catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh + first-page search. + """ + policy_session_id: str | None = field(default=None, kw_only=True) + """Select an existing attached local session. Requires authenticated, session-bound search. + The runtime never creates, resumes or reconfigures a session to honour this selector. + """ + + @staticmethod + def from_dict(obj: Any) -> 'CatalogSearchRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + query = from_str(obj.get("query")) + kinds = from_union([lambda x: from_list(CatalogCandidateKind, x), from_none], obj.get("kinds")) + limit = from_union([from_int, from_none], obj.get("limit")) + page = from_union([CatalogSearchPage.from_dict, from_none], obj.get("page")) + policy_session_id = from_union([from_str, from_none], obj.get("policySessionId")) + return CatalogSearchRequest(contract, query, kinds, limit, page, policy_session_id=policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["query"] = from_str(self.query) + if self.kinds is not None: + result["kinds"] = from_union([lambda x: from_list(lambda x: to_enum(CatalogCandidateKind, x), x), from_none], self.kinds) + if self.limit is not None: + result["limit"] = from_union([from_int, from_none], self.limit) + if self.page is not None: + result["page"] = from_union([lambda x: to_class(CatalogSearchPage, x), from_none], self.page) + if self.policy_session_id is not None: + result["policySessionId"] = from_union([from_str, from_none], self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CatalogSelectionRequest: + """Terminates one retained catalog selection group through an opaque reference previously + returned by the model-safe search projection. + """ + contract: CatalogClientContract + """Protocol version and capabilities the caller requires.""" + + outcome: CatalogSelectionDecision + """The terminal outcome declared by the caller. Timed-out means the host's live interaction + deadline elapsed; a reference whose runtime TTL elapsed is rejected separately as stale. + """ + selection_ref: str + """Opaque runtime-instance scoped reference to one visible candidate. For a non-selected + outcome, any candidate reference from the same search closes that search's retained group. + """ + session_id: str + """Locally owned root session whose retained search state is being resolved.""" + + @staticmethod + def from_dict(obj: Any) -> 'CatalogSelectionRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + outcome = CatalogSelectionDecision(obj.get("outcome")) + selection_ref = from_str(obj.get("selectionRef")) + session_id = from_str(obj.get("sessionId")) + return CatalogSelectionRequest(contract, outcome, selection_ref, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["outcome"] = to_enum(CatalogSelectionDecision, self.outcome) + result["selectionRef"] = from_str(self.selection_ref) + 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 MCPApplyInstallRequest: + """Applies exactly one previously prepared operation on its original connection.""" + + contract: CatalogClientContract + """Capabilities required by the original prepared operation.""" + + operation_id: str + """Runtime-issued ID already returned by prepareInstall, never reused or rebound.""" + + policy_session_id: str + """Same existing attached or privately borrowed session as preparation.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPApplyInstallRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + operation_id = from_str(obj.get("operationId")) + policy_session_id = from_str(obj.get("policySessionId")) + return MCPApplyInstallRequest(contract, operation_id, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["operationId"] = from_str(self.operation_id) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPApplyUninstallRequest: + """One-use application of the exact retained removal plan.""" + + contract: CatalogClientContract + """Required authenticated bound installation capabilities.""" + + plan_handle: str + """Opaque original removal plan, consumed once.""" + + policy_session_id: str + """Same existing selected session as removal preparation.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPApplyUninstallRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + plan_handle = from_str(obj.get("planHandle")) + policy_session_id = from_str(obj.get("policySessionId")) + return MCPApplyUninstallRequest(contract, plan_handle, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["planHandle"] = from_str(self.plan_handle) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPInstallationOperationRequest: + """Existing-operation control. A new session selector is deliberately not accepted.""" + + contract: CatalogClientContract + """Original installation wire capability; new-work authentication is not reacquired.""" + + operation_id: str + """Exact runtime-issued operation ID on the original connection.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationOperationRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + operation_id = from_str(obj.get("operationId")) + return MCPInstallationOperationRequest(contract, operation_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + 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 MCPInstallationsRequest: + """New-work inventory or recovery request under an explicitly selected existing session.""" + + contract: CatalogClientContract + """Required authenticated bound installation contract.""" + + policy_session_id: str + """Existing selected local session on this connection.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationsRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + policy_session_id = from_str(obj.get("policySessionId")) + return MCPInstallationsRequest(contract, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPlanInstallRequest: + """A side-effect-free request for an MCP install plan. Computing a plan never writes + configuration, stores a secret, or reloads MCP servers. + """ + contract: CatalogClientContract + """Protocol version and capabilities the caller requires.""" + + source: MCPPlanInstallSource + """What to plan: either a candidate handle from a previous search, or a card supplied + directly. + """ + policy_session_id: str | None = field(default=None, kw_only=True) + """The same existing attached session that owns the original catalogue candidate.""" + + scope: MCPPlanScope | None = None + """Configuration scope the plan targets. Defaults to user scope when omitted.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPlanInstallRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + source = _load_MCPPlanInstallSource(obj.get("source")) + policy_session_id = from_union([from_str, from_none], obj.get("policySessionId")) + scope = from_union([MCPPlanScope, from_none], obj.get("scope")) + return MCPPlanInstallRequest(contract, source, scope, policy_session_id=policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["source"] = (self.source).to_dict() + if self.policy_session_id is not None: + result["policySessionId"] = from_union([from_str, from_none], self.policy_session_id) + if self.scope is not None: + result["scope"] = from_union([lambda x: to_enum(MCPPlanScope, x), from_none], self.scope) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPlanUninstallRequest: + """Read-only preparation of one owned removal under fresh selected-session authority.""" + + contract: CatalogClientContract + """Required authenticated bound installation capabilities.""" + + installation_id: str + """Exact receipt to inspect, not a server-name guess.""" + + policy_session_id: str + """Existing selected session on the original connection.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPlanUninstallRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + installation_id = from_str(obj.get("installationId")) + policy_session_id = from_str(obj.get("policySessionId")) + return MCPPlanUninstallRequest(contract, installation_id, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["installationId"] = from_str(self.installation_id) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPrepareInstallRequest: + """Side-effect-free preparation of one original bound remote MCP choice.""" + + choice_id: str + """Exact selected alternative from that plan.""" + + contract: CatalogClientContract + """Required bound catalogue and confirmed remote installation capabilities.""" + + inputs: list[MCPInstallationInput] + """Declared non-secret values. Non-empty only when the caller requires + `mcp-configured-remote-installation`; omitted values use the card default. + """ + plan_handle: str + """Original single-use bound plan, never a client-authored configuration.""" + + policy_session_id: str + """An existing local session attached to this connection, not permission to attach one.""" + + secrets: list[MCPInstallationSecret] + """One entry per declared secret placeholder of the selected choice. Non-empty + only when the caller requires `mcp-configured-remote-installation`. + """ + secret_storage: MCPInstallationSecretStorage + """The trusted host presents this choice alongside the exact secret placeholders.""" + + source: MCPServerCardReference + """The exact original source, used transiently only after confirmation.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPrepareInstallRequest': + assert isinstance(obj, dict) + choice_id = from_str(obj.get("choiceId")) + contract = CatalogClientContract.from_dict(obj.get("contract")) + inputs = from_list(MCPInstallationInput.from_dict, obj.get("inputs")) + plan_handle = from_str(obj.get("planHandle")) + policy_session_id = from_str(obj.get("policySessionId")) + secrets = from_list(MCPInstallationSecret.from_dict, obj.get("secrets")) + secret_storage = MCPInstallationSecretStorage(obj.get("secretStorage")) + source = _load_MCPServerCardReference(obj.get("source")) + return MCPPrepareInstallRequest(choice_id, contract, inputs, plan_handle, policy_session_id, secrets, secret_storage, source) + + def to_dict(self) -> dict: + result: dict = {} + result["choiceId"] = from_str(self.choice_id) + result["contract"] = to_class(CatalogClientContract, self.contract) + result["inputs"] = from_list(lambda x: to_class(MCPInstallationInput, x), self.inputs) + result["planHandle"] = from_str(self.plan_handle) + result["policySessionId"] = from_str(self.policy_session_id) + result["secrets"] = from_list(lambda x: to_class(MCPInstallationSecret, x), self.secrets) + result["secretStorage"] = to_enum(MCPInstallationSecretStorage, self.secret_storage) + result["source"] = (self.source).to_dict() + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillApplyInstallRequest: + """Applies exactly one retained verified Skill installation plan.""" + + contract: CatalogClientContract + """Required authenticated bound catalogue and Skill installation capabilities.""" + + plan_handle: str + """Opaque original plan, consumed once.""" + + policy_session_id: str + """Same existing selected session as planning.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillApplyInstallRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + plan_handle = from_str(obj.get("planHandle")) + policy_session_id = from_str(obj.get("policySessionId")) + return SkillApplyInstallRequest(contract, plan_handle, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["planHandle"] = from_str(self.plan_handle) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillApplyUninstallRequest: + """One-use application of the exact retained Skill removal plan.""" + + contract: CatalogClientContract + """Required authenticated bound installation contract.""" + + plan_handle: str + """Opaque original removal plan, consumed once.""" + + policy_session_id: str + """Same existing selected session as removal preparation.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillApplyUninstallRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + plan_handle = from_str(obj.get("planHandle")) + policy_session_id = from_str(obj.get("policySessionId")) + return SkillApplyUninstallRequest(contract, plan_handle, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["planHandle"] = from_str(self.plan_handle) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationOperationRequest: + """Existing-operation control. A new session selector is deliberately not accepted.""" + + contract: CatalogClientContract + """Required authenticated bound Skill installation capability.""" + + operation_id: str + """Exact runtime-issued operation ID on the original connection.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationOperationRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + operation_id = from_str(obj.get("operationId")) + return SkillInstallationOperationRequest(contract, operation_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + 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 SkillInstallationsRequest: + """Inventory request under an explicitly selected existing session.""" + + contract: CatalogClientContract + """Required authenticated bound installation contract.""" + + policy_session_id: str + """Existing selected local session on this connection.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationsRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + policy_session_id = from_str(obj.get("policySessionId")) + return SkillInstallationsRequest(contract, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillPlanInstallRequest: + """Side-effect-free planning of one verified Agent Finder Skill candidate.""" + + candidate_handle: str + """Fresh single-use AI skill candidate handle returned by a bound catalogue search.""" + + contract: CatalogClientContract + """Required authenticated bound catalogue and Skill installation capabilities.""" + + policy_session_id: str + """Existing local session attached to this connection.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillPlanInstallRequest': + assert isinstance(obj, dict) + candidate_handle = from_str(obj.get("candidateHandle")) + contract = CatalogClientContract.from_dict(obj.get("contract")) + policy_session_id = from_str(obj.get("policySessionId")) + return SkillPlanInstallRequest(candidate_handle, contract, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["candidateHandle"] = from_str(self.candidate_handle) + result["contract"] = to_class(CatalogClientContract, self.contract) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillPlanUninstallRequest: + """Read-only preparation of one owned Skill removal under fresh selected-session authority.""" + + contract: CatalogClientContract + """Required authenticated bound installation contract.""" + + installation_id: str + """Exact receipt to inspect.""" + + policy_session_id: str + """Existing selected local session on this connection.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillPlanUninstallRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + installation_id = from_str(obj.get("installationId")) + policy_session_id = from_str(obj.get("policySessionId")) + return SkillPlanUninstallRequest(contract, installation_id, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["installationId"] = from_str(self.installation_id) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillSetEnabledRequest: + """Persisted enablement update for one owned Skill installation.""" + + contract: CatalogClientContract + """Required authenticated bound Skill installation capability.""" + + enabled: bool + """Persisted enablement value.""" + + installation_id: str + """Exact receipt identity to update.""" + + policy_session_id: str + """Existing selected local session to reconcile after persistence.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillSetEnabledRequest': + assert isinstance(obj, dict) + contract = CatalogClientContract.from_dict(obj.get("contract")) + enabled = from_bool(obj.get("enabled")) + installation_id = from_str(obj.get("installationId")) + policy_session_id = from_str(obj.get("policySessionId")) + return SkillSetEnabledRequest(contract, enabled, installation_id, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["contract"] = to_class(CatalogClientContract, self.contract) + result["enabled"] = from_bool(self.enabled) + result["installationId"] = from_str(self.installation_id) + result["policySessionId"] = from_str(self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationSummary: + """Owned installation being removed. + + Durable verified Skill ownership summary. + + Owned Skill installation being removed. + + Durable installed Skill summary. + + Updated installation summary. + """ + catalogue: InstallationCatalogueIdentity + """Catalogue identity retained at install time.""" + + configured_enabled: bool + """Persisted enablement requested for this installation.""" + + installation_id: str + """Exact durable installation receipt identity.""" + + installed_at: str + """ISO 8601 wall-clock installation time.""" + + name: str + """Skill invocation name.""" + + operation_id: str + """Operation that installed this Skill.""" + + ownership_state: SkillInstallationOwnershipState + """Ownership state observed from files and receipts.""" + + session_state: SkillInstallationSessionState + """Bound-session load observation.""" + + source: SkillInstallationSource + """Exact retained verified source identity.""" + + target: SkillInstallationLocation + """User-facing installation location without an absolute host path.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationSummary': + assert isinstance(obj, dict) + catalogue = InstallationCatalogueIdentity.from_dict(obj.get("catalogue")) + configured_enabled = from_bool(obj.get("configuredEnabled")) + installation_id = from_str(obj.get("installationId")) + installed_at = from_str(obj.get("installedAt")) + name = from_str(obj.get("name")) + operation_id = from_str(obj.get("operationId")) + ownership_state = SkillInstallationOwnershipState(obj.get("ownershipState")) + session_state = SkillInstallationSessionState(obj.get("sessionState")) + source = SkillInstallationSource.from_dict(obj.get("source")) + target = SkillInstallationLocation.from_dict(obj.get("target")) + return SkillInstallationSummary(catalogue, configured_enabled, installation_id, installed_at, name, operation_id, ownership_state, session_state, source, target) + + def to_dict(self) -> dict: + result: dict = {} + result["catalogue"] = to_class(InstallationCatalogueIdentity, self.catalogue) + result["configuredEnabled"] = from_bool(self.configured_enabled) + result["installationId"] = from_str(self.installation_id) + result["installedAt"] = from_str(self.installed_at) + result["name"] = from_str(self.name) + result["operationId"] = from_str(self.operation_id) + result["ownershipState"] = to_enum(SkillInstallationOwnershipState, self.ownership_state) + result["sessionState"] = to_enum(SkillInstallationSessionState, self.session_state) + result["source"] = to_class(SkillInstallationSource, self.source) + result["target"] = to_class(SkillInstallationLocation, self.target) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SkillsGetInvokedResult: @@ -34090,114 +38937,77 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class WorkflowRunTerminal: - """Prompt-safe terminal workflow outcome.""" - - error: str | None = None - """Human-readable terminal error.""" - - failure: WorkflowRunFailure | None = None - """Machine-readable terminal failure.""" +class WorkflowLogRequest: + """Parameters for recording workflow progress.""" - pause_info: WorkflowRunTerminalPauseInfo | None = None - """Pause initiator metadata, or null when the run did not pause.""" + execution_token: str + """Opaque token identifying the current workflow execution attempt.""" - reason: str | None = None - """Human-readable terminal reason.""" + lines: list[WorkflowLogLine] + """Ordered progress lines to append.""" - result_preview: str | None = None - """Prompt-safe preview of the completed result.""" + run_id: str + """Workflow run identifier.""" @staticmethod - def from_dict(obj: Any) -> 'WorkflowRunTerminal': + def from_dict(obj: Any) -> 'WorkflowLogRequest': assert isinstance(obj, dict) - error = from_union([from_str, from_none], obj.get("error")) - failure = from_union([WorkflowRunFailure.from_dict, from_none], obj.get("failure")) - pause_info = from_union([WorkflowRunTerminalPauseInfo.from_dict, from_none], obj.get("pauseInfo")) - reason = from_union([from_str, from_none], obj.get("reason")) - result_preview = from_union([from_str, from_none], obj.get("resultPreview")) - return WorkflowRunTerminal(error, failure, pause_info, reason, result_preview) + execution_token = from_str(obj.get("executionToken")) + lines = from_list(WorkflowLogLine.from_dict, obj.get("lines")) + run_id = from_str(obj.get("runId")) + return WorkflowLogRequest(execution_token, lines, run_id) def to_dict(self) -> dict: result: dict = {} - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.failure is not None: - result["failure"] = from_union([lambda x: to_class(WorkflowRunFailure, x), from_none], self.failure) - result["pauseInfo"] = from_union([lambda x: to_class(WorkflowRunTerminalPauseInfo, x), from_none], self.pause_info) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.result_preview is not None: - result["resultPreview"] = from_union([from_str, from_none], self.result_preview) + result["executionToken"] = from_str(self.execution_token) + result["lines"] = from_list(lambda x: to_class(WorkflowLogLine, x), self.lines) + result["runId"] = from_str(self.run_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class WorkflowRunResult: - """Terminal resumed run envelope. - - Complete current or terminal workflow run envelope. - """ - run_id: str - """Workflow run identifier.""" - - status: RunStatus - """Current or terminal workflow run status.""" +class WorkflowProgressPage: + """A bidirectional page of workflow progress. - attempt: int | None = None - """One-based execution attempt represented by this envelope. Absent before the first attempt - starts or when returned by an older runtime. + Bidirectional page of durable workflow progress. """ - error: str | None = None - """Error message for an errored run.""" + has_more_newer: bool + """Whether progress records newer than this page exist.""" - failure: WorkflowRunFailure | None = None - """Machine-readable failure details for a halted or errored run.""" + has_more_older: bool + """Whether progress records older than this page exist.""" - pause_info: WorkflowPauseInfo | None = None - """Structured pause initiator metadata for a paused attempt.""" + records: list[WorkflowProgressLine] + """Progress records in sequence order.""" - reason: str | None = None - """Reason for a halted or cancelled run.""" + revision: int + """Run revision reflected by this page.""" - result: Any = None - """Completed workflow result.""" + newest_seq: int | None = None + """Newest sequence number in this page, or null when empty.""" - snapshot: Any = None - """Partial journal and progress snapshot for a halted, cancelled, or errored run.""" + oldest_seq: int | None = None + """Oldest sequence number in this page, or null when empty.""" @staticmethod - def from_dict(obj: Any) -> 'WorkflowRunResult': + def from_dict(obj: Any) -> 'WorkflowProgressPage': assert isinstance(obj, dict) - run_id = from_str(obj.get("runId")) - status = RunStatus(obj.get("status")) - attempt = from_union([from_int, from_none], obj.get("attempt")) - error = from_union([from_str, from_none], obj.get("error")) - failure = from_union([WorkflowRunFailure.from_dict, from_none], obj.get("failure")) - pause_info = from_union([WorkflowPauseInfo.from_dict, from_none], obj.get("pauseInfo")) - reason = from_union([from_str, from_none], obj.get("reason")) - result = obj.get("result") - snapshot = obj.get("snapshot") - return WorkflowRunResult(run_id, status, attempt, error, failure, pause_info, reason, result, snapshot) + has_more_newer = from_bool(obj.get("hasMoreNewer")) + has_more_older = from_bool(obj.get("hasMoreOlder")) + records = from_list(WorkflowProgressLine.from_dict, obj.get("records")) + revision = from_int(obj.get("revision")) + newest_seq = from_union([from_int, from_none], obj.get("newestSeq")) + oldest_seq = from_union([from_int, from_none], obj.get("oldestSeq")) + return WorkflowProgressPage(has_more_newer, has_more_older, records, revision, newest_seq, oldest_seq) def to_dict(self) -> dict: result: dict = {} - result["runId"] = from_str(self.run_id) - result["status"] = to_enum(RunStatus, self.status) - if self.attempt is not None: - result["attempt"] = from_union([from_int, from_none], self.attempt) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.failure is not None: - result["failure"] = from_union([lambda x: to_class(WorkflowRunFailure, x), from_none], self.failure) - if self.pause_info is not None: - result["pauseInfo"] = from_union([lambda x: to_class(WorkflowPauseInfo, x), from_none], self.pause_info) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.result is not None: - result["result"] = self.result - if self.snapshot is not None: - result["snapshot"] = self.snapshot + result["hasMoreNewer"] = from_bool(self.has_more_newer) + result["hasMoreOlder"] = from_bool(self.has_more_older) + result["records"] = from_list(lambda x: to_class(WorkflowProgressLine, x), self.records) + result["revision"] = from_int(self.revision) + result["newestSeq"] = from_union([from_int, from_none], self.newest_seq) + result["oldestSeq"] = from_union([from_int, from_none], self.oldest_seq) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -34285,40 +39095,47 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class _FactoryToolRunRequest: - """Internal parameters for invoking a registered factory from a tool.""" - - args: Any - """Factory input value.""" - - name: str - """Registered factory name.""" - - options: _FactoryToolRunOptions | None = None - """Tool-originated factory invocation options.""" +class SessionModelList: + """The list of models available to this session.""" - tool_call_id: str | None = None - """Opaque identifier of the originating tool call.""" + list: list[Any] + """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`). + """ + model_price_categories: list[SessionModelPriceCategory] | None = None + """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. + """ + providers: list[ModelProviderDescriptor] | None = None + """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. + """ + quota_snapshots: dict[str, Any] | None = None + """Per-quota snapshots returned alongside the model list, keyed by quota type.""" @staticmethod - def from_dict(obj: Any) -> '_FactoryToolRunRequest': + def from_dict(obj: Any) -> 'SessionModelList': assert isinstance(obj, dict) - args = obj.get("args") - name = from_str(obj.get("name")) - options = from_union([_FactoryToolRunOptions.from_dict, from_none], obj.get("options")) - tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) - return _FactoryToolRunRequest(args, name, options, tool_call_id) + list = from_list(lambda x: x, obj.get("list")) + model_price_categories = from_union([lambda x: from_list(SessionModelPriceCategory.from_dict, x), from_none], obj.get("modelPriceCategories")) + providers = from_union([lambda x: from_list(ModelProviderDescriptor.from_dict, x), from_none], obj.get("providers")) + quota_snapshots = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("quotaSnapshots")) + return SessionModelList(list, model_price_categories, providers, quota_snapshots) def to_dict(self) -> dict: result: dict = {} - result["args"] = self.args - result["name"] = from_str(self.name) - if self.options is not None: - result["options"] = from_union([lambda x: to_class(_FactoryToolRunOptions, x), from_none], self.options) - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_str, from_none], self.tool_call_id) + result["list"] = from_list(lambda x: x, self.list) + if self.model_price_categories is not None: + result["modelPriceCategories"] = from_union([lambda x: from_list(lambda x: to_class(SessionModelPriceCategory, x), x), from_none], self.model_price_categories) + if self.providers is not None: + result["providers"] = from_union([lambda x: from_list(lambda x: to_class(ModelProviderDescriptor, x), x), from_none], self.providers) + if self.quota_snapshots is not None: + result["quotaSnapshots"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.quota_snapshots) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -35096,7 +39913,7 @@ class MCPInstallPlan: ready. A later apply operation must validate the selected choice's own inputs, secrets and policy after explicit confirmation. """ - target: MCPPlanTarget + target: MCPPlanTargetClass """Configuration scope and key the plan would write to.""" transport_choices: list[MCPPlanTransportChoice] @@ -35120,7 +39937,7 @@ def from_dict(obj: Any) -> 'MCPInstallPlan': provenance = MCPPlanProvenance.from_dict(obj.get("provenance")) reload_required = from_bool(obj.get("reloadRequired")) requires_interactive_configuration = from_bool(obj.get("requiresInteractiveConfiguration")) - target = MCPPlanTarget.from_dict(obj.get("target")) + target = MCPPlanTargetClass.from_dict(obj.get("target")) transport_choices = from_list(_load_MCPPlanTransportChoice, obj.get("transportChoices")) recommended_transport_choice_id = from_union([from_str, from_none], obj.get("recommendedTransportChoiceId")) return MCPInstallPlan(configuration_changes, identity, plan_handle, plan_handle_expires_at, policy, provenance, reload_required, requires_interactive_configuration, target, transport_choices, recommended_transport_choice_id) @@ -35135,12 +39952,134 @@ def to_dict(self) -> dict: result["provenance"] = to_class(MCPPlanProvenance, self.provenance) result["reloadRequired"] = from_bool(self.reload_required) result["requiresInteractiveConfiguration"] = from_bool(self.requires_interactive_configuration) - result["target"] = to_class(MCPPlanTarget, self.target) + result["target"] = to_class(MCPPlanTargetClass, self.target) result["transportChoices"] = from_list(lambda x: (x).to_dict(), self.transport_choices) if self.recommended_transport_choice_id is not None: result["recommendedTransportChoiceId"] = from_union([from_str, from_none], self.recommended_transport_choice_id) return result +@dataclass +class MCPInstallationReview: + """The exact MCP action and its reviewed changes. + + Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. + """ + action: MCPInstallationReviewAction + """Exact reviewed installation action.""" + + identity: MCPPlanResourceIdentity + """Identity from the retained plan, not caller display text. + + Identity from the installed receipt. + """ + policy: MCPPlanPolicyResult + """Policy decision bound to this plan. + + Current removal policy, independent of permission to activate the server. + """ + provenance: MCPPlanProvenance + """Original source identity and content commitment. + + Source identity and content commitment retained by the installed receipt. + """ + target: MCPPlanTargetClass + """Exact reviewed user-scope destination. + + Exact destination, checked for intervening changes before mutation. + """ + catalogue: InstallationCatalogueIdentity | None = None + """Catalogue identity retained from the bound candidate when available.""" + + catalogue_trust: CatalogTrustSnapshot | None = None + """Original catalogue trust metadata, not a verification claim.""" + + configuration_change: MCPPlanConfigurationChange | None = None + """The configuration change for the selected alternative only.""" + + effective_configuration: MCPInstallationRemoteConfiguration | None = None + """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. + """ + inputs: list[MCPInstallationInput] | None = None + """Non-secret values supplied for this selected alternative.""" + + secret_storage: MCPInstallationSecretStorage | None = None + """Explicit reviewed backend selection; no backend is accessed when no secrets are supplied.""" + + selected_choice: MCPPlanTransportChoice | None = None + """Only the selected alternative is applied.""" + + supplied_secrets: list[str] | None = None + """Exact reviewed placeholders supplied separately. Never secret values.""" + + installation_id: str | None = None + """Receipt-owned installation being removed.""" + + owned_secret_count: int | None = None + """Exact planner-owned secret slots to remove, excluding shared OAuth grants.""" + + preserves_shared_authentication: bool | None = None + """Shared profile authentication is deliberately retained, not pending cleanup.""" + + restores_previous_configuration: bool | None = None + """Whether uninstall restores a protected pre-install configuration.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationReview': + assert isinstance(obj, dict) + action = MCPInstallationReviewAction(obj.get("action")) + identity = MCPPlanResourceIdentity.from_dict(obj.get("identity")) + policy = MCPPlanPolicyResult.from_dict(obj.get("policy")) + provenance = MCPPlanProvenance.from_dict(obj.get("provenance")) + target = MCPPlanTargetClass.from_dict(obj.get("target")) + catalogue = from_union([InstallationCatalogueIdentity.from_dict, from_none], obj.get("catalogue")) + catalogue_trust = from_union([_load_CatalogTrustSnapshot, from_none], obj.get("catalogueTrust")) + configuration_change = from_union([MCPPlanConfigurationChange.from_dict, from_none], obj.get("configurationChange")) + effective_configuration = from_union([MCPInstallationRemoteConfiguration.from_dict, from_none], obj.get("effectiveConfiguration")) + inputs = from_union([lambda x: from_list(MCPInstallationInput.from_dict, x), from_none], obj.get("inputs")) + secret_storage = from_union([MCPInstallationSecretStorage, from_none], obj.get("secretStorage")) + selected_choice = from_union([_load_MCPPlanTransportChoice, from_none], obj.get("selectedChoice")) + supplied_secrets = from_union([lambda x: from_list(from_str, x), from_none], obj.get("suppliedSecrets")) + installation_id = from_union([from_str, from_none], obj.get("installationId")) + owned_secret_count = from_union([from_int, from_none], obj.get("ownedSecretCount")) + preserves_shared_authentication = from_union([from_bool, from_none], obj.get("preservesSharedAuthentication")) + restores_previous_configuration = from_union([from_bool, from_none], obj.get("restoresPreviousConfiguration")) + return MCPInstallationReview(action, identity, policy, provenance, target, catalogue, catalogue_trust, configuration_change, effective_configuration, inputs, secret_storage, selected_choice, supplied_secrets, installation_id, owned_secret_count, preserves_shared_authentication, restores_previous_configuration) + + def to_dict(self) -> dict: + result: dict = {} + result["action"] = to_enum(MCPInstallationReviewAction, self.action) + result["identity"] = to_class(MCPPlanResourceIdentity, self.identity) + result["policy"] = to_class(MCPPlanPolicyResult, self.policy) + result["provenance"] = to_class(MCPPlanProvenance, self.provenance) + result["target"] = to_class(MCPPlanTargetClass, self.target) + if self.catalogue is not None: + result["catalogue"] = from_union([lambda x: to_class(InstallationCatalogueIdentity, x), from_none], self.catalogue) + if self.catalogue_trust is not None: + result["catalogueTrust"] = from_union([lambda x: (x).to_dict(), from_none], self.catalogue_trust) + if self.configuration_change is not None: + result["configurationChange"] = from_union([lambda x: to_class(MCPPlanConfigurationChange, x), from_none], self.configuration_change) + if self.effective_configuration is not None: + result["effectiveConfiguration"] = from_union([lambda x: to_class(MCPInstallationRemoteConfiguration, x), from_none], self.effective_configuration) + if self.inputs is not None: + result["inputs"] = from_union([lambda x: from_list(lambda x: to_class(MCPInstallationInput, x), x), from_none], self.inputs) + if self.secret_storage is not None: + result["secretStorage"] = from_union([lambda x: to_enum(MCPInstallationSecretStorage, x), from_none], self.secret_storage) + if self.selected_choice is not None: + result["selectedChoice"] = from_union([lambda x: (x).to_dict(), from_none], self.selected_choice) + if self.supplied_secrets is not None: + result["suppliedSecrets"] = from_union([lambda x: from_list(from_str, x), from_none], self.supplied_secrets) + if self.installation_id is not None: + result["installationId"] = from_union([from_str, from_none], self.installation_id) + if self.owned_secret_count is not None: + result["ownedSecretCount"] = from_union([from_int, from_none], self.owned_secret_count) + if self.preserves_shared_authentication is not None: + result["preservesSharedAuthentication"] = from_union([from_bool, from_none], self.preserves_shared_authentication) + if self.restores_previous_configuration is not None: + result["restoresPreviousConfiguration"] = from_union([from_bool, from_none], self.restores_previous_configuration) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CommandList: @@ -35218,6 +40157,57 @@ def to_dict(self) -> dict: result["catalog"] = from_union([lambda x: to_class(ConnectorCatalogResult, x), from_none], self.catalog) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginStep: + """The first step of the flow. + + 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.""" + + 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) -> 'AuthLoginStep': + 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) + + 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) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CanvasProviderCloseRequest: @@ -35373,6 +40363,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: @@ -35411,6 +40439,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: @@ -35470,161 +40560,311 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryRunSummary: - """Durable factory run summary with read-time live overlays.""" +class MCPPromptsListResult: + """One page of prompts advertised by the named MCP server.""" - can_resume: bool - """Whether the durable run state currently passes runtime resume eligibility checks.""" + prompts: list[MCPPrompt] + """Prompts advertised by the server""" - consumed: FactoryRunConsumed - """Durable resource consumption.""" + meta: dict[str, Any] | None = None + """MCP result metadata""" - created_at: int - """Epoch milliseconds when the run was created.""" + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard result fields""" - declared_limits: FactoryDeclaredLimits - """Resource ceilings declared by the factory.""" + next_cursor: str | None = None + """Opaque cursor for the next page, if the server has more prompts""" - declared_phase_count: int - """Number of phases declared by the factory.""" + @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) - description: str - """Human-readable factory description.""" + 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 - factory_name: str - """Registered factory name.""" +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _GlobalStateLoadResult: + """The host's machine-wide state. Every field is optional because a fresh install has + recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a + negative answer. Stored credentials are deliberately absent from this shape. + """ + app_install_nudge_responded: bool | None = None + """Whether the user has answered the prompt suggesting they install the desktop app.""" - live_agent_count: int - """Number of direct factory agents currently live.""" + app_tip_shown: bool | None = None + """Whether the app tip has been shown.""" - observed_at: int - """Epoch milliseconds when this live-overlay snapshot was observed.""" + asked_setup_terminals: list[str] | None = None + """Terminals the user has already been asked to set up, so the host does not ask twice.""" - revision: int - """Monotonic durable run revision.""" + auto_feedback_last_prompted_at: str | None = None + """When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the + once-per-day cap for non-staff users across restarts. + """ + first_launch_at: str | None = None + """When the host first ran on this machine.""" - run_id: str - """Factory run identifier.""" + installed_plugins: list[InstalledPlugin] | None = None + """Plugins installed on this machine.""" - status: RunStatus - """Current factory run status.""" + last_logged_in_user: _LoggedInUser | None = None + """Account used for the most recent sign-in.""" - total_spawned_agent_count: int - """Total direct factory agents spawned across all attempts.""" + logged_in_users: list[_LoggedInUser] | None = None + """Every account the host has signed in to on this machine.""" - updated_at: int - """Epoch milliseconds when the durable run was last updated.""" + reasoning_summaries_cleanup_done: bool | None = None + """Whether the one-off cleanup of stored reasoning summaries has run.""" - active_segment_started_at: int | None = None - """Epoch milliseconds when the current active segment started, or null while inactive.""" + recent_model_ids: list[str] | None = None + """Models the user selected recently, most recent first.""" - approved: FactoryDeclaredLimits | None = None - """Approved effective resource ceilings, or null until approved.""" + sandbox_credential_proxy_ca_declined: bool | None = None + """Whether the user declined to trust the sandbox credential proxy CA.""" - completed_at: int | None = None - """Epoch milliseconds when the run completed, or null while nonterminal.""" + sandbox_onboarding_shown: bool | None = None + """Whether the sandbox onboarding has been shown.""" - current_phase: FactoryCurrentPhase | None = None - """Current phase identity, or null before any phase is entered.""" + staff: bool | None = None + """Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only + behavior. + """ + staff_github: bool | None = None + """Whether the user was recognized as GitHub staff.""" - started_at: int | None = None - """Epoch milliseconds when execution first started, or null before start.""" + staff_log_level_migration_at: str | None = None + """When the staff-only log level migration last ran.""" - terminal: FactoryRunTerminal | None = None - """Terminal run outcome, or null while nonterminal.""" + staff_microsoft: bool | None = None + """Whether the user was recognized as Microsoft staff.""" + + staff_model_reset_at: str | None = None + """When the staff-only model reset last ran.""" + + staff_update_channel_migration_at: str | None = None + """When the staff-only update channel migration last ran.""" + + suppress_init_folders: list[str] | None = None + """Folders where the user declined the init prompt, so it stays hidden there.""" + + trusted_folders: list[str] | None = None + """Folders the user has marked as trusted.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryRunSummary': + def from_dict(obj: Any) -> '_GlobalStateLoadResult': assert isinstance(obj, dict) - can_resume = from_bool(obj.get("canResume")) - consumed = FactoryRunConsumed.from_dict(obj.get("consumed")) - created_at = from_int(obj.get("createdAt")) - declared_limits = FactoryDeclaredLimits.from_dict(obj.get("declaredLimits")) - declared_phase_count = from_int(obj.get("declaredPhaseCount")) - description = from_str(obj.get("description")) - factory_name = from_str(obj.get("factoryName")) - live_agent_count = from_int(obj.get("liveAgentCount")) - observed_at = from_int(obj.get("observedAt")) - revision = from_int(obj.get("revision")) - run_id = from_str(obj.get("runId")) - status = RunStatus(obj.get("status")) - total_spawned_agent_count = from_int(obj.get("totalSpawnedAgentCount")) - updated_at = from_int(obj.get("updatedAt")) - active_segment_started_at = from_union([from_int, from_none], obj.get("activeSegmentStartedAt")) - approved = from_union([FactoryDeclaredLimits.from_dict, from_none], obj.get("approved")) - completed_at = from_union([from_int, from_none], obj.get("completedAt")) - current_phase = from_union([FactoryCurrentPhase.from_dict, from_none], obj.get("currentPhase")) - started_at = from_union([from_int, from_none], obj.get("startedAt")) - terminal = from_union([FactoryRunTerminal.from_dict, from_none], obj.get("terminal")) - return FactoryRunSummary(can_resume, consumed, created_at, declared_limits, declared_phase_count, description, factory_name, live_agent_count, observed_at, revision, run_id, status, total_spawned_agent_count, updated_at, active_segment_started_at, approved, completed_at, current_phase, started_at, terminal) + app_install_nudge_responded = from_union([from_bool, from_none], obj.get("appInstallNudgeResponded")) + app_tip_shown = from_union([from_bool, from_none], obj.get("appTipShown")) + asked_setup_terminals = from_union([lambda x: from_list(from_str, x), from_none], obj.get("askedSetupTerminals")) + auto_feedback_last_prompted_at = from_union([from_str, from_none], obj.get("autoFeedbackLastPromptedAt")) + first_launch_at = from_union([from_str, from_none], obj.get("firstLaunchAt")) + installed_plugins = from_union([lambda x: from_list(InstalledPlugin.from_dict, x), from_none], obj.get("installedPlugins")) + last_logged_in_user = from_union([_LoggedInUser.from_dict, from_none], obj.get("lastLoggedInUser")) + logged_in_users = from_union([lambda x: from_list(_LoggedInUser.from_dict, x), from_none], obj.get("loggedInUsers")) + reasoning_summaries_cleanup_done = from_union([from_bool, from_none], obj.get("reasoningSummariesCleanupDone")) + recent_model_ids = from_union([lambda x: from_list(from_str, x), from_none], obj.get("recentModelIds")) + sandbox_credential_proxy_ca_declined = from_union([from_bool, from_none], obj.get("sandboxCredentialProxyCaDeclined")) + sandbox_onboarding_shown = from_union([from_bool, from_none], obj.get("sandboxOnboardingShown")) + staff = from_union([from_bool, from_none], obj.get("staff")) + staff_github = from_union([from_bool, from_none], obj.get("staffGithub")) + staff_log_level_migration_at = from_union([from_str, from_none], obj.get("staffLogLevelMigrationAt")) + staff_microsoft = from_union([from_bool, from_none], obj.get("staffMicrosoft")) + staff_model_reset_at = from_union([from_str, from_none], obj.get("staffModelResetAt")) + staff_update_channel_migration_at = from_union([from_str, from_none], obj.get("staffUpdateChannelMigrationAt")) + suppress_init_folders = from_union([lambda x: from_list(from_str, x), from_none], obj.get("suppressInitFolders")) + trusted_folders = from_union([lambda x: from_list(from_str, x), from_none], obj.get("trustedFolders")) + return _GlobalStateLoadResult(app_install_nudge_responded, app_tip_shown, asked_setup_terminals, auto_feedback_last_prompted_at, first_launch_at, installed_plugins, last_logged_in_user, logged_in_users, reasoning_summaries_cleanup_done, recent_model_ids, sandbox_credential_proxy_ca_declined, sandbox_onboarding_shown, staff, staff_github, staff_log_level_migration_at, staff_microsoft, staff_model_reset_at, staff_update_channel_migration_at, suppress_init_folders, trusted_folders) + + def to_dict(self) -> dict: + result: dict = {} + if self.app_install_nudge_responded is not None: + result["appInstallNudgeResponded"] = from_union([from_bool, from_none], self.app_install_nudge_responded) + if self.app_tip_shown is not None: + result["appTipShown"] = from_union([from_bool, from_none], self.app_tip_shown) + if self.asked_setup_terminals is not None: + result["askedSetupTerminals"] = from_union([lambda x: from_list(from_str, x), from_none], self.asked_setup_terminals) + if self.auto_feedback_last_prompted_at is not None: + result["autoFeedbackLastPromptedAt"] = from_union([from_str, from_none], self.auto_feedback_last_prompted_at) + if self.first_launch_at is not None: + result["firstLaunchAt"] = from_union([from_str, from_none], self.first_launch_at) + if self.installed_plugins is not None: + result["installedPlugins"] = from_union([lambda x: from_list(lambda x: to_class(InstalledPlugin, x), x), from_none], self.installed_plugins) + if self.last_logged_in_user is not None: + result["lastLoggedInUser"] = from_union([lambda x: to_class(_LoggedInUser, x), from_none], self.last_logged_in_user) + if self.logged_in_users is not None: + result["loggedInUsers"] = from_union([lambda x: from_list(lambda x: to_class(_LoggedInUser, x), x), from_none], self.logged_in_users) + if self.reasoning_summaries_cleanup_done is not None: + result["reasoningSummariesCleanupDone"] = from_union([from_bool, from_none], self.reasoning_summaries_cleanup_done) + if self.recent_model_ids is not None: + result["recentModelIds"] = from_union([lambda x: from_list(from_str, x), from_none], self.recent_model_ids) + if self.sandbox_credential_proxy_ca_declined is not None: + result["sandboxCredentialProxyCaDeclined"] = from_union([from_bool, from_none], self.sandbox_credential_proxy_ca_declined) + if self.sandbox_onboarding_shown is not None: + result["sandboxOnboardingShown"] = from_union([from_bool, from_none], self.sandbox_onboarding_shown) + if self.staff is not None: + result["staff"] = from_union([from_bool, from_none], self.staff) + if self.staff_github is not None: + result["staffGithub"] = from_union([from_bool, from_none], self.staff_github) + if self.staff_log_level_migration_at is not None: + result["staffLogLevelMigrationAt"] = from_union([from_str, from_none], self.staff_log_level_migration_at) + if self.staff_microsoft is not None: + result["staffMicrosoft"] = from_union([from_bool, from_none], self.staff_microsoft) + if self.staff_model_reset_at is not None: + result["staffModelResetAt"] = from_union([from_str, from_none], self.staff_model_reset_at) + if self.staff_update_channel_migration_at is not None: + result["staffUpdateChannelMigrationAt"] = from_union([from_str, from_none], self.staff_update_channel_migration_at) + if self.suppress_init_folders is not None: + result["suppressInitFolders"] = from_union([lambda x: from_list(from_str, x), from_none], self.suppress_init_folders) + if self.trusted_folders is not None: + result["trustedFolders"] = from_union([lambda x: from_list(from_str, x), from_none], self.trusted_folders) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsSetAdditionalPluginsRequest: + """Manager-wide additional plugins to register; replaces any previously-configured set.""" + + plugins: list[InstalledPlugin] + """Manager-wide additional plugins to register. Replaces any previously-configured set. Pass + an empty array to clear. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionsSetAdditionalPluginsRequest': + assert isinstance(obj, dict) + plugins = from_list(InstalledPlugin.from_dict, obj.get("plugins")) + return SessionsSetAdditionalPluginsRequest(plugins) def to_dict(self) -> dict: result: dict = {} - result["canResume"] = from_bool(self.can_resume) - result["consumed"] = to_class(FactoryRunConsumed, self.consumed) - result["createdAt"] = from_int(self.created_at) - result["declaredLimits"] = to_class(FactoryDeclaredLimits, self.declared_limits) - result["declaredPhaseCount"] = from_int(self.declared_phase_count) - result["description"] = from_str(self.description) - result["factoryName"] = from_str(self.factory_name) - result["liveAgentCount"] = from_int(self.live_agent_count) - result["observedAt"] = from_int(self.observed_at) - result["revision"] = from_int(self.revision) - result["runId"] = from_str(self.run_id) - result["status"] = to_enum(RunStatus, self.status) - result["totalSpawnedAgentCount"] = from_int(self.total_spawned_agent_count) - result["updatedAt"] = from_int(self.updated_at) - result["activeSegmentStartedAt"] = from_union([from_int, from_none], self.active_segment_started_at) - result["approved"] = from_union([lambda x: to_class(FactoryDeclaredLimits, x), from_none], self.approved) - result["completedAt"] = from_union([from_int, from_none], self.completed_at) - result["currentPhase"] = from_union([lambda x: to_class(FactoryCurrentPhase, x), from_none], self.current_phase) - result["startedAt"] = from_union([from_int, from_none], self.started_at) - result["terminal"] = from_union([lambda x: to_class(FactoryRunTerminal, x), from_none], self.terminal) + result["plugins"] = from_list(lambda x: to_class(InstalledPlugin, x), self.plugins) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryResumeResult: - """Resolved persisted factory identity and resumed run envelope.""" +class WorkflowRunTerminal: + """Prompt-safe terminal workflow outcome.""" - factory_name: str - """Persisted factory name resolved for the resumed run.""" + error: str | None = None + """Human-readable terminal error.""" - run: FactoryRunResult - """Terminal resumed run envelope.""" + failure: WorkflowRunFailure | None = None + """Machine-readable terminal failure.""" + + pause_info: PauseInfoClass | None = None + """Pause initiator metadata, or null when the run did not pause.""" + + reason: str | None = None + """Human-readable terminal reason.""" + + result_preview: str | None = None + """Prompt-safe preview of the completed result.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryResumeResult': + def from_dict(obj: Any) -> 'WorkflowRunTerminal': assert isinstance(obj, dict) - factory_name = from_str(obj.get("factoryName")) - run = FactoryRunResult.from_dict(obj.get("run")) - return FactoryResumeResult(factory_name, run) + error = from_union([from_str, from_none], obj.get("error")) + failure = from_union([WorkflowRunFailure.from_dict, from_none], obj.get("failure")) + pause_info = from_union([PauseInfoClass.from_dict, from_none], obj.get("pauseInfo")) + reason = from_union([from_str, from_none], obj.get("reason")) + result_preview = from_union([from_str, from_none], obj.get("resultPreview")) + return WorkflowRunTerminal(error, failure, pause_info, reason, result_preview) def to_dict(self) -> dict: result: dict = {} - result["factoryName"] = from_str(self.factory_name) - result["run"] = to_class(FactoryRunResult, self.run) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.failure is not None: + result["failure"] = from_union([lambda x: to_class(WorkflowRunFailure, x), from_none], self.failure) + result["pauseInfo"] = from_union([lambda x: to_class(PauseInfoClass, x), from_none], self.pause_info) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + if self.result_preview is not None: + result["resultPreview"] = from_union([from_str, from_none], self.result_preview) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionsSetAdditionalPluginsRequest: - """Manager-wide additional plugins to register; replaces any previously-configured set.""" +class WorkflowRunResult: + """Terminal resumed run envelope. - plugins: list[InstalledPlugin] - """Manager-wide additional plugins to register. Replaces any previously-configured set. Pass - an empty array to clear. + Complete current or terminal workflow run envelope. """ + run_id: str + """Workflow run identifier.""" + + status: WorkflowRunStatus + """Current or terminal workflow run status.""" + + attempt: int | None = None + """One-based execution attempt represented by this envelope. Absent before the first attempt + starts or when returned by an older runtime. + """ + error: str | None = None + """Error message for an errored run.""" + + failure: WorkflowRunFailure | None = None + """Machine-readable failure details for a halted or errored run.""" + + pause_info: WorkflowPauseInfo | None = None + """Structured pause initiator metadata for a paused attempt.""" + + reason: str | None = None + """Reason for a halted or cancelled run.""" + + result: Any = None + """Completed workflow result.""" + + snapshot: Any = None + """Partial journal and progress snapshot for a halted, cancelled, or errored run.""" @staticmethod - def from_dict(obj: Any) -> 'SessionsSetAdditionalPluginsRequest': + def from_dict(obj: Any) -> 'WorkflowRunResult': assert isinstance(obj, dict) - plugins = from_list(InstalledPlugin.from_dict, obj.get("plugins")) - return SessionsSetAdditionalPluginsRequest(plugins) + run_id = from_str(obj.get("runId")) + status = WorkflowRunStatus(obj.get("status")) + attempt = from_union([from_int, from_none], obj.get("attempt")) + error = from_union([from_str, from_none], obj.get("error")) + failure = from_union([WorkflowRunFailure.from_dict, from_none], obj.get("failure")) + pause_info = from_union([WorkflowPauseInfo.from_dict, from_none], obj.get("pauseInfo")) + reason = from_union([from_str, from_none], obj.get("reason")) + result = obj.get("result") + snapshot = obj.get("snapshot") + return WorkflowRunResult(run_id, status, attempt, error, failure, pause_info, reason, result, snapshot) def to_dict(self) -> dict: result: dict = {} - result["plugins"] = from_list(lambda x: to_class(InstalledPlugin, x), self.plugins) + result["runId"] = from_str(self.run_id) + result["status"] = to_enum(WorkflowRunStatus, self.status) + if self.attempt is not None: + result["attempt"] = from_union([from_int, from_none], self.attempt) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.failure is not None: + result["failure"] = from_union([lambda x: to_class(WorkflowRunFailure, x), from_none], self.failure) + if self.pause_info is not None: + result["pauseInfo"] = from_union([lambda x: to_class(WorkflowPauseInfo, x), from_none], self.pause_info) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + if self.result is not None: + result["result"] = self.result + if self.snapshot is not None: + result["snapshot"] = self.snapshot return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -35883,6 +41123,210 @@ def to_dict(self) -> dict: result["checkpoints"] = from_list(lambda x: to_class(WorkspacesCheckpoints, x), self.checkpoints) return result +@dataclass +class OperationClass: + """Known original operation identity, returned before callback or effects. + + Inert, runtime-owned admission. The operation ID is known before confirmation or + effects. + + Original-connection operation snapshot. + + Status snapshot from the original connection, independent of new-work account + availability. + """ + operation_id: str + """Original connection-owned operation, known before the first confirmation callback. + + Original runtime-issued operation identity. + """ + expires_at_epoch_ms: int | None = None + """Original plan expiry in Unix epoch milliseconds; preparation does not extend it.""" + + cancellation_requested: bool | None = None + """Whether cancellation has been requested, not proof that a write was undone. + + Whether the inert prepared operation was asked to cancel. + + Whether the pending human callback was asked to cancel. + + Whether source or authority revalidation was asked to cancel. + + Whether applying was asked to cancel; already-started effects retain their lease. + + Whether cancellation was requested before the terminal result. + """ + phase: Phase | None = None + """Original operation progress discriminator.""" + + outcome: MCPInstallationOutcome | None = None + """Immutable terminal receipt.""" + + @staticmethod + def from_dict(obj: Any) -> 'OperationClass': + assert isinstance(obj, dict) + operation_id = from_str(obj.get("operationId")) + expires_at_epoch_ms = from_union([from_int, from_none], obj.get("expiresAtEpochMs")) + cancellation_requested = from_union([from_bool, from_none], obj.get("cancellationRequested")) + phase = from_union([Phase, from_none], obj.get("phase")) + outcome = from_union([MCPInstallationOutcome.from_dict, from_none], obj.get("outcome")) + return OperationClass(operation_id, expires_at_epoch_ms, cancellation_requested, phase, outcome) + + def to_dict(self) -> dict: + result: dict = {} + result["operationId"] = from_str(self.operation_id) + if self.expires_at_epoch_ms is not None: + result["expiresAtEpochMs"] = from_union([from_int, from_none], self.expires_at_epoch_ms) + if self.cancellation_requested is not None: + result["cancellationRequested"] = from_union([from_bool, from_none], self.cancellation_requested) + if self.phase is not None: + result["phase"] = from_union([lambda x: to_enum(Phase, x), from_none], self.phase) + if self.outcome is not None: + result["outcome"] = from_union([lambda x: to_class(MCPInstallationOutcome, x), from_none], self.outcome) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPInstallationOperationStatus: + """Original-connection operation snapshot. + + Status snapshot from the original connection, independent of new-work account + availability. + """ + cancellation_requested: bool + """Whether cancellation has been requested, not proof that a write was undone. + + Whether the inert prepared operation was asked to cancel. + + Whether the pending human callback was asked to cancel. + + Whether source or authority revalidation was asked to cancel. + + Whether applying was asked to cancel; already-started effects retain their lease. + + Whether cancellation was requested before the terminal result. + """ + operation_id: str + """Original runtime-issued operation identity.""" + + phase: Phase + """Original operation progress discriminator.""" + + outcome: MCPInstallationOutcome | None = None + """Immutable terminal receipt.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationOperationStatus': + assert isinstance(obj, dict) + cancellation_requested = from_bool(obj.get("cancellationRequested")) + operation_id = from_str(obj.get("operationId")) + phase = Phase(obj.get("phase")) + outcome = from_union([MCPInstallationOutcome.from_dict, from_none], obj.get("outcome")) + return MCPInstallationOperationStatus(cancellation_requested, operation_id, phase, outcome) + + def to_dict(self) -> dict: + result: dict = {} + result["cancellationRequested"] = from_bool(self.cancellation_requested) + result["operationId"] = from_str(self.operation_id) + result["phase"] = to_enum(Phase, self.phase) + if self.outcome is not None: + result["outcome"] = from_union([lambda x: to_class(MCPInstallationOutcome, x), from_none], self.outcome) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPInstallationResult: + """An installation result together with the exact honoured contract, or a negotiation + refusal. + + The caller's protocol version or required capabilities cannot be honoured. Returned + instead of a partial or ambiguous success. + + The request was rejected because a bounded field fell outside its permitted range or a + required field was unusable. Pagination may also be rejected by the authority after a + continuation request; repeat the search without page. + """ + kind: MCPInstallationManagementResultKind + """A handled operation outcome with an honoured contract. + + Discriminator: capability or protocol-version negotiation failed + + Discriminator: the request itself was invalid + """ + negotiated: CatalogNegotiatedContract | None = None + """Capabilities actually honoured for this request.""" + + outcome: MCPInstallationOutcome | None = None + """Terminal result of the original operation.""" + + message: str | None = None + """Human-readable explanation, safe to surface. Never contains a query, URL, handle, or + secret. + + Human-readable explanation, safe to surface. Never echoes the offending value, nor a + query, URL, handle, or secret. + """ + minimum_supported_protocol_version: int | None = None + """Lowest caller protocol version this runtime will serve.""" + + reason: CatalogNegotiationRefusedReason | None = None + """Whether the version or the capability set was the problem.""" + + runtime_protocol_version: int | None = None + """Protocol version of the runtime that refused the request.""" + + supported_capabilities: list[str] | None = None + """Capabilities this runtime can safely advertise to this caller. The complete + five-capability protocol-3 legacy set is always present; every capability added after + that baseline appears only when the caller required it, so an older closed-enum decoder + can still consume a refusal. This list does not imply that every deployment has enabled + every operation. + """ + unsupported_capabilities: list[str] | None = None + """The subset of the caller's bounded extensible capability identifiers this runtime cannot + honour. + """ + field: CatalogInvalidRequestField | None = None + """Which request field was rejected.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationResult': + assert isinstance(obj, dict) + kind = MCPInstallationManagementResultKind(obj.get("kind")) + negotiated = from_union([CatalogNegotiatedContract.from_dict, from_none], obj.get("negotiated")) + outcome = from_union([MCPInstallationOutcome.from_dict, from_none], obj.get("outcome")) + message = from_union([from_str, from_none], obj.get("message")) + minimum_supported_protocol_version = from_union([from_int, from_none], obj.get("minimumSupportedProtocolVersion")) + reason = from_union([CatalogNegotiationRefusedReason, from_none], obj.get("reason")) + runtime_protocol_version = from_union([from_int, from_none], obj.get("runtimeProtocolVersion")) + supported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedCapabilities")) + unsupported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("unsupportedCapabilities")) + field = from_union([CatalogInvalidRequestField, from_none], obj.get("field")) + return MCPInstallationResult(kind, negotiated, outcome, message, minimum_supported_protocol_version, reason, runtime_protocol_version, supported_capabilities, unsupported_capabilities, field) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPInstallationManagementResultKind, self.kind) + if self.negotiated is not None: + result["negotiated"] = from_union([lambda x: to_class(CatalogNegotiatedContract, x), from_none], self.negotiated) + if self.outcome is not None: + result["outcome"] = from_union([lambda x: to_class(MCPInstallationOutcome, x), from_none], self.outcome) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.minimum_supported_protocol_version is not None: + result["minimumSupportedProtocolVersion"] = from_union([from_int, from_none], self.minimum_supported_protocol_version) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(CatalogNegotiationRefusedReason, x), from_none], self.reason) + if self.runtime_protocol_version is not None: + result["runtimeProtocolVersion"] = from_union([from_int, from_none], self.runtime_protocol_version) + if self.supported_capabilities is not None: + result["supportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_capabilities) + if self.unsupported_capabilities is not None: + result["unsupportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.unsupported_capabilities) + if self.field is not None: + result["field"] = from_union([lambda x: to_enum(CatalogInvalidRequestField, x), from_none], self.field) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsRequest: @@ -36012,6 +41456,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.""" @@ -36043,12 +41492,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 = {} @@ -36073,6 +41523,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: @@ -36085,6 +41537,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: @@ -36241,8 +41713,9 @@ class CatalogCandidate: must never be treated as an instruction, and the handle is the only way to refer to the candidate in a later operation. - An inert AI skill catalog result. AI skills are discovery-only and cannot be represented - as installable through this surface. + An inert AI skill catalog result. Verified Skill candidates may be installable only when + the runtime reports installability and the selected session is permitted to plan + installation. An inert Agent Plugin catalog result. Its canonical catalog identity, declared version, repository source claim, and explicit compatibility tags are safe to correlate, while its @@ -36290,10 +41763,11 @@ class CatalogCandidate: handle_expires_at: str | None = None """ISO 8601 timestamp after which the handle is stale and will be rejected.""" - installability: CatalogCandidateInstallability | None = None + installability: CatalogInstallability | None = None """Whether this MCP server can be planned for installation, and if policy prevents it. - AI skills are discovery-only and cannot be installed through this surface + Whether this AI skill candidate can be planned for verified installation in the selected + session. """ publisher: str | None = None """Publisher taken verbatim from the card. Inert untrusted text.""" @@ -36332,7 +41806,7 @@ def from_dict(obj: Any) -> 'CatalogCandidate': description = from_union([from_str, from_none], obj.get("description")) handle = from_union([from_str, from_none], obj.get("handle")) handle_expires_at = from_union([from_str, from_none], obj.get("handleExpiresAt")) - installability = from_union([CatalogCandidateInstallability, from_none], obj.get("installability")) + installability = from_union([CatalogInstallability, from_none], obj.get("installability")) publisher = from_union([from_str, from_none], obj.get("publisher")) trust = from_union([_load_CatalogTrustSnapshot, from_none], obj.get("trust")) compatibility_tags = from_union([lambda x: from_list(CatalogAgentPluginCompatibilityTag, x), from_none], obj.get("compatibilityTags")) @@ -36354,7 +41828,7 @@ def to_dict(self) -> dict: if self.handle_expires_at is not None: result["handleExpiresAt"] = from_union([from_str, from_none], self.handle_expires_at) if self.installability is not None: - result["installability"] = from_union([lambda x: to_enum(CatalogCandidateInstallability, x), from_none], self.installability) + result["installability"] = from_union([lambda x: to_enum(CatalogInstallability, x), from_none], self.installability) if self.publisher is not None: result["publisher"] = from_union([from_str, from_none], self.publisher) if self.trust is not None: @@ -36468,8 +41942,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.""" @@ -36499,6 +41976,13 @@ class SandboxConfig: auth: SandboxConfigAuth | None = None """Credential-injection capability flags.""" + credentials: SandboxCredentialsConfig | None = None + """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. + """ # Internal: this field is an internal SDK API and is not part of the public surface. managed_lsp_routing_locked: bool | None = None """The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`.""" @@ -36533,12 +42017,13 @@ def from_dict(obj: Any) -> 'SandboxConfig': allow_bypass = from_union([from_bool, from_none], obj.get("allowBypass")) allow_dev_tool_access = from_union([from_bool, from_none], obj.get("allowDevToolAccess")) auth = from_union([SandboxConfigAuth.from_dict, from_none], obj.get("auth")) + credentials = from_union([SandboxCredentialsConfig.from_dict, from_none], obj.get("credentials")) managed_lsp_routing_locked = from_union([from_bool, from_none], obj.get("managedLspRoutingLocked")) managed_mcp_routing_locked = from_union([from_bool, from_none], obj.get("managedMcpRoutingLocked")) sandbox_lsp_servers = from_union([from_bool, from_none], obj.get("sandboxLspServers")) sandbox_mcp_servers = from_union([from_bool, from_none], obj.get("sandboxMcpServers")) user_policy = from_union([SandboxConfigUserPolicy.from_dict, from_none], obj.get("userPolicy")) - return SandboxConfig(enabled, add_current_working_directory, allow_bypass, allow_dev_tool_access, auth, managed_lsp_routing_locked, managed_mcp_routing_locked, sandbox_lsp_servers, sandbox_mcp_servers, user_policy) + return SandboxConfig(enabled, add_current_working_directory, allow_bypass, allow_dev_tool_access, auth, credentials, managed_lsp_routing_locked, managed_mcp_routing_locked, sandbox_lsp_servers, sandbox_mcp_servers, user_policy) def to_dict(self) -> dict: result: dict = {} @@ -36551,6 +42036,8 @@ def to_dict(self) -> dict: result["allowDevToolAccess"] = from_union([from_bool, from_none], self.allow_dev_tool_access) if self.auth is not None: result["auth"] = from_union([lambda x: to_class(SandboxConfigAuth, x), from_none], self.auth) + if self.credentials is not None: + result["credentials"] = from_union([lambda x: to_class(SandboxCredentialsConfig, x), from_none], self.credentials) if self.managed_lsp_routing_locked is not None: result["managedLspRoutingLocked"] = from_union([from_bool, from_none], self.managed_lsp_routing_locked) if self.managed_mcp_routing_locked is not None: @@ -36588,6 +42075,392 @@ def to_dict(self) -> dict: result["error"] = from_union([lambda x: to_class(SessionFSSqliteTransactionError, x), from_none], self.error) return result +@dataclass +class ReviewClass: + """The exact MCP action and its reviewed changes. + + Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. + + The exact verified Skill action and its reviewed files. + + Safe verified Skill review fields. No raw credential, candidate handle or plan handle. + + Safe review to present before applying the plan. + + Safe review to present before applying removal. + + Review for installing a verified Skill. + + Review for uninstalling an owned verified Skill. + """ + action: MCPInstallationReviewAction + """Exact reviewed installation action. + + Exact reviewed Skill installation action. + """ + catalogue: InstallationCatalogueIdentity | None = None + """Catalogue identity retained from the bound candidate when available. + + Catalogue identity retained from the bound candidate before consent. + + Catalogue identity retained at install time. + """ + catalogue_trust: CatalogTrustSnapshot | None = None + """Original catalogue trust metadata, not a verification claim.""" + + configuration_change: MCPPlanConfigurationChange | None = None + """The configuration change for the selected alternative only.""" + + effective_configuration: MCPInstallationRemoteConfiguration | None = None + """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. + """ + identity: MCPPlanResourceIdentity | None = None + """Identity from the retained plan, not caller display text. + + Identity from the installed receipt. + """ + inputs: list[MCPInstallationInput] | None = None + """Non-secret values supplied for this selected alternative.""" + + policy: MCPPlanPolicyResult | None = None + """Policy decision bound to this plan. + + Current removal policy, independent of permission to activate the server. + """ + provenance: MCPPlanProvenance | None = None + """Original source identity and content commitment. + + Source identity and content commitment retained by the installed receipt. + """ + secret_storage: MCPInstallationSecretStorage | None = None + """Explicit reviewed backend selection; no backend is accessed when no secrets are supplied.""" + + selected_choice: MCPPlanTransportChoice | None = None + """Only the selected alternative is applied.""" + + supplied_secrets: list[str] | None = None + """Exact reviewed placeholders supplied separately. Never secret values.""" + + target: MCPPlanTarget | None = None + """Exact reviewed user-scope destination. + + Exact destination, checked for intervening changes before mutation. + + Exact user-scope target location without an absolute host path. + """ + installation_id: str | None = None + """Receipt-owned installation being removed.""" + + owned_secret_count: int | None = None + """Exact planner-owned secret slots to remove, excluding shared OAuth grants.""" + + preserves_shared_authentication: bool | None = None + """Shared profile authentication is deliberately retained, not pending cleanup.""" + + restores_previous_configuration: bool | None = None + """Whether uninstall restores a protected pre-install configuration.""" + + description: str | None = None + """Skill description from SKILL.md when present.""" + + entrypoint_content: str | None = None + """Complete verified SKILL.md content. Planning refuses with review-too-large + when this exceeds 262144 UTF-8 bytes; it is never truncated. + """ + entrypoint_path: str | None = None + """Relative path of the verified Skill entrypoint.""" + + files: list[SkillInstallationFileReview] | None = None + """Reviewed files and digests. + + Files recorded by the installation receipt. + """ + installs_disabled: bool | None = None + """Installing never grants immediate use; the Skill is written disabled.""" + + name: str | None = None + """Skill invocation name from SKILL.md.""" + + source: SkillInstallationSource | None = None + """Exact verified source identity.""" + + total_bytes: int | None = None + """Total reviewed payload size in bytes. + + Total receipt-owned payload size in bytes. + """ + files_modified: bool | None = None + """Whether current files differ from the receipt. Apply refuses drift.""" + + installation: SkillInstallationSummary | None = None + """Owned installation being removed.""" + + @staticmethod + def from_dict(obj: Any) -> 'ReviewClass': + assert isinstance(obj, dict) + action = MCPInstallationReviewAction(obj.get("action")) + catalogue = from_union([InstallationCatalogueIdentity.from_dict, from_none], obj.get("catalogue")) + catalogue_trust = from_union([_load_CatalogTrustSnapshot, from_none], obj.get("catalogueTrust")) + configuration_change = from_union([MCPPlanConfigurationChange.from_dict, from_none], obj.get("configurationChange")) + effective_configuration = from_union([MCPInstallationRemoteConfiguration.from_dict, from_none], obj.get("effectiveConfiguration")) + identity = from_union([MCPPlanResourceIdentity.from_dict, from_none], obj.get("identity")) + inputs = from_union([lambda x: from_list(MCPInstallationInput.from_dict, x), from_none], obj.get("inputs")) + policy = from_union([MCPPlanPolicyResult.from_dict, from_none], obj.get("policy")) + provenance = from_union([MCPPlanProvenance.from_dict, from_none], obj.get("provenance")) + secret_storage = from_union([MCPInstallationSecretStorage, from_none], obj.get("secretStorage")) + selected_choice = from_union([_load_MCPPlanTransportChoice, from_none], obj.get("selectedChoice")) + supplied_secrets = from_union([lambda x: from_list(from_str, x), from_none], obj.get("suppliedSecrets")) + target = from_union([MCPPlanTarget.from_dict, from_none], obj.get("target")) + installation_id = from_union([from_str, from_none], obj.get("installationId")) + owned_secret_count = from_union([from_int, from_none], obj.get("ownedSecretCount")) + preserves_shared_authentication = from_union([from_bool, from_none], obj.get("preservesSharedAuthentication")) + restores_previous_configuration = from_union([from_bool, from_none], obj.get("restoresPreviousConfiguration")) + description = from_union([from_str, from_none], obj.get("description")) + entrypoint_content = from_union([from_str, from_none], obj.get("entrypointContent")) + entrypoint_path = from_union([from_str, from_none], obj.get("entrypointPath")) + files = from_union([lambda x: from_list(SkillInstallationFileReview.from_dict, x), from_none], obj.get("files")) + installs_disabled = from_union([from_bool, from_none], obj.get("installsDisabled")) + name = from_union([from_str, from_none], obj.get("name")) + source = from_union([SkillInstallationSource.from_dict, from_none], obj.get("source")) + total_bytes = from_union([from_int, from_none], obj.get("totalBytes")) + files_modified = from_union([from_bool, from_none], obj.get("filesModified")) + installation = from_union([SkillInstallationSummary.from_dict, from_none], obj.get("installation")) + return ReviewClass(action, catalogue, catalogue_trust, configuration_change, effective_configuration, identity, inputs, policy, provenance, secret_storage, selected_choice, supplied_secrets, target, installation_id, owned_secret_count, preserves_shared_authentication, restores_previous_configuration, description, entrypoint_content, entrypoint_path, files, installs_disabled, name, source, total_bytes, files_modified, installation) + + def to_dict(self) -> dict: + result: dict = {} + result["action"] = to_enum(MCPInstallationReviewAction, self.action) + if self.catalogue is not None: + result["catalogue"] = from_union([lambda x: to_class(InstallationCatalogueIdentity, x), from_none], self.catalogue) + if self.catalogue_trust is not None: + result["catalogueTrust"] = from_union([lambda x: (x).to_dict(), from_none], self.catalogue_trust) + if self.configuration_change is not None: + result["configurationChange"] = from_union([lambda x: to_class(MCPPlanConfigurationChange, x), from_none], self.configuration_change) + if self.effective_configuration is not None: + result["effectiveConfiguration"] = from_union([lambda x: to_class(MCPInstallationRemoteConfiguration, x), from_none], self.effective_configuration) + if self.identity is not None: + result["identity"] = from_union([lambda x: to_class(MCPPlanResourceIdentity, x), from_none], self.identity) + if self.inputs is not None: + result["inputs"] = from_union([lambda x: from_list(lambda x: to_class(MCPInstallationInput, x), x), from_none], self.inputs) + if self.policy is not None: + result["policy"] = from_union([lambda x: to_class(MCPPlanPolicyResult, x), from_none], self.policy) + if self.provenance is not None: + result["provenance"] = from_union([lambda x: to_class(MCPPlanProvenance, x), from_none], self.provenance) + if self.secret_storage is not None: + result["secretStorage"] = from_union([lambda x: to_enum(MCPInstallationSecretStorage, x), from_none], self.secret_storage) + if self.selected_choice is not None: + result["selectedChoice"] = from_union([lambda x: (x).to_dict(), from_none], self.selected_choice) + if self.supplied_secrets is not None: + result["suppliedSecrets"] = from_union([lambda x: from_list(from_str, x), from_none], self.supplied_secrets) + if self.target is not None: + result["target"] = from_union([lambda x: to_class(MCPPlanTarget, x), from_none], self.target) + if self.installation_id is not None: + result["installationId"] = from_union([from_str, from_none], self.installation_id) + if self.owned_secret_count is not None: + result["ownedSecretCount"] = from_union([from_int, from_none], self.owned_secret_count) + if self.preserves_shared_authentication is not None: + result["preservesSharedAuthentication"] = from_union([from_bool, from_none], self.preserves_shared_authentication) + if self.restores_previous_configuration is not None: + result["restoresPreviousConfiguration"] = from_union([from_bool, from_none], self.restores_previous_configuration) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.entrypoint_content is not None: + result["entrypointContent"] = from_union([from_str, from_none], self.entrypoint_content) + if self.entrypoint_path is not None: + result["entrypointPath"] = from_union([from_str, from_none], self.entrypoint_path) + if self.files is not None: + result["files"] = from_union([lambda x: from_list(lambda x: to_class(SkillInstallationFileReview, x), x), from_none], self.files) + if self.installs_disabled is not None: + result["installsDisabled"] = from_union([from_bool, from_none], self.installs_disabled) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.source is not None: + result["source"] = from_union([lambda x: to_class(SkillInstallationSource, x), from_none], self.source) + if self.total_bytes is not None: + result["totalBytes"] = from_union([from_int, from_none], self.total_bytes) + if self.files_modified is not None: + result["filesModified"] = from_union([from_bool, from_none], self.files_modified) + if self.installation is not None: + result["installation"] = from_union([lambda x: to_class(SkillInstallationSummary, x), from_none], self.installation) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationReview: + """The exact verified Skill action and its reviewed files. + + Safe verified Skill review fields. No raw credential, candidate handle or plan handle. + + Safe review to present before applying the plan. + + Safe review to present before applying removal. + + Review for installing a verified Skill. + + Review for uninstalling an owned verified Skill. + """ + action: MCPInstallationReviewAction + """Exact reviewed Skill installation action.""" + + catalogue: InstallationCatalogueIdentity + """Catalogue identity retained from the bound candidate before consent. + + Catalogue identity retained at install time. + """ + files: list[SkillInstallationFileReview] + """Reviewed files and digests. + + Files recorded by the installation receipt. + """ + total_bytes: int + """Total reviewed payload size in bytes. + + Total receipt-owned payload size in bytes. + """ + description: str | None = None + """Skill description from SKILL.md when present.""" + + entrypoint_content: str | None = None + """Complete verified SKILL.md content. Planning refuses with review-too-large + when this exceeds 262144 UTF-8 bytes; it is never truncated. + """ + entrypoint_path: str | None = None + """Relative path of the verified Skill entrypoint.""" + + installs_disabled: bool | None = None + """Installing never grants immediate use; the Skill is written disabled.""" + + name: str | None = None + """Skill invocation name from SKILL.md.""" + + source: SkillInstallationSource | None = None + """Exact verified source identity.""" + + target: SkillInstallationLocation | None = None + """Exact user-scope target location without an absolute host path.""" + + files_modified: bool | None = None + """Whether current files differ from the receipt. Apply refuses drift.""" + + installation: SkillInstallationSummary | None = None + """Owned installation being removed.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationReview': + assert isinstance(obj, dict) + action = MCPInstallationReviewAction(obj.get("action")) + catalogue = InstallationCatalogueIdentity.from_dict(obj.get("catalogue")) + files = from_list(SkillInstallationFileReview.from_dict, obj.get("files")) + total_bytes = from_int(obj.get("totalBytes")) + description = from_union([from_str, from_none], obj.get("description")) + entrypoint_content = from_union([from_str, from_none], obj.get("entrypointContent")) + entrypoint_path = from_union([from_str, from_none], obj.get("entrypointPath")) + installs_disabled = from_union([from_bool, from_none], obj.get("installsDisabled")) + name = from_union([from_str, from_none], obj.get("name")) + source = from_union([SkillInstallationSource.from_dict, from_none], obj.get("source")) + target = from_union([SkillInstallationLocation.from_dict, from_none], obj.get("target")) + files_modified = from_union([from_bool, from_none], obj.get("filesModified")) + installation = from_union([SkillInstallationSummary.from_dict, from_none], obj.get("installation")) + return SkillInstallationReview(action, catalogue, files, total_bytes, description, entrypoint_content, entrypoint_path, installs_disabled, name, source, target, files_modified, installation) + + def to_dict(self) -> dict: + result: dict = {} + result["action"] = to_enum(MCPInstallationReviewAction, self.action) + result["catalogue"] = to_class(InstallationCatalogueIdentity, self.catalogue) + result["files"] = from_list(lambda x: to_class(SkillInstallationFileReview, x), self.files) + result["totalBytes"] = from_int(self.total_bytes) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.entrypoint_content is not None: + result["entrypointContent"] = from_union([from_str, from_none], self.entrypoint_content) + if self.entrypoint_path is not None: + result["entrypointPath"] = from_union([from_str, from_none], self.entrypoint_path) + if self.installs_disabled is not None: + result["installsDisabled"] = from_union([from_bool, from_none], self.installs_disabled) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.source is not None: + result["source"] = from_union([lambda x: to_class(SkillInstallationSource, x), from_none], self.source) + if self.target is not None: + result["target"] = from_union([lambda x: to_class(SkillInstallationLocation, x), from_none], self.target) + if self.files_modified is not None: + result["filesModified"] = from_union([from_bool, from_none], self.files_modified) + if self.installation is not None: + result["installation"] = from_union([lambda x: to_class(SkillInstallationSummary, x), from_none], self.installation) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationOutcome: + """Immutable terminal receipt. + + Terminal verified Skill mutation result. + + Terminal operation outcome. + + The verified Skill was installed disabled. + + The owned Skill was removed. + + The durable transaction was aborted or fully compensated. + + A write may have completed. Recover and inspect durable state before retrying. + + The user declined the confirmation request. + + The operation was cancelled before a terminal mutation. + + The operation was refused without applying changes. + """ + kind: MCPInstallationOutcomeKind + """Terminal Skill installation outcome discriminator.""" + + installation: SkillInstallationSummary | None = None + """Durable installed Skill summary.""" + + installation_id: str | None = None + """Removed installation identity.""" + + operation_id: str | None = None + """Original removal operation identity. + + Original operation identity. + + Operation whose durable result must be recovered and inspected. + + Present once an operation has been allocated. + """ + reason: SkillInstallationFailureReason | None = None + """Cause of the fully aborted or compensated operation. + + Bounded refusal reason. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationOutcome': + assert isinstance(obj, dict) + kind = MCPInstallationOutcomeKind(obj.get("kind")) + installation = from_union([SkillInstallationSummary.from_dict, from_none], obj.get("installation")) + installation_id = from_union([from_str, from_none], obj.get("installationId")) + operation_id = from_union([from_str, from_none], obj.get("operationId")) + reason = from_union([SkillInstallationFailureReason, from_none], obj.get("reason")) + return SkillInstallationOutcome(kind, installation, installation_id, operation_id, reason) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPInstallationOutcomeKind, self.kind) + if self.installation is not None: + result["installation"] = from_union([lambda x: to_class(SkillInstallationSummary, x), from_none], self.installation) + if self.installation_id is not None: + result["installationId"] = from_union([from_str, from_none], self.installation_id) + if self.operation_id is not None: + result["operationId"] = from_union([from_str, from_none], self.operation_id) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(SkillInstallationFailureReason, x), from_none], self.reason) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TasksRegisterResult: @@ -36757,144 +42630,6 @@ def to_dict(self) -> dict: result["totalNanoAiu"] = from_union([to_float, from_none], self.total_nano_aiu) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class WorkflowRunSummary: - """Durable workflow run summary with read-time live overlays.""" - - can_resume: bool - """Whether the durable run state currently passes runtime resume eligibility checks.""" - - consumed: WorkflowRunConsumed - """Durable resource consumption.""" - - created_at: int - """Epoch milliseconds when the run was created.""" - - declared_limits: WorkflowDeclaredLimits - """Resource ceilings declared by the workflow.""" - - declared_phase_count: int - """Number of phases declared by the workflow.""" - - description: str - """Human-readable workflow description.""" - - live_agent_count: int - """Number of direct workflow agents currently live.""" - - observed_at: int - """Epoch milliseconds when this live-overlay snapshot was observed.""" - - revision: int - """Monotonic durable run revision.""" - - run_id: str - """Workflow run identifier.""" - - status: RunStatus - """Current workflow run status.""" - - total_spawned_agent_count: int - """Total direct workflow agents spawned across all attempts.""" - - updated_at: int - """Epoch milliseconds when the durable run was last updated.""" - - workflow_name: str - """Registered workflow name.""" - - active_segment_started_at: int | None = None - """Epoch milliseconds when the current active segment started, or null while inactive.""" - - approved: WorkflowDeclaredLimits | None = None - """Approved effective resource ceilings, or null until approved.""" - - completed_at: int | None = None - """Epoch milliseconds when the run completed, or null while nonterminal.""" - - current_phase: WorkflowCurrentPhase | None = None - """Current phase identity, or null before any phase is entered.""" - - started_at: int | None = None - """Epoch milliseconds when execution first started, or null before start.""" - - terminal: WorkflowRunTerminal | None = None - """Terminal run outcome, or null while nonterminal.""" - - @staticmethod - def from_dict(obj: Any) -> 'WorkflowRunSummary': - assert isinstance(obj, dict) - can_resume = from_bool(obj.get("canResume")) - consumed = WorkflowRunConsumed.from_dict(obj.get("consumed")) - created_at = from_int(obj.get("createdAt")) - declared_limits = WorkflowDeclaredLimits.from_dict(obj.get("declaredLimits")) - declared_phase_count = from_int(obj.get("declaredPhaseCount")) - description = from_str(obj.get("description")) - live_agent_count = from_int(obj.get("liveAgentCount")) - observed_at = from_int(obj.get("observedAt")) - revision = from_int(obj.get("revision")) - run_id = from_str(obj.get("runId")) - status = RunStatus(obj.get("status")) - total_spawned_agent_count = from_int(obj.get("totalSpawnedAgentCount")) - updated_at = from_int(obj.get("updatedAt")) - workflow_name = from_str(obj.get("workflowName")) - active_segment_started_at = from_union([from_int, from_none], obj.get("activeSegmentStartedAt")) - approved = from_union([WorkflowDeclaredLimits.from_dict, from_none], obj.get("approved")) - completed_at = from_union([from_int, from_none], obj.get("completedAt")) - current_phase = from_union([WorkflowCurrentPhase.from_dict, from_none], obj.get("currentPhase")) - started_at = from_union([from_int, from_none], obj.get("startedAt")) - terminal = from_union([WorkflowRunTerminal.from_dict, from_none], obj.get("terminal")) - return WorkflowRunSummary(can_resume, consumed, created_at, declared_limits, declared_phase_count, description, live_agent_count, observed_at, revision, run_id, status, total_spawned_agent_count, updated_at, workflow_name, active_segment_started_at, approved, completed_at, current_phase, started_at, terminal) - - def to_dict(self) -> dict: - result: dict = {} - result["canResume"] = from_bool(self.can_resume) - result["consumed"] = to_class(WorkflowRunConsumed, self.consumed) - result["createdAt"] = from_int(self.created_at) - result["declaredLimits"] = to_class(WorkflowDeclaredLimits, self.declared_limits) - result["declaredPhaseCount"] = from_int(self.declared_phase_count) - result["description"] = from_str(self.description) - result["liveAgentCount"] = from_int(self.live_agent_count) - result["observedAt"] = from_int(self.observed_at) - result["revision"] = from_int(self.revision) - result["runId"] = from_str(self.run_id) - result["status"] = to_enum(RunStatus, self.status) - result["totalSpawnedAgentCount"] = from_int(self.total_spawned_agent_count) - result["updatedAt"] = from_int(self.updated_at) - result["workflowName"] = from_str(self.workflow_name) - result["activeSegmentStartedAt"] = from_union([from_int, from_none], self.active_segment_started_at) - result["approved"] = from_union([lambda x: to_class(WorkflowDeclaredLimits, x), from_none], self.approved) - result["completedAt"] = from_union([from_int, from_none], self.completed_at) - result["currentPhase"] = from_union([lambda x: to_class(WorkflowCurrentPhase, x), from_none], self.current_phase) - result["startedAt"] = from_union([from_int, from_none], self.started_at) - result["terminal"] = from_union([lambda x: to_class(WorkflowRunTerminal, x), from_none], self.terminal) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class WorkflowResumeResult: - """Resolved persisted workflow identity and resumed run envelope.""" - - run: WorkflowRunResult - """Terminal resumed run envelope.""" - - workflow_name: str - """Persisted workflow name resolved for the resumed run.""" - - @staticmethod - def from_dict(obj: Any) -> 'WorkflowResumeResult': - assert isinstance(obj, dict) - run = WorkflowRunResult.from_dict(obj.get("run")) - workflow_name = from_str(obj.get("workflowName")) - return WorkflowResumeResult(run, workflow_name) - - def to_dict(self) -> dict: - result: dict = {} - result["run"] = to_class(WorkflowRunResult, self.run) - result["workflowName"] = from_str(self.workflow_name) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPConfigAddRequest: @@ -36975,19 +42710,24 @@ 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). """ + expected_installation_id: str | None = field(default=None, kw_only=True) + """Exact receipt identity for an explicit owned restart; configuration overrides are refused.""" @staticmethod def from_dict(obj: Any) -> 'MCPRestartServerRequest': assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) config = from_union([MCPSerializableServerConfig.from_dict, from_none], obj.get("config")) - return MCPRestartServerRequest(server_name, config) + expected_installation_id = from_union([from_str, from_none], obj.get("expectedInstallationId")) + return MCPRestartServerRequest(server_name, config, expected_installation_id=expected_installation_id) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) if self.config is not None: result["config"] = from_union([lambda x: to_class(MCPSerializableServerConfig, x), from_none], self.config) + if self.expected_installation_id is not None: + result["expectedInstallationId"] = from_union([from_str, from_none], self.expected_installation_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -37003,19 +42743,24 @@ 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). """ + expected_installation_id: str | None = field(default=None, kw_only=True) + """Exact receipt identity for explicit owned activation in this session.""" @staticmethod def from_dict(obj: Any) -> 'MCPStartServerRequest': assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) config = from_union([MCPSerializableServerConfig.from_dict, from_none], obj.get("config")) - return MCPStartServerRequest(server_name, config) + expected_installation_id = from_union([from_str, from_none], obj.get("expectedInstallationId")) + return MCPStartServerRequest(server_name, config, expected_installation_id=expected_installation_id) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) if self.config is not None: result["config"] = from_union([lambda x: to_class(MCPSerializableServerConfig, x), from_none], self.config) + if self.expected_installation_id is not None: + result["expectedInstallationId"] = from_union([from_str, from_none], self.expected_installation_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -37139,45 +42884,215 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class FactoryListRunsResult: - """A page of factory runs in durable creation order.""" +class AuthLoginBegun: + """A started login flow: its opaque id and first step.""" - runs: list[FactoryRunSummary] - """Factory run summaries in durable creation order.""" + flow_id: str + """Opaque flow id used to advance or cancel this login.""" - has_more_newer: bool | None = None - """Whether terminal runs newer than this page exist.""" + step: AuthLoginStep + """The first step of the flow.""" - newest_seq: int | None = None - """Newest terminal-run cursor in this page, or null when the terminal window is empty.""" + @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) - oldest_seq: int | None = None - """Oldest terminal-run cursor in this page, or null when the terminal window is empty.""" + def to_dict(self) -> dict: + result: dict = {} + result["flowId"] = from_str(self.flow_id) + result["step"] = to_class(AuthLoginStep, self.step) + return result - omitted_older: int | None = None - """Number of terminal runs older than this page.""" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class WorkflowRunSummary: + """Durable workflow run summary with read-time live overlays.""" + + can_resume: bool + """Whether the durable run state currently passes runtime resume eligibility checks.""" + + consumed: WorkflowRunConsumed + """Durable resource consumption.""" + + created_at: int + """Epoch milliseconds when the run was created.""" + + declared_limits: WorkflowDeclaredLimits + """Resource ceilings declared by the workflow.""" + + declared_phase_count: int + """Number of phases declared by the workflow.""" + + description: str + """Human-readable workflow description.""" + + live_agent_count: int + """Number of direct workflow agents currently live.""" + + observed_at: int + """Epoch milliseconds when this live-overlay snapshot was observed.""" + + revision: int + """Monotonic durable run revision.""" + + run_id: str + """Workflow run identifier.""" + + status: WorkflowRunStatus + """Current workflow run status.""" + + total_spawned_agent_count: int + """Total direct workflow agents spawned across all attempts.""" + + updated_at: int + """Epoch milliseconds when the durable run was last updated.""" + + workflow_name: str + """Registered workflow name.""" + + active_segment_started_at: int | None = None + """Epoch milliseconds when the current active segment started, or null while inactive.""" + + approved: WorkflowDeclaredLimits | None = None + """Approved effective resource ceilings, or null until approved.""" + + completed_at: int | None = None + """Epoch milliseconds when the run completed, or null while nonterminal.""" + + current_phase: WorkflowCurrentPhase | None = None + """Current phase identity, or null before any phase is entered.""" + + started_at: int | None = None + """Epoch milliseconds when execution first started, or null before start.""" + + terminal: WorkflowRunTerminal | None = None + """Terminal run outcome, or null while nonterminal.""" @staticmethod - def from_dict(obj: Any) -> 'FactoryListRunsResult': + def from_dict(obj: Any) -> 'WorkflowRunSummary': assert isinstance(obj, dict) - runs = from_list(FactoryRunSummary.from_dict, obj.get("runs")) - has_more_newer = from_union([from_bool, from_none], obj.get("hasMoreNewer")) - newest_seq = from_union([from_int, from_none], obj.get("newestSeq")) - oldest_seq = from_union([from_int, from_none], obj.get("oldestSeq")) - omitted_older = from_union([from_int, from_none], obj.get("omittedOlder")) - return FactoryListRunsResult(runs, has_more_newer, newest_seq, oldest_seq, omitted_older) + can_resume = from_bool(obj.get("canResume")) + consumed = WorkflowRunConsumed.from_dict(obj.get("consumed")) + created_at = from_int(obj.get("createdAt")) + declared_limits = WorkflowDeclaredLimits.from_dict(obj.get("declaredLimits")) + declared_phase_count = from_int(obj.get("declaredPhaseCount")) + description = from_str(obj.get("description")) + live_agent_count = from_int(obj.get("liveAgentCount")) + observed_at = from_int(obj.get("observedAt")) + revision = from_int(obj.get("revision")) + run_id = from_str(obj.get("runId")) + status = WorkflowRunStatus(obj.get("status")) + total_spawned_agent_count = from_int(obj.get("totalSpawnedAgentCount")) + updated_at = from_int(obj.get("updatedAt")) + workflow_name = from_str(obj.get("workflowName")) + active_segment_started_at = from_union([from_int, from_none], obj.get("activeSegmentStartedAt")) + approved = from_union([WorkflowDeclaredLimits.from_dict, from_none], obj.get("approved")) + completed_at = from_union([from_int, from_none], obj.get("completedAt")) + current_phase = from_union([WorkflowCurrentPhase.from_dict, from_none], obj.get("currentPhase")) + started_at = from_union([from_int, from_none], obj.get("startedAt")) + terminal = from_union([WorkflowRunTerminal.from_dict, from_none], obj.get("terminal")) + return WorkflowRunSummary(can_resume, consumed, created_at, declared_limits, declared_phase_count, description, live_agent_count, observed_at, revision, run_id, status, total_spawned_agent_count, updated_at, workflow_name, active_segment_started_at, approved, completed_at, current_phase, started_at, terminal) def to_dict(self) -> dict: result: dict = {} - result["runs"] = from_list(lambda x: to_class(FactoryRunSummary, x), self.runs) - if self.has_more_newer is not None: - result["hasMoreNewer"] = from_union([from_bool, from_none], self.has_more_newer) - if self.newest_seq is not None: - result["newestSeq"] = from_union([from_int, from_none], self.newest_seq) - if self.oldest_seq is not None: - result["oldestSeq"] = from_union([from_int, from_none], self.oldest_seq) - if self.omitted_older is not None: - result["omittedOlder"] = from_union([from_int, from_none], self.omitted_older) + result["canResume"] = from_bool(self.can_resume) + result["consumed"] = to_class(WorkflowRunConsumed, self.consumed) + result["createdAt"] = from_int(self.created_at) + result["declaredLimits"] = to_class(WorkflowDeclaredLimits, self.declared_limits) + result["declaredPhaseCount"] = from_int(self.declared_phase_count) + result["description"] = from_str(self.description) + result["liveAgentCount"] = from_int(self.live_agent_count) + result["observedAt"] = from_int(self.observed_at) + result["revision"] = from_int(self.revision) + result["runId"] = from_str(self.run_id) + result["status"] = to_enum(WorkflowRunStatus, self.status) + result["totalSpawnedAgentCount"] = from_int(self.total_spawned_agent_count) + result["updatedAt"] = from_int(self.updated_at) + result["workflowName"] = from_str(self.workflow_name) + result["activeSegmentStartedAt"] = from_union([from_int, from_none], self.active_segment_started_at) + result["approved"] = from_union([lambda x: to_class(WorkflowDeclaredLimits, x), from_none], self.approved) + result["completedAt"] = from_union([from_int, from_none], self.completed_at) + result["currentPhase"] = from_union([lambda x: to_class(WorkflowCurrentPhase, x), from_none], self.current_phase) + result["startedAt"] = from_union([from_int, from_none], self.started_at) + result["terminal"] = from_union([lambda x: to_class(WorkflowRunTerminal, x), from_none], self.terminal) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class WorkflowResumeResult: + """Resolved persisted workflow identity and resumed run envelope.""" + + run: WorkflowRunResult + """Terminal resumed run envelope.""" + + workflow_name: str + """Persisted workflow name resolved for the resumed run.""" + + @staticmethod + def from_dict(obj: Any) -> 'WorkflowResumeResult': + assert isinstance(obj, dict) + run = WorkflowRunResult.from_dict(obj.get("run")) + workflow_name = from_str(obj.get("workflowName")) + return WorkflowResumeResult(run, workflow_name) + + def to_dict(self) -> dict: + result: dict = {} + result["run"] = to_class(WorkflowRunResult, self.run) + result["workflowName"] = from_str(self.workflow_name) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPInstallationManagementOutcome: + """Read-only or recovery management result, never permission to activate or replay. + + Observed management outcome. + + Already-confirmed durable work must be reconciled before new mutations or inventory. + """ + kind: MCPInstallationManagementOutcomeKind + """Installation management outcome discriminator.""" + + operation: OperationClass | None = None + """Known original operation identity, returned before callback or effects. + + Original-connection operation snapshot. + """ + installations: list[MCPInstallationSummary] | None = None + """Owned receipts visible to the selected account and host. + + Freshly inspected receipts after successful durable reconciliation. + """ + plan: MCPUninstallPlan | None = None + """Original owned removal plan and operation.""" + + reason: MCPInstallationFailureReason | None = None + """Specific bounded refusal.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationManagementOutcome': + assert isinstance(obj, dict) + kind = MCPInstallationManagementOutcomeKind(obj.get("kind")) + operation = from_union([OperationClass.from_dict, from_none], obj.get("operation")) + installations = from_union([lambda x: from_list(MCPInstallationSummary.from_dict, x), from_none], obj.get("installations")) + plan = from_union([MCPUninstallPlan.from_dict, from_none], obj.get("plan")) + reason = from_union([MCPInstallationFailureReason, from_none], obj.get("reason")) + return MCPInstallationManagementOutcome(kind, operation, installations, plan, reason) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPInstallationManagementOutcomeKind, self.kind) + if self.operation is not None: + result["operation"] = from_union([lambda x: to_class(OperationClass, x), from_none], self.operation) + if self.installations is not None: + result["installations"] = from_union([lambda x: from_list(lambda x: to_class(MCPInstallationSummary, x), x), from_none], self.installations) + if self.plan is not None: + result["plan"] = from_union([lambda x: to_class(MCPUninstallPlan, x), from_none], self.plan) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(MCPInstallationFailureReason, x), from_none], self.reason) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -37255,6 +43170,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: @@ -37424,10 +43363,11 @@ class SessionUpdateOptionsParams: sandbox_config: SandboxConfig | None = None """Resolved sandbox configuration.""" - # Internal: this field is an internal SDK API and is not part of the public surface. - sandbox_config_source: _SandboxConfigSource | None = None - """Origin of the sandbox choice. The runtime uses this only for internal telemetry - provenance; managed policy is derived independently. + sandbox_config_source: SandboxConfigSource | None = None + """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. """ session_capabilities: list[SessionCapability] | None = None """Replaces the session's capability set with the given list. Use to enable or disable @@ -37522,7 +43462,7 @@ def from_dict(obj: Any) -> 'SessionUpdateOptionsParams': reasoning_summary = from_union([ReasoningSummary, from_none], obj.get("reasoningSummary")) running_in_interactive_mode = from_union([from_bool, from_none], obj.get("runningInInteractiveMode")) sandbox_config = from_union([SandboxConfig.from_dict, from_none], obj.get("sandboxConfig")) - sandbox_config_source = from_union([_SandboxConfigSource, from_none], obj.get("sandboxConfigSource")) + sandbox_config_source = from_union([SandboxConfigSource, from_none], obj.get("sandboxConfigSource")) session_capabilities = from_union([lambda x: from_list(SessionCapability, x), from_none], obj.get("sessionCapabilities")) session_limits = from_union([SessionLimitsConfig.from_dict, from_none], obj.get("sessionLimits")) shell = from_union([ShellOptions.from_dict, from_none], obj.get("shell")) @@ -37633,7 +43573,7 @@ def to_dict(self) -> dict: if self.sandbox_config is not None: result["sandboxConfig"] = from_union([lambda x: to_class(SandboxConfig, x), from_none], self.sandbox_config) if self.sandbox_config_source is not None: - result["sandboxConfigSource"] = from_union([lambda x: to_enum(_SandboxConfigSource, x), from_none], self.sandbox_config_source) + result["sandboxConfigSource"] = from_union([lambda x: to_enum(SandboxConfigSource, x), from_none], self.sandbox_config_source) if self.session_capabilities is not None: result["sessionCapabilities"] = from_union([lambda x: from_list(lambda x: to_enum(SessionCapability, x), x), from_none], self.session_capabilities) if self.session_limits is not None: @@ -37662,6 +43602,298 @@ def to_dict(self) -> dict: result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) return result +@dataclass +class InstallationReview: + """Resource-specific review to present before collecting the user's decision. + + Only resource kinds with an implemented installation engine have a review variant. + """ + resource: Resource + """Reviewed resource discriminator.""" + + review: ReviewClass + """The exact MCP action and its reviewed changes. + + The exact verified Skill action and its reviewed files. + """ + + @staticmethod + def from_dict(obj: Any) -> 'InstallationReview': + assert isinstance(obj, dict) + resource = Resource(obj.get("resource")) + review = ReviewClass.from_dict(obj.get("review")) + return InstallationReview(resource, review) + + def to_dict(self) -> dict: + result: dict = {} + result["resource"] = to_enum(Resource, self.resource) + result["review"] = to_class(ReviewClass, self.review) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallPlan: + """Prepared install plan. + + A computed Skill install plan. Nothing has been applied. + """ + expires_at: str + """Original wall-clock expiry as an ISO 8601 timestamp.""" + + operation_id: str + """Original operation identifier returned before confirmation.""" + + plan_handle: str + """One-use plan handle, bound to the original candidate authority.""" + + review: SkillInstallationReview + """Safe review to present before applying the plan.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallPlan': + assert isinstance(obj, dict) + expires_at = from_str(obj.get("expiresAt")) + operation_id = from_str(obj.get("operationId")) + plan_handle = from_str(obj.get("planHandle")) + review = SkillInstallationReview.from_dict(obj.get("review")) + return SkillInstallPlan(expires_at, operation_id, plan_handle, review) + + def to_dict(self) -> dict: + result: dict = {} + result["expiresAt"] = from_str(self.expires_at) + result["operationId"] = from_str(self.operation_id) + result["planHandle"] = from_str(self.plan_handle) + result["review"] = to_class(SkillInstallationReview, self.review) + return result + +@dataclass +class PlanClass: + """Prepared install plan. + + A computed Skill install plan. Nothing has been applied. + + Prepared uninstall plan. + + A computed Skill uninstall plan. Nothing has been removed. + """ + expires_at: str + """Original wall-clock expiry as an ISO 8601 timestamp.""" + + operation_id: str + """Original operation identifier returned before confirmation. + + Original removal operation identifier returned before confirmation. + """ + plan_handle: str + """One-use plan handle, bound to the original candidate authority. + + One-use uninstall plan handle. + """ + review: SkillInstallationReview + """Safe review to present before applying the plan. + + Safe review to present before applying removal. + """ + installation: SkillInstallationSummary | None = None + """Owned Skill installation being removed.""" + + @staticmethod + def from_dict(obj: Any) -> 'PlanClass': + assert isinstance(obj, dict) + expires_at = from_str(obj.get("expiresAt")) + operation_id = from_str(obj.get("operationId")) + plan_handle = from_str(obj.get("planHandle")) + review = SkillInstallationReview.from_dict(obj.get("review")) + installation = from_union([SkillInstallationSummary.from_dict, from_none], obj.get("installation")) + return PlanClass(expires_at, operation_id, plan_handle, review, installation) + + def to_dict(self) -> dict: + result: dict = {} + result["expiresAt"] = from_str(self.expires_at) + result["operationId"] = from_str(self.operation_id) + result["planHandle"] = from_str(self.plan_handle) + result["review"] = to_class(SkillInstallationReview, self.review) + if self.installation is not None: + result["installation"] = from_union([lambda x: to_class(SkillInstallationSummary, x), from_none], self.installation) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillUninstallPlan: + """Prepared uninstall plan. + + A computed Skill uninstall plan. Nothing has been removed. + """ + expires_at: str + """Original wall-clock expiry as an ISO 8601 timestamp.""" + + installation: SkillInstallationSummary + """Owned Skill installation being removed.""" + + operation_id: str + """Original removal operation identifier returned before confirmation.""" + + plan_handle: str + """One-use uninstall plan handle.""" + + review: SkillInstallationReview + """Safe review to present before applying removal.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillUninstallPlan': + assert isinstance(obj, dict) + expires_at = from_str(obj.get("expiresAt")) + installation = SkillInstallationSummary.from_dict(obj.get("installation")) + operation_id = from_str(obj.get("operationId")) + plan_handle = from_str(obj.get("planHandle")) + review = SkillInstallationReview.from_dict(obj.get("review")) + return SkillUninstallPlan(expires_at, installation, operation_id, plan_handle, review) + + def to_dict(self) -> dict: + result: dict = {} + result["expiresAt"] = from_str(self.expires_at) + result["installation"] = to_class(SkillInstallationSummary, self.installation) + result["operationId"] = from_str(self.operation_id) + result["planHandle"] = from_str(self.plan_handle) + result["review"] = to_class(SkillInstallationReview, self.review) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationOperationStatus: + """Operation status. + + Status snapshot from the original connection, independent of new-work account + availability. + + Original operation progress discriminator. + """ + cancellation_requested: bool + """Whether cancellation has been requested. + + Whether cancellation has been requested; already-started effects require recovery. + + Whether cancellation was requested before the terminal result. + """ + operation_id: str + """Original runtime-issued operation identity.""" + + phase: Phase + outcome: SkillInstallationOutcome | None = None + """Immutable terminal receipt.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationOperationStatus': + assert isinstance(obj, dict) + cancellation_requested = from_bool(obj.get("cancellationRequested")) + operation_id = from_str(obj.get("operationId")) + phase = Phase(obj.get("phase")) + outcome = from_union([SkillInstallationOutcome.from_dict, from_none], obj.get("outcome")) + return SkillInstallationOperationStatus(cancellation_requested, operation_id, phase, outcome) + + def to_dict(self) -> dict: + result: dict = {} + result["cancellationRequested"] = from_bool(self.cancellation_requested) + result["operationId"] = from_str(self.operation_id) + result["phase"] = to_enum(Phase, self.phase) + if self.outcome is not None: + result["outcome"] = from_union([lambda x: to_class(SkillInstallationOutcome, x), from_none], self.outcome) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationResult: + """Skill installation result with the honoured contract, or a typed request/negotiation + refusal. + + The caller's protocol version or required capabilities cannot be honoured. Returned + instead of a partial or ambiguous success. + + The request was rejected because a bounded field fell outside its permitted range or a + required field was unusable. Pagination may also be rejected by the authority after a + continuation request; repeat the search without page. + """ + kind: MCPInstallationManagementResultKind + """Discriminator: handled operation outcome. + + Discriminator: capability or protocol-version negotiation failed + + Discriminator: the request itself was invalid + """ + negotiated: CatalogNegotiatedContract | None = None + """Capabilities honoured for this request.""" + + outcome: SkillInstallationOutcome | None = None + """Terminal operation outcome.""" + + message: str | None = None + """Human-readable explanation, safe to surface. Never contains a query, URL, handle, or + secret. + + Human-readable explanation, safe to surface. Never echoes the offending value, nor a + query, URL, handle, or secret. + """ + minimum_supported_protocol_version: int | None = None + """Lowest caller protocol version this runtime will serve.""" + + reason: CatalogNegotiationRefusedReason | None = None + """Whether the version or the capability set was the problem.""" + + runtime_protocol_version: int | None = None + """Protocol version of the runtime that refused the request.""" + + supported_capabilities: list[str] | None = None + """Capabilities this runtime can safely advertise to this caller. The complete + five-capability protocol-3 legacy set is always present; every capability added after + that baseline appears only when the caller required it, so an older closed-enum decoder + can still consume a refusal. This list does not imply that every deployment has enabled + every operation. + """ + unsupported_capabilities: list[str] | None = None + """The subset of the caller's bounded extensible capability identifiers this runtime cannot + honour. + """ + field: CatalogInvalidRequestField | None = None + """Which request field was rejected.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationResult': + assert isinstance(obj, dict) + kind = MCPInstallationManagementResultKind(obj.get("kind")) + negotiated = from_union([CatalogNegotiatedContract.from_dict, from_none], obj.get("negotiated")) + outcome = from_union([SkillInstallationOutcome.from_dict, from_none], obj.get("outcome")) + message = from_union([from_str, from_none], obj.get("message")) + minimum_supported_protocol_version = from_union([from_int, from_none], obj.get("minimumSupportedProtocolVersion")) + reason = from_union([CatalogNegotiationRefusedReason, from_none], obj.get("reason")) + runtime_protocol_version = from_union([from_int, from_none], obj.get("runtimeProtocolVersion")) + supported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedCapabilities")) + unsupported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("unsupportedCapabilities")) + field = from_union([CatalogInvalidRequestField, from_none], obj.get("field")) + return SkillInstallationResult(kind, negotiated, outcome, message, minimum_supported_protocol_version, reason, runtime_protocol_version, supported_capabilities, unsupported_capabilities, field) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPInstallationManagementResultKind, self.kind) + if self.negotiated is not None: + result["negotiated"] = from_union([lambda x: to_class(CatalogNegotiatedContract, x), from_none], self.negotiated) + if self.outcome is not None: + result["outcome"] = from_union([lambda x: to_class(SkillInstallationOutcome, x), from_none], self.outcome) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.minimum_supported_protocol_version is not None: + result["minimumSupportedProtocolVersion"] = from_union([from_int, from_none], self.minimum_supported_protocol_version) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(CatalogNegotiationRefusedReason, x), from_none], self.reason) + if self.runtime_protocol_version is not None: + result["runtimeProtocolVersion"] = from_union([from_int, from_none], self.runtime_protocol_version) + if self.supported_capabilities is not None: + result["supportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_capabilities) + if self.unsupported_capabilities is not None: + result["unsupportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.unsupported_capabilities) + if self.field is not None: + result["field"] = from_union([lambda x: to_enum(CatalogInvalidRequestField, x), from_none], self.field) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class UIElicitationRequest: @@ -37747,6 +43979,325 @@ def to_dict(self) -> dict: result["omittedOlder"] = from_union([from_int, from_none], self.omitted_older) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPInstallationManagementResult: + """Management result with contract receipt, or a typed request/negotiation refusal. + + The caller's protocol version or required capabilities cannot be honoured. Returned + instead of a partial or ambiguous success. + + The request was rejected because a bounded field fell outside its permitted range or a + required field was unusable. Pagination may also be rejected by the authority after a + continuation request; repeat the search without page. + """ + kind: MCPInstallationManagementResultKind + """A handled management outcome with an honoured contract. + + Discriminator: capability or protocol-version negotiation failed + + Discriminator: the request itself was invalid + """ + negotiated: CatalogNegotiatedContract | None = None + """Capabilities actually honoured for this request.""" + + outcome: MCPInstallationManagementOutcome | None = None + """Observed management outcome.""" + + message: str | None = None + """Human-readable explanation, safe to surface. Never contains a query, URL, handle, or + secret. + + Human-readable explanation, safe to surface. Never echoes the offending value, nor a + query, URL, handle, or secret. + """ + minimum_supported_protocol_version: int | None = None + """Lowest caller protocol version this runtime will serve.""" + + reason: CatalogNegotiationRefusedReason | None = None + """Whether the version or the capability set was the problem.""" + + runtime_protocol_version: int | None = None + """Protocol version of the runtime that refused the request.""" + + supported_capabilities: list[str] | None = None + """Capabilities this runtime can safely advertise to this caller. The complete + five-capability protocol-3 legacy set is always present; every capability added after + that baseline appears only when the caller required it, so an older closed-enum decoder + can still consume a refusal. This list does not imply that every deployment has enabled + every operation. + """ + unsupported_capabilities: list[str] | None = None + """The subset of the caller's bounded extensible capability identifiers this runtime cannot + honour. + """ + field: CatalogInvalidRequestField | None = None + """Which request field was rejected.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPInstallationManagementResult': + assert isinstance(obj, dict) + kind = MCPInstallationManagementResultKind(obj.get("kind")) + negotiated = from_union([CatalogNegotiatedContract.from_dict, from_none], obj.get("negotiated")) + outcome = from_union([MCPInstallationManagementOutcome.from_dict, from_none], obj.get("outcome")) + message = from_union([from_str, from_none], obj.get("message")) + minimum_supported_protocol_version = from_union([from_int, from_none], obj.get("minimumSupportedProtocolVersion")) + reason = from_union([CatalogNegotiationRefusedReason, from_none], obj.get("reason")) + runtime_protocol_version = from_union([from_int, from_none], obj.get("runtimeProtocolVersion")) + supported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedCapabilities")) + unsupported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("unsupportedCapabilities")) + field = from_union([CatalogInvalidRequestField, from_none], obj.get("field")) + return MCPInstallationManagementResult(kind, negotiated, outcome, message, minimum_supported_protocol_version, reason, runtime_protocol_version, supported_capabilities, unsupported_capabilities, field) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPInstallationManagementResultKind, self.kind) + if self.negotiated is not None: + result["negotiated"] = from_union([lambda x: to_class(CatalogNegotiatedContract, x), from_none], self.negotiated) + if self.outcome is not None: + result["outcome"] = from_union([lambda x: to_class(MCPInstallationManagementOutcome, x), from_none], self.outcome) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.minimum_supported_protocol_version is not None: + result["minimumSupportedProtocolVersion"] = from_union([from_int, from_none], self.minimum_supported_protocol_version) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(CatalogNegotiationRefusedReason, x), from_none], self.reason) + if self.runtime_protocol_version is not None: + result["runtimeProtocolVersion"] = from_union([from_int, from_none], self.runtime_protocol_version) + if self.supported_capabilities is not None: + result["supportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_capabilities) + if self.unsupported_capabilities is not None: + result["unsupportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.unsupported_capabilities) + if self.field is not None: + result["field"] = from_union([lambda x: to_enum(CatalogInvalidRequestField, x), from_none], self.field) + return result + +@dataclass +class InstallationConfirmationRequest: + """One connection-owned, expiring request for a trusted host's explicit user decision.""" + + confirmation_id: str + """Opaque one-use challenge. Return unchanged; never log or persist.""" + + expires_at: str + """Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it.""" + + operation_id: str + """Random identifier of this installation operation, not a plan handle.""" + + review: InstallationReview + """Resource-specific review to present before collecting the user's decision.""" + + review_fingerprint: str + """Opaque commitment to the exact review and inputs. Return unchanged; never log.""" + + policy_session_id: str | None = None + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'InstallationConfirmationRequest': + assert isinstance(obj, dict) + confirmation_id = from_str(obj.get("confirmationId")) + expires_at = from_str(obj.get("expiresAt")) + operation_id = from_str(obj.get("operationId")) + review = InstallationReview.from_dict(obj.get("review")) + review_fingerprint = from_str(obj.get("reviewFingerprint")) + policy_session_id = from_union([from_str, from_none], obj.get("policySessionId")) + return InstallationConfirmationRequest(confirmation_id, expires_at, operation_id, review, review_fingerprint, policy_session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["confirmationId"] = from_str(self.confirmation_id) + result["expiresAt"] = from_str(self.expires_at) + result["operationId"] = from_str(self.operation_id) + result["review"] = to_class(InstallationReview, self.review) + result["reviewFingerprint"] = from_str(self.review_fingerprint) + if self.policy_session_id is not None: + result["policySessionId"] = from_union([from_str, from_none], self.policy_session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationManagementOutcome: + """Management outcome for verified Skill inventory, planning and removal. + + Observed management outcome. + + Already-confirmed durable work must be reconciled before new mutations or inventory. + + A verified Skill installation plan was prepared. + + Owned Skill installations were listed. + + Recovery completed and inventory was inspected. + + Interrupted work was safely compensated and the pending marker was cleared. + + An owned Skill uninstall plan was prepared. + + Original-connection operation snapshot. + + Enablement changed and the selected session was reconciled. + + The management request was refused. + """ + kind: SkillInstallationManagementOutcomeKind + """Skill installation management outcome discriminator.""" + + plan: PlanClass | None = None + """Prepared install plan. + + Prepared uninstall plan. + """ + installations: list[SkillInstallationSummary] | None = None + """Owned Skill installation summaries. + + Owned Skill installation summaries after recovery. + """ + operation_id: str | None = None + """Original operation identity.""" + + reason: SkillInstallationFailureReason | None = None + """Cause of the compensation. + + Bounded refusal reason. + """ + operation: SkillInstallationOperationStatus | None = None + """Operation status.""" + + diagnostics: list[str] | None = None + """Safe reload or reconciliation diagnostics.""" + + installation: SkillInstallationSummary | None = None + """Updated installation summary.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationManagementOutcome': + assert isinstance(obj, dict) + kind = SkillInstallationManagementOutcomeKind(obj.get("kind")) + plan = from_union([PlanClass.from_dict, from_none], obj.get("plan")) + installations = from_union([lambda x: from_list(SkillInstallationSummary.from_dict, x), from_none], obj.get("installations")) + operation_id = from_union([from_str, from_none], obj.get("operation_id")) + reason = from_union([SkillInstallationFailureReason, from_none], obj.get("reason")) + operation = from_union([SkillInstallationOperationStatus.from_dict, from_none], obj.get("operation")) + diagnostics = from_union([lambda x: from_list(from_str, x), from_none], obj.get("diagnostics")) + installation = from_union([SkillInstallationSummary.from_dict, from_none], obj.get("installation")) + return SkillInstallationManagementOutcome(kind, plan, installations, operation_id, reason, operation, diagnostics, installation) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(SkillInstallationManagementOutcomeKind, self.kind) + if self.plan is not None: + result["plan"] = from_union([lambda x: to_class(PlanClass, x), from_none], self.plan) + if self.installations is not None: + result["installations"] = from_union([lambda x: from_list(lambda x: to_class(SkillInstallationSummary, x), x), from_none], self.installations) + if self.operation_id is not None: + result["operation_id"] = from_union([from_str, from_none], self.operation_id) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(SkillInstallationFailureReason, x), from_none], self.reason) + if self.operation is not None: + result["operation"] = from_union([lambda x: to_class(SkillInstallationOperationStatus, x), from_none], self.operation) + if self.diagnostics is not None: + result["diagnostics"] = from_union([lambda x: from_list(from_str, x), from_none], self.diagnostics) + if self.installation is not None: + result["installation"] = from_union([lambda x: to_class(SkillInstallationSummary, x), from_none], self.installation) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillInstallationManagementResult: + """Skill installation management result with the honoured contract, or a typed refusal. + + The caller's protocol version or required capabilities cannot be honoured. Returned + instead of a partial or ambiguous success. + + The request was rejected because a bounded field fell outside its permitted range or a + required field was unusable. Pagination may also be rejected by the authority after a + continuation request; repeat the search without page. + """ + kind: MCPInstallationManagementResultKind + """Discriminator: handled management outcome. + + Discriminator: capability or protocol-version negotiation failed + + Discriminator: the request itself was invalid + """ + negotiated: CatalogNegotiatedContract | None = None + """Capabilities honoured for this request.""" + + outcome: SkillInstallationManagementOutcome | None = None + """Observed management outcome.""" + + message: str | None = None + """Human-readable explanation, safe to surface. Never contains a query, URL, handle, or + secret. + + Human-readable explanation, safe to surface. Never echoes the offending value, nor a + query, URL, handle, or secret. + """ + minimum_supported_protocol_version: int | None = None + """Lowest caller protocol version this runtime will serve.""" + + reason: CatalogNegotiationRefusedReason | None = None + """Whether the version or the capability set was the problem.""" + + runtime_protocol_version: int | None = None + """Protocol version of the runtime that refused the request.""" + + supported_capabilities: list[str] | None = None + """Capabilities this runtime can safely advertise to this caller. The complete + five-capability protocol-3 legacy set is always present; every capability added after + that baseline appears only when the caller required it, so an older closed-enum decoder + can still consume a refusal. This list does not imply that every deployment has enabled + every operation. + """ + unsupported_capabilities: list[str] | None = None + """The subset of the caller's bounded extensible capability identifiers this runtime cannot + honour. + """ + field: CatalogInvalidRequestField | None = None + """Which request field was rejected.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillInstallationManagementResult': + assert isinstance(obj, dict) + kind = MCPInstallationManagementResultKind(obj.get("kind")) + negotiated = from_union([CatalogNegotiatedContract.from_dict, from_none], obj.get("negotiated")) + outcome = from_union([SkillInstallationManagementOutcome.from_dict, from_none], obj.get("outcome")) + message = from_union([from_str, from_none], obj.get("message")) + minimum_supported_protocol_version = from_union([from_int, from_none], obj.get("minimumSupportedProtocolVersion")) + reason = from_union([CatalogNegotiationRefusedReason, from_none], obj.get("reason")) + runtime_protocol_version = from_union([from_int, from_none], obj.get("runtimeProtocolVersion")) + supported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedCapabilities")) + unsupported_capabilities = from_union([lambda x: from_list(from_str, x), from_none], obj.get("unsupportedCapabilities")) + field = from_union([CatalogInvalidRequestField, from_none], obj.get("field")) + return SkillInstallationManagementResult(kind, negotiated, outcome, message, minimum_supported_protocol_version, reason, runtime_protocol_version, supported_capabilities, unsupported_capabilities, field) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPInstallationManagementResultKind, self.kind) + if self.negotiated is not None: + result["negotiated"] = from_union([lambda x: to_class(CatalogNegotiatedContract, x), from_none], self.negotiated) + if self.outcome is not None: + result["outcome"] = from_union([lambda x: to_class(SkillInstallationManagementOutcome, x), from_none], self.outcome) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.minimum_supported_protocol_version is not None: + result["minimumSupportedProtocolVersion"] = from_union([from_int, from_none], self.minimum_supported_protocol_version) + if self.reason is not None: + result["reason"] = from_union([lambda x: to_enum(CatalogNegotiationRefusedReason, x), from_none], self.reason) + if self.runtime_protocol_version is not None: + result["runtimeProtocolVersion"] = from_union([from_int, from_none], self.runtime_protocol_version) + if self.supported_capabilities is not None: + result["supportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_capabilities) + if self.unsupported_capabilities is not None: + result["unsupportedCapabilities"] = from_union([lambda x: from_list(from_str, x), from_none], self.unsupported_capabilities) + if self.field is not None: + result["field"] = from_union([lambda x: to_enum(CatalogInvalidRequestField, x), from_none], self.field) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CopilotUserResponse: @@ -38197,8 +44748,11 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AuthIdentity: - """Credential-free authentication identity safe to expose to hosts and user interfaces.""" + """Credential-free authentication identity safe to expose to hosts and user interfaces. + Credential-free identity metadata for the active account, including resolved Copilot user + information when available. + """ host: str """Authentication host""" @@ -38244,6 +44798,47 @@ def to_dict(self) -> dict: result["registrationId"] = from_union([from_str, from_none], self.registration_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthReadValue: + """The read result, keyed by the same selector as the query.""" + + kind: AuthReadQueryKind + """Account read-datum variant discriminator.""" + account: AccountStatus | None = None + """The active account, or absent when not logged in.""" + status: AuthStatusDto | None = None + """The neutral authentication status summary.""" + errors: list[AuthValidationError] | None = None + """Validation errors from the most recent authentication attempt.""" + auth_info: AuthIdentity | None = None + """Credential-free identity metadata for the active account, including resolved Copilot user + information when available. + """ + + @staticmethod + def from_dict(obj: Any) -> 'AuthReadValue': + assert isinstance(obj, dict) + kind = AuthReadQueryKind(obj.get("kind")) + account = from_union([AccountStatus.from_dict, from_none], obj.get("account")) + auth_info = from_union([AuthIdentity.from_dict, from_none], obj.get("authInfo")) + status = from_union([AuthStatusDto.from_dict, from_none], obj.get("status")) + errors = from_union([lambda x: from_list(AuthValidationError.from_dict, x), from_none], obj.get("errors")) + return AuthReadValue(kind, account, status, errors, auth_info) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(AuthReadQueryKind, self.kind) + if self.account is not None: + result["account"] = from_union([lambda x: to_class(AccountStatus, x), from_none], self.account) + if self.auth_info is not None: + result["authInfo"] = from_union([lambda x: to_class(AuthIdentity, x), from_none], self.auth_info) + if self.status is not None: + result["status"] = from_union([lambda x: to_class(AuthStatusDto, x), from_none], self.status) + if self.errors is not None: + result["errors"] = from_union([lambda x: from_list(lambda x: to_class(AuthValidationError, x), x), from_none], self.errors) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class BuiltinToolDescriptor: @@ -38308,6 +44903,64 @@ def to_dict(self) -> dict: result["type"] = from_union([from_none, from_str], self.type) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorDiscoveryAccountList: + """Eligible accounts.""" + + accounts: list[ConnectorDiscoveryAccount] + """Eligible accounts.""" + + availability: ConnectorDiscoveryAvailability + """Availability.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDiscoveryAccountList': + assert isinstance(obj, dict) + accounts = from_list(ConnectorDiscoveryAccount.from_dict, obj.get("accounts")) + availability = ConnectorDiscoveryAvailability(obj.get("availability")) + return ConnectorDiscoveryAccountList(accounts, availability) + + def to_dict(self) -> dict: + result: dict = {} + result["accounts"] = from_list(lambda x: to_class(ConnectorDiscoveryAccount, x), self.accounts) + result["availability"] = to_enum(ConnectorDiscoveryAvailability, self.availability) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorDiscoveryCapabilities: + """Feature availability.""" + + api_version: int + """API version.""" + + availability: ConnectorDiscoveryAvailability + """Availability.""" + + conditional_cache: bool + """Whether results are cached.""" + + opaque_account_selection: bool + """Whether accounts are selected by opaque ID.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDiscoveryCapabilities': + assert isinstance(obj, dict) + api_version = from_int(obj.get("apiVersion")) + availability = ConnectorDiscoveryAvailability(obj.get("availability")) + conditional_cache = from_bool(obj.get("conditionalCache")) + opaque_account_selection = from_bool(obj.get("opaqueAccountSelection")) + return ConnectorDiscoveryCapabilities(api_version, availability, conditional_cache, opaque_account_selection) + + def to_dict(self) -> dict: + result: dict = {} + result["apiVersion"] = from_int(self.api_version) + result["availability"] = to_enum(ConnectorDiscoveryAvailability, self.availability) + result["conditionalCache"] = from_bool(self.conditional_cache) + result["opaqueAccountSelection"] = from_bool(self.opaque_account_selection) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CopilotAPITokenAuthInfo: @@ -38464,6 +45117,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: @@ -38568,225 +45344,6 @@ def to_dict(self) -> dict: result["hasMore"] = from_bool(self.has_more) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryAgentSummary: - """Prompt-safe durable identity and live status for a direct factory agent.""" - - active_ms: int - """Accumulated active agent time in milliseconds.""" - - agent_id: str - """Stable direct-agent identifier.""" - - agent_type: str - """Registered agent type.""" - - label: str - """Friendly, non-unique name intended for display""" - - run_id: str - """Owning factory run identifier.""" - - status: str - """Current durable or live agent status.""" - - tool_call_id: str - """Tool-call identifier that launched the agent.""" - - activity: str | None = None - """Prompt-safe live activity text.""" - - completed_at: int | None = None - """Epoch milliseconds when the agent completed.""" - - display_name: str | None = None - """Friendly, non-unique name intended for display""" - - phase_id: str | None = None - """Phase identifier active when the agent was launched, or null.""" - - requested_model: str | None = None - """Model requested when the agent was launched.""" - - resolved_model: str | None = None - """Concrete model resolved for the agent.""" - - started_at: int | None = None - """Epoch milliseconds when the agent started.""" - - @staticmethod - def from_dict(obj: Any) -> 'FactoryAgentSummary': - assert isinstance(obj, dict) - active_ms = from_int(obj.get("activeMs")) - agent_id = from_str(obj.get("agentId")) - agent_type = from_str(obj.get("agentType")) - label = from_str(obj.get("label")) - run_id = from_str(obj.get("runId")) - status = from_str(obj.get("status")) - tool_call_id = from_str(obj.get("toolCallId")) - activity = from_union([from_str, from_none], obj.get("activity")) - completed_at = from_union([from_int, from_none], obj.get("completedAt")) - display_name = from_union([from_str, from_none], obj.get("displayName")) - phase_id = from_union([from_none, from_str], obj.get("phaseId")) - requested_model = from_union([from_str, from_none], obj.get("requestedModel")) - resolved_model = from_union([from_str, from_none], obj.get("resolvedModel")) - started_at = from_union([from_int, from_none], obj.get("startedAt")) - return FactoryAgentSummary(active_ms, agent_id, agent_type, label, run_id, status, tool_call_id, activity, completed_at, display_name, phase_id, requested_model, resolved_model, started_at) - - def to_dict(self) -> dict: - result: dict = {} - result["activeMs"] = from_int(self.active_ms) - result["agentId"] = from_str(self.agent_id) - result["agentType"] = from_str(self.agent_type) - result["label"] = from_str(self.label) - result["runId"] = from_str(self.run_id) - result["status"] = from_str(self.status) - result["toolCallId"] = from_str(self.tool_call_id) - if self.activity is not None: - result["activity"] = from_union([from_str, from_none], self.activity) - if self.completed_at is not None: - result["completedAt"] = from_union([from_int, from_none], self.completed_at) - if self.display_name is not None: - result["displayName"] = from_union([from_str, from_none], self.display_name) - result["phaseId"] = from_union([from_none, from_str], self.phase_id) - if self.requested_model is not None: - result["requestedModel"] = from_union([from_str, from_none], self.requested_model) - if self.resolved_model is not None: - result["resolvedModel"] = from_union([from_str, from_none], self.resolved_model) - if self.started_at is not None: - result["startedAt"] = from_union([from_int, from_none], self.started_at) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryRunDetail: - """Full factory run observability detail.""" - - agents: list[FactoryAgentSummary] - """Durable identities and live statuses for direct factory agents.""" - - can_resume: bool - """Whether the durable run state currently passes runtime resume eligibility checks.""" - - consumed: FactoryRunConsumed - """Durable resource consumption.""" - - created_at: int - """Epoch milliseconds when the run was created.""" - - declared_limits: FactoryDeclaredLimits - """Resource ceilings declared by the factory.""" - - declared_phase_count: int - """Number of phases declared by the factory.""" - - description: str - """Human-readable factory description.""" - - factory_name: str - """Registered factory name.""" - - live_agent_count: int - """Number of direct factory agents currently live.""" - - observed_at: int - """Epoch milliseconds when this live-overlay snapshot was observed.""" - - phases: list[FactoryPhaseObservation] - """Lifecycle and timing observations for each factory phase.""" - - progress: FactoryProgressPage - """Bidirectional page of durable factory progress.""" - - revision: int - """Monotonic durable run revision.""" - - run_id: str - """Factory run identifier.""" - - status: RunStatus - """Current factory run status.""" - - total_spawned_agent_count: int - """Total direct factory agents spawned across all attempts.""" - - updated_at: int - """Epoch milliseconds when the durable run was last updated.""" - - active_segment_started_at: int | None = None - """Epoch milliseconds when the current active segment started, or null while inactive.""" - - approved: FactoryDeclaredLimits | None = None - """Approved effective resource ceilings, or null until approved.""" - - completed_at: int | None = None - """Epoch milliseconds when the run completed, or null while nonterminal.""" - - current_phase: FactoryCurrentPhase | None = None - """Current phase identity, or null before any phase is entered.""" - - started_at: int | None = None - """Epoch milliseconds when execution first started, or null before start.""" - - terminal: FactoryRunTerminal | None = None - """Terminal run outcome, or null while nonterminal.""" - - @staticmethod - def from_dict(obj: Any) -> 'FactoryRunDetail': - assert isinstance(obj, dict) - agents = from_list(FactoryAgentSummary.from_dict, obj.get("agents")) - can_resume = from_bool(obj.get("canResume")) - consumed = FactoryRunConsumed.from_dict(obj.get("consumed")) - created_at = from_int(obj.get("createdAt")) - declared_limits = FactoryDeclaredLimits.from_dict(obj.get("declaredLimits")) - declared_phase_count = from_int(obj.get("declaredPhaseCount")) - description = from_str(obj.get("description")) - factory_name = from_str(obj.get("factoryName")) - live_agent_count = from_int(obj.get("liveAgentCount")) - observed_at = from_int(obj.get("observedAt")) - phases = from_list(FactoryPhaseObservation.from_dict, obj.get("phases")) - progress = FactoryProgressPage.from_dict(obj.get("progress")) - revision = from_int(obj.get("revision")) - run_id = from_str(obj.get("runId")) - status = RunStatus(obj.get("status")) - total_spawned_agent_count = from_int(obj.get("totalSpawnedAgentCount")) - updated_at = from_int(obj.get("updatedAt")) - active_segment_started_at = from_union([from_int, from_none], obj.get("activeSegmentStartedAt")) - approved = from_union([FactoryDeclaredLimits.from_dict, from_none], obj.get("approved")) - completed_at = from_union([from_int, from_none], obj.get("completedAt")) - current_phase = from_union([FactoryCurrentPhase.from_dict, from_none], obj.get("currentPhase")) - started_at = from_union([from_int, from_none], obj.get("startedAt")) - terminal = from_union([FactoryRunTerminal.from_dict, from_none], obj.get("terminal")) - return FactoryRunDetail(agents, can_resume, consumed, created_at, declared_limits, declared_phase_count, description, factory_name, live_agent_count, observed_at, phases, progress, revision, run_id, status, total_spawned_agent_count, updated_at, active_segment_started_at, approved, completed_at, current_phase, started_at, terminal) - - def to_dict(self) -> dict: - result: dict = {} - result["agents"] = from_list(lambda x: to_class(FactoryAgentSummary, x), self.agents) - result["canResume"] = from_bool(self.can_resume) - result["consumed"] = to_class(FactoryRunConsumed, self.consumed) - result["createdAt"] = from_int(self.created_at) - result["declaredLimits"] = to_class(FactoryDeclaredLimits, self.declared_limits) - result["declaredPhaseCount"] = from_int(self.declared_phase_count) - result["description"] = from_str(self.description) - result["factoryName"] = from_str(self.factory_name) - result["liveAgentCount"] = from_int(self.live_agent_count) - result["observedAt"] = from_int(self.observed_at) - result["phases"] = from_list(lambda x: to_class(FactoryPhaseObservation, x), self.phases) - result["progress"] = to_class(FactoryProgressPage, self.progress) - result["revision"] = from_int(self.revision) - result["runId"] = from_str(self.run_id) - result["status"] = to_enum(RunStatus, self.status) - result["totalSpawnedAgentCount"] = from_int(self.total_spawned_agent_count) - result["updatedAt"] = from_int(self.updated_at) - result["activeSegmentStartedAt"] = from_union([from_int, from_none], self.active_segment_started_at) - result["approved"] = from_union([lambda x: to_class(FactoryDeclaredLimits, x), from_none], self.approved) - result["completedAt"] = from_union([from_int, from_none], self.completed_at) - result["currentPhase"] = from_union([lambda x: to_class(FactoryCurrentPhase, x), from_none], self.current_phase) - result["startedAt"] = from_union([from_int, from_none], self.started_at) - result["terminal"] = from_union([lambda x: to_class(FactoryRunTerminal, x), from_none], self.terminal) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class GhCLIAuthInfo: @@ -38980,11 +45537,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 @@ -38998,11 +45587,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) @@ -39020,6 +45604,27 @@ def to_dict(self) -> dict: result["serverName"] = from_str(self.server_name) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPOauthCancelLoginResult: + """Honest terminal cancellation result; persistence or recovery failures remain RPC errors.""" + + cancelled: bool + """True after cancellation settles, false when the original login already connected + successfully. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPOauthCancelLoginResult': + assert isinstance(obj, dict) + cancelled = from_bool(obj.get("cancelled")) + return MCPOauthCancelLoginResult(cancelled) + + def to_dict(self) -> dict: + result: dict = {} + result["cancelled"] = from_bool(self.cancelled) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPOauthProbeResult: @@ -39078,18 +45683,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. """ @@ -39667,10 +46272,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.""" @@ -39812,11 +46417,13 @@ class MetadataContextInfoRequest: """Model identifier and token limits used to compute the context-info breakdown.""" output_token_limit: int - """Maximum output tokens allowed by the target model. Pass 0 if unknown.""" - + """Requested output allowance to reserve against the combined context ceiling. Pass 0 to + resolve the session's request cap, falling back to the model's advertised output limit. + """ prompt_token_limit: int - """Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default.""" - + """Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from + the session. + """ selected_model: str | None = None """Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. @@ -39859,40 +46466,54 @@ 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. +class ModelPickerCategory(Enum): + """Model capability category for grouping in the model picker""" + + LIGHTWEIGHT = "lightweight" + POWERFUL = "powerful" + VERSATILE = "versatile" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ModelCapabilities: - """Model capabilities and limits""" +class ModelProviderRef: + """The model provider that produced this model, as a neutral reference (opaque id, + human-readable label, and provider kind). Present on models returned by + `session.model.list`, which resolves provider attribution from the session's account + roster; absent on the flat `server.models.list`, which does not resolve a session roster. + The model picker groups by this reference. - limits: ModelCapabilitiesLimits | None = None - """Token limits for prompts, outputs, and context window""" + A neutral reference to the model provider that produced a model: an opaque id, a + human-readable label, and the provider kind. Carried on each enumerated Model so + consumers can group by provider without reaching into a provider-shaped internal type. + """ + id: str + """Opaque, stable id of the provider that produced this model. Matches the enumerated + `ModelProviderDescriptor.id`. + """ + kind: ModelProviderKind + """The provider kind.""" - supports: ModelCapabilitiesSupports | None = None - """Feature flags indicating what the model supports""" + label: str + """Human-readable provider label, owned by the runtime so every consumer renders identical + text. + """ @staticmethod - def from_dict(obj: Any) -> 'ModelCapabilities': + def from_dict(obj: Any) -> 'ModelProviderRef': 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) + id = from_str(obj.get("id")) + kind = ModelProviderKind(obj.get("kind")) + label = from_str(obj.get("label")) + return ModelProviderRef(id, kind, label) 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) + result["id"] = from_str(self.id) + result["kind"] = to_enum(ModelProviderKind, self.kind) + result["label"] = from_str(self.label) 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""" - - LIGHTWEIGHT = "lightweight" - POWERFUL = "powerful" - VERSATILE = "versatile" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class Model: @@ -39933,6 +46554,13 @@ class Model: policy: ModelPolicy | None = None """Policy state (if applicable)""" + provider: ModelProviderRef | None = None + """The model provider that produced this model, as a neutral reference (opaque id, + human-readable label, and provider kind). Present on models returned by + `session.model.list`, which resolves provider attribution from the session's account + roster; absent on the flat `server.models.list`, which does not resolve a session roster. + The model picker groups by this reference. + """ supported_context_tiers: list[str] | None = None """Context-window tiers this model offers, when the provider advertises them independently of tiered token pricing. Copilot models carry their tiers in `billing.tokenPrices`; a @@ -39942,6 +46570,12 @@ class Model: supported_reasoning_efforts: list[str] | None = None """Supported reasoning effort levels (only present if model supports reasoning effort)""" + vendor: str | None = None + """Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". + Open vocabulary, passed through unchanged. It can name the vendor that serves the model + instead of the one that built it, or a label that is not a vendor, such as + "Experimental". Absent when the Copilot API reports no vendor. + """ warning_messages: list[ModelMessage] | None = None """Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; @@ -39965,11 +46599,13 @@ def from_dict(obj: Any) -> 'Model': model_picker_category = from_union([ModelPickerCategory, from_none], obj.get("modelPickerCategory")) model_picker_price_category = from_union([ModelPickerPriceCategory, from_none], obj.get("modelPickerPriceCategory")) policy = from_union([ModelPolicy.from_dict, from_none], obj.get("policy")) + provider = from_union([ModelProviderRef.from_dict, from_none], obj.get("provider")) supported_context_tiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedContextTiers")) supported_reasoning_efforts = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedReasoningEfforts")) + vendor = from_union([from_str, from_none], obj.get("vendor")) warning_messages = from_union([lambda x: from_list(ModelMessage.from_dict, x), from_none], obj.get("warningMessages")) warning_text = from_union([ModelWarningText.from_dict, from_none], obj.get("warningText")) - return Model(capabilities, id, name, billing, default_reasoning_effort, info_messages, metadata, model_picker_category, model_picker_price_category, policy, supported_context_tiers, supported_reasoning_efforts, warning_messages, warning_text) + return Model(capabilities, id, name, billing, default_reasoning_effort, info_messages, metadata, model_picker_category, model_picker_price_category, policy, provider, supported_context_tiers, supported_reasoning_efforts, vendor, warning_messages, warning_text) def to_dict(self) -> dict: result: dict = {} @@ -39990,10 +46626,14 @@ def to_dict(self) -> dict: result["modelPickerPriceCategory"] = from_union([lambda x: to_enum(ModelPickerPriceCategory, x), from_none], self.model_picker_price_category) if self.policy is not None: result["policy"] = from_union([lambda x: to_class(ModelPolicy, x), from_none], self.policy) + if self.provider is not None: + result["provider"] = from_union([lambda x: to_class(ModelProviderRef, x), from_none], self.provider) if self.supported_context_tiers is not None: result["supportedContextTiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_context_tiers) if self.supported_reasoning_efforts is not None: result["supportedReasoningEfforts"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_reasoning_efforts) + if self.vendor is not None: + result["vendor"] = from_union([from_str, from_none], self.vendor) if self.warning_messages is not None: result["warningMessages"] = from_union([lambda x: from_list(lambda x: to_class(ModelMessage, x), x), from_none], self.warning_messages) if self.warning_text is not None: @@ -40018,6 +46658,16 @@ class ModelApplyStartupOverlayRequest: device_managed_model: str | None = None """Model required by device-managed policy, when configured.""" + managed_context_tier: str | None = None + """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. + """ + managed_reasoning_effort: str | None = None + """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. + """ policy_helper_model: str | None = None """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 @@ -40046,13 +46696,15 @@ def from_dict(obj: Any) -> 'ModelApplyStartupOverlayRequest': cli_model = from_union([from_str, from_none], obj.get("cliModel")) deferred_resume = from_union([from_bool, from_none], obj.get("deferredResume")) device_managed_model = from_union([from_str, from_none], obj.get("deviceManagedModel")) + managed_context_tier = from_union([from_str, from_none], obj.get("managedContextTier")) + managed_reasoning_effort = from_union([from_str, from_none], obj.get("managedReasoningEffort")) policy_helper_model = from_union([from_str, from_none], obj.get("policyHelperModel")) repo_auto_tier = from_union([from_str, from_none], obj.get("repoAutoTier")) repo_context_tier = from_union([from_str, from_none], obj.get("repoContextTier")) repo_model = from_union([from_str, from_none], obj.get("repoModel")) repo_reasoning_effort = from_union([from_str, from_none], obj.get("repoReasoningEffort")) server_managed_model = from_union([from_str, from_none], obj.get("serverManagedModel")) - return ModelApplyStartupOverlayRequest(auto_tier, cli_model, deferred_resume, device_managed_model, policy_helper_model, repo_auto_tier, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) + return ModelApplyStartupOverlayRequest(auto_tier, cli_model, deferred_resume, device_managed_model, managed_context_tier, managed_reasoning_effort, policy_helper_model, repo_auto_tier, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) def to_dict(self) -> dict: result: dict = {} @@ -40064,6 +46716,10 @@ def to_dict(self) -> dict: result["deferredResume"] = from_union([from_bool, from_none], self.deferred_resume) if self.device_managed_model is not None: result["deviceManagedModel"] = from_union([from_str, from_none], self.device_managed_model) + if self.managed_context_tier is not None: + result["managedContextTier"] = from_union([from_str, from_none], self.managed_context_tier) + if self.managed_reasoning_effort is not None: + result["managedReasoningEffort"] = from_union([from_str, from_none], self.managed_reasoning_effort) if self.policy_helper_model is not None: result["policyHelperModel"] = from_union([from_str, from_none], self.policy_helper_model) if self.repo_auto_tier is not None: @@ -40098,6 +46754,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: @@ -40218,8 +47043,9 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. class PermissionModeSource(Enum): - """Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK - callers. + """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. """ AUTOPILOT_CONFIRMATION = "autopilot_confirmation" CLI_FLAG = "cli_flag" @@ -40277,8 +47103,9 @@ class PermissionsSetModeRequest: sessions. """ source: PermissionModeSource | None = None - """Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK - callers. + """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. """ @staticmethod @@ -40365,91 +47192,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: @@ -40602,6 +47344,27 @@ def to_dict(self) -> dict: result["reason"] = from_union([lambda x: to_enum(SessionLimitPredictionUnavailableReason, x), from_none], self.reason) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionMCPOauthCancelLoginResult: + """Honest terminal cancellation result; persistence or recovery failures remain RPC errors.""" + + cancelled: bool + """True after cancellation settles, false when the original login already connected + successfully. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionMCPOauthCancelLoginResult': + assert isinstance(obj, dict) + cancelled = from_bool(obj.get("cancelled")) + return SessionMCPOauthCancelLoginResult(cancelled) + + def to_dict(self) -> dict: + result: dict = {} + result["cancelled"] = from_bool(self.cancelled) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionOpenOptions: @@ -40861,10 +47624,11 @@ class SessionOpenOptions: sandbox_config: SandboxConfig | None = None """Resolved sandbox configuration.""" - # Internal: this field is an internal SDK API and is not part of the public surface. - sandbox_config_source: _SandboxConfigSource | None = None - """Origin of the sandbox choice. The runtime uses this only for internal telemetry - provenance; managed policy is derived independently. + sandbox_config_source: SandboxConfigSource | None = None + """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. """ session_capabilities: list[SessionCapability] | None = None """Capabilities enabled for this session.""" @@ -40967,7 +47731,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': remote_steerable = from_union([from_bool, from_none], obj.get("remoteSteerable")) running_in_interactive_mode = from_union([from_bool, from_none], obj.get("runningInInteractiveMode")) sandbox_config = from_union([SandboxConfig.from_dict, from_none], obj.get("sandboxConfig")) - sandbox_config_source = from_union([_SandboxConfigSource, from_none], obj.get("sandboxConfigSource")) + sandbox_config_source = from_union([SandboxConfigSource, from_none], obj.get("sandboxConfigSource")) session_capabilities = from_union([lambda x: from_list(SessionCapability, x), from_none], obj.get("sessionCapabilities")) session_id = from_union([from_str, from_none], obj.get("sessionId")) session_limits = from_union([SessionLimitsConfig.from_dict, from_none], obj.get("sessionLimits")) @@ -41109,7 +47873,7 @@ def to_dict(self) -> dict: if self.sandbox_config is not None: result["sandboxConfig"] = from_union([lambda x: to_class(SandboxConfig, x), from_none], self.sandbox_config) if self.sandbox_config_source is not None: - result["sandboxConfigSource"] = from_union([lambda x: to_enum(_SandboxConfigSource, x), from_none], self.sandbox_config_source) + result["sandboxConfigSource"] = from_union([lambda x: to_enum(SandboxConfigSource, x), from_none], self.sandbox_config_source) if self.session_capabilities is not None: result["sessionCapabilities"] = from_union([lambda x: from_list(lambda x: to_enum(SessionCapability, x), x), from_none], self.session_capabilities) if self.session_id is not None: @@ -41579,6 +48343,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: @@ -41625,6 +48421,9 @@ class SlashCommandSetModelResult: model: str """Model selected by the command.""" + auto_tier: AutoTier | None = None + """Auto routing profile selected by the command, when the model is Auto.""" + reasoning_effort: str | None = None """Reasoning effort selected for the model.""" @@ -41647,18 +48446,21 @@ class SlashCommandSetModelResult: def from_dict(obj: Any) -> 'SlashCommandSetModelResult': assert isinstance(obj, dict) model = from_str(obj.get("model")) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) repo_scope = from_union([from_str, from_none], obj.get("repoScope")) revert_on_cancel = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("revertOnCancel")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) scope = from_union([from_str, from_none], obj.get("scope")) warning = from_union([from_str, from_none], obj.get("warning")) - return SlashCommandSetModelResult(model, reasoning_effort, repo_scope, revert_on_cancel, runtime_settings_changed, scope, warning) + return SlashCommandSetModelResult(model, auto_tier, reasoning_effort, repo_scope, revert_on_cancel, runtime_settings_changed, scope, warning) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind result["model"] = from_str(self.model) + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) if self.repo_scope is not None: @@ -42113,7 +48915,7 @@ class WorkflowRunDetail: run_id: str """Workflow run identifier.""" - status: RunStatus + status: WorkflowRunStatus """Current workflow run status.""" total_spawned_agent_count: int @@ -42159,7 +48961,7 @@ def from_dict(obj: Any) -> 'WorkflowRunDetail': progress = WorkflowProgressPage.from_dict(obj.get("progress")) revision = from_int(obj.get("revision")) run_id = from_str(obj.get("runId")) - status = RunStatus(obj.get("status")) + status = WorkflowRunStatus(obj.get("status")) total_spawned_agent_count = from_int(obj.get("totalSpawnedAgentCount")) updated_at = from_int(obj.get("updatedAt")) workflow_name = from_str(obj.get("workflowName")) @@ -42186,7 +48988,7 @@ def to_dict(self) -> dict: result["progress"] = to_class(WorkflowProgressPage, self.progress) result["revision"] = from_int(self.revision) result["runId"] = from_str(self.run_id) - result["status"] = to_enum(RunStatus, self.status) + result["status"] = to_enum(WorkflowRunStatus, self.status) result["totalSpawnedAgentCount"] = from_int(self.total_spawned_agent_count) result["updatedAt"] = from_int(self.updated_at) result["workflowName"] = from_str(self.workflow_name) @@ -42204,15 +49006,21 @@ 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 account_get_quota_result: AccountGetQuotaResult + account_kind: AccountKind account_login_request: AccountLoginRequest account_login_result: AccountLoginResult account_logout_request: AccountLogoutRequest account_logout_result: AccountLogoutResult account_quota_snapshot: AccountQuotaSnapshot + accounts_enumerate_request: AccountsEnumerateRequest + accounts_get_request: AccountsGetRequest + accounts_set_request: AccountsSetRequest + account_status: AccountStatus adaptive_thinking_support: AdaptiveThinkingSupport agent_discovery_path: AgentDiscoveryPath agent_discovery_path_list: AgentDiscoveryPathList @@ -42239,21 +49047,47 @@ class RPC: agent_registry_spawn_validation_error_field: AgentRegistrySpawnValidationErrorField agent_registry_spawn_validation_error_reason: AgentRegistrySpawnValidationErrorReason agent_reload_result: AgentReloadResult + agents_custom_agent_initial_model_decision_params: _AgentsCustomAgentInitialModelDecisionParams + agents_custom_agent_initial_model_decision_result: _AgentsCustomAgentInitialModelDecisionResult agents_discover_request: AgentsDiscoverRequest agent_select_request: AgentSelectRequest agent_select_result: AgentSelectResult agent_set_prompt_request: AgentSetPromptRequest + agents_get_available_builtins_request: _AgentsGetAvailableBuiltinsRequest + agents_get_available_builtins_result: _AgentsGetAvailableBuiltinsResult + agents_get_builtin_definition_request: _AgentsGetBuiltinDefinitionRequest + agents_get_builtin_definition_result: _AgentsGetBuiltinDefinitionResult + agents_get_builtin_listing_definition_request: _AgentsGetBuiltinListingDefinitionRequest + agents_get_builtin_listing_definition_result: _AgentsGetBuiltinListingDefinitionResult + agents_get_builtins_result: _AgentsGetBuiltinsResult agents_get_discovery_paths_request: AgentsGetDiscoveryPathsRequest api_key_auth_info: APIKeyAuthInfo + 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 + auth_login_cancel_request: AuthLoginCancelRequest + auth_login_result_dto: AuthLoginResultDto + auth_login_result_status: AuthLoginResultStatus + auth_login_step: AuthLoginStep + auth_read_query: AuthReadQuery + auth_read_value: AuthReadValue + auth_status_dto: AuthStatusDto auth_validation_error: AuthValidationError auth_validation_errors: list[AuthValidationError] + auth_write: AuthWrite + auth_write_result: AuthWriteResult autopilot_objective_credit_limit: AutopilotObjectiveCreditLimit autopilot_objective_get_state_result: AutopilotObjectiveGetStateResult autopilot_objective_state: AutopilotObjectiveState autopilot_objective_status: AutopilotObjectiveStatus + builtin_agent_summary: _BuiltinAgentSummary built_in_model_catalog: BuiltInModelCatalog built_in_model_catalog_entry: BuiltInModelCatalogEntry builtin_tool_descriptor: BuiltinToolDescriptor @@ -42317,7 +49151,7 @@ class RPC: catalog_mcp_server_candidate: CatalogMCPServerCandidate catalog_mcp_server_candidate_kind: CatalogMCPServerCandidateKind catalog_mcp_server_candidate_provenance: CatalogMCPServerCandidateProvenance - catalog_mcp_server_installability: CatalogMCPServerInstallabilityEnum + catalog_mcp_server_installability: CatalogMCPServerInstallability catalog_media_type: CatalogMediaType catalog_negotiated_contract: CatalogNegotiatedContract catalog_negotiation_refused_error: CatalogNegotiationRefusedError @@ -42410,9 +49244,18 @@ class RPC: connector_connect_result: ConnectorConnectResult connector_continue_request: ConnectorContinueRequest connector_disconnect_result: ConnectorDisconnectResult + connector_discovery_account: ConnectorDiscoveryAccount + connector_discovery_account_list: ConnectorDiscoveryAccountList + connector_discovery_account_request: ConnectorDiscoveryAccountRequest + connector_discovery_auth_info: ConnectorDiscoveryAuthInfo + connector_discovery_availability: ConnectorDiscoveryAvailability + connector_discovery_capabilities: ConnectorDiscoveryCapabilities + connector_discovery_catalog_entry: ConnectorDiscoveryCatalogEntry + connector_discovery_catalog_result: ConnectorDiscoveryCatalogResult 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 @@ -42431,6 +49274,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 @@ -42463,9 +49310,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 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 @@ -42501,52 +49358,6 @@ class RPC: external_tool_text_result_for_llm_content_shell_exit: ExternalToolTextResultForLlmContentShellExit external_tool_text_result_for_llm_content_terminal: ExternalToolTextResultForLlmContentTerminal external_tool_text_result_for_llm_content_text: ExternalToolTextResultForLlmContentText - factory_abort_request: FactoryAbortRequest - factory_ack_result: FactoryACKResult - factory_agent_options: FactoryAgentOptions - factory_agent_request: FactoryAgentRequest - factory_agent_result: FactoryAgentResult - factory_agent_summary: FactoryAgentSummary - factory_cancel_request: FactoryCancelRequest - factory_current_phase: FactoryCurrentPhase - factory_declared_limits: FactoryDeclaredLimits - factory_durable_operation: DurableOperation - factory_execute_request: FactoryExecuteRequest - factory_execute_result: FactoryExecuteResult - factory_get_run_progress_request: FactoryGetRunProgressRequest - factory_get_run_request: FactoryGetRunRequest - factory_journal_get_request: FactoryJournalGetRequest - factory_journal_get_result: FactoryJournalGetResult - factory_journal_put_request: FactoryJournalPutRequest - factory_list_runs_request: FactoryListRunsRequest - factory_list_runs_result: FactoryListRunsResult - factory_log_line: FactoryLogLine - factory_log_line_kind: LogLineKind - factory_log_request: FactoryLogRequest - factory_pause_checkpoint_action: PauseCheckpointAction - factory_pause_checkpoint_request: FactoryPauseCheckpointRequest - factory_pause_checkpoint_result: FactoryPauseCheckpointResult - factory_pause_info: FactoryPauseInfo - factory_pause_request: FactoryPauseRequest - factory_phase_observation: FactoryPhaseObservation - factory_phase_status: PhaseStatus - factory_progress_line: FactoryProgressLine - factory_progress_page: FactoryProgressPage - factory_resume_request: FactoryResumeRequest - factory_resume_result: FactoryResumeResult - factory_run_consumed: FactoryRunConsumed - factory_run_detail: FactoryRunDetail - factory_run_failure: FactoryRunFailure - factory_run_failure_kind: RunFailureKind - factory_run_limits: FactoryRunLimits - factory_run_request: FactoryRunRequest - factory_run_result: FactoryRunResult - factory_run_status: RunStatus - factory_run_summary: FactoryRunSummary - factory_run_terminal: FactoryRunTerminal - factory_tool_resume_request: _FactoryToolResumeRequest - factory_tool_run_options: _FactoryToolRunOptions - factory_tool_run_request: _FactoryToolRunRequest filter_mapping: dict[str, ContentFilterMode] | ContentFilterMode fleet_start_request: FleetStartRequest fleet_start_result: FleetStartResult @@ -42554,12 +49365,30 @@ class RPC: folder_trust_check_params: FolderTrustCheckParams folder_trust_check_result: FolderTrustCheckResult gh_cli_auth_info: GhCLIAuthInfo + git_current_branch_remote_result: _GitCurrentBranchRemoteResult + git_cwd_request: _GitCwdRequest + git_hub_environment: GitHubEnvironment + git_hub_owner_option: _GitHubOwnerOption + git_hub_owners_cancel_request: _GitHubOwnersCancelRequest + git_hub_owners_cancel_result: _GitHubOwnersCancelResult + git_hub_owners_list_request: _GitHubOwnersListRequest + git_hub_owners_list_result: _GitHubOwnersListResult + git_hub_owners_request_id_result: GitHubOwnersRequestIDResult + git_hub_repository_at_path_request: _GitHubRepositoryAtPathRequest + git_hub_repository_at_path_result: _GitHubRepositoryAtPathResult + git_hub_repository_identity: GitHubRepositoryIdentity git_hub_telemetry_client_info: GitHubTelemetryClientInfo git_hub_telemetry_event: GitHubTelemetryEvent git_hub_telemetry_notification: GitHubTelemetryNotification git_hub_token_acquire_reason: GitHubTokenAcquireReason git_hub_token_acquire_request: GitHubTokenAcquireRequest git_hub_token_acquire_result: GitHubTokenAcquireResult + git_remote_repository: _GitRemoteRepository + git_repos_from_remotes_request: _GitReposFromRemotesRequest + git_repos_from_remotes_result: _GitReposFromRemotesResult + global_state_load_for_config_dir_request: _GlobalStateLoadForConfigDirRequest + global_state_load_result: _GlobalStateLoadResult + global_state_write_key_request: _GlobalStateWriteKeyRequest handle_pending_tool_call_request: HandlePendingToolCallRequest handle_pending_tool_call_result: HandlePendingToolCallResult history_abort_manual_compaction_result: HistoryAbortManualCompactionResult @@ -42592,6 +49421,31 @@ class RPC: hooks_discover_request: HooksDiscoverRequest hooks_discover_result: HooksDiscoverResult hook_type: HookType + 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 + host_register_session_request: HostRegisterSessionRequest + host_session_create_callback: HostSessionCreateCallback + host_session_create_request: HostSessionCreateRequest + host_session_create_result: HostSessionCreateResult + host_session_released_notification: HostSessionReleasedNotification + host_session_release_request: HostSessionReleaseRequest + host_start_request: HostStartRequest + host_start_result: HostStartResult + installation_catalogue_identity: InstallationCatalogueIdentity + installation_confirmation_request: InstallationConfirmationRequest + installation_confirmation_response: InstallationConfirmationResponse + installation_decision: InstallationDecision + installation_review: InstallationReview installed_plugin: InstalledPlugin installed_plugin_info: InstalledPluginInfo installed_plugin_source: InstalledPluginSource | str @@ -42624,12 +49478,29 @@ class RPC: llm_inference_http_response_start_result: LlmInferenceHTTPResponseStartResult llm_inference_set_provider_result: LlmInferenceSetProviderResult local_session_metadata_value: LocalSessionMetadataValue + logged_in_user: _LoggedInUser + login_provider_kind: LoginProviderKind log_request: LogRequest 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 @@ -42639,6 +49510,8 @@ class RPC: marketplace_refresh_result: MarketplaceRefreshResult marketplace_remove_result: MarketplaceRemoveResult mcp_allowed_server: MCPAllowedServer + mcp_apply_install_request: MCPApplyInstallRequest + mcp_apply_uninstall_request: MCPApplyUninstallRequest mcp_apps_call_tool_request: MCPAppsCallToolRequest mcp_apps_diagnose_capability: MCPAppsDiagnoseCapability mcp_apps_diagnose_request: MCPAppsDiagnoseRequest @@ -42669,6 +49542,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 @@ -42689,23 +49565,44 @@ class RPC: mcp_headers_handle_pending_headers_refresh_request_request: MCPHeadersHandlePendingHeadersRefreshRequestRequest mcp_headers_handle_pending_headers_refresh_request_result: MCPHeadersHandlePendingHeadersRefreshRequestResult mcp_host_state: MCPHostState + mcp_installation_failure_reason: MCPInstallationFailureReason + mcp_installation_input: MCPInstallationInput + mcp_installation_management_outcome: MCPInstallationManagementOutcome + mcp_installation_management_result: MCPInstallationManagementResult + mcp_installation_operation_request: MCPInstallationOperationRequest + mcp_installation_operation_status: MCPInstallationOperationStatus + mcp_installation_outcome: MCPInstallationOutcome + mcp_installation_remote_configuration: MCPInstallationRemoteConfiguration + mcp_installation_result: MCPInstallationResult + mcp_installation_review: MCPInstallationReview + mcp_installation_secret: MCPInstallationSecret + mcp_installation_secret_storage: MCPInstallationSecretStorage + mcp_installations_request: MCPInstallationsRequest + mcp_installation_state: MCPInstallationState + mcp_installation_summary: MCPInstallationSummary mcp_install_plan: MCPInstallPlan mcp_is_server_running_request: MCPIsServerRunningRequest mcp_is_server_running_result: MCPIsServerRunningResult mcp_list_tools_request: MCPListToolsRequest mcp_list_tools_result: MCPListToolsResult 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 mcp_oauth_login_request: MCPOauthLoginRequest mcp_oauth_login_result: MCPOauthLoginResult mcp_oauth_pending_request_response: MCPOauthPendingRequestResponse + mcp_oauth_prepare_login_request: MCPOauthPrepareLoginRequest + mcp_oauth_prepare_login_result: MCPOauthPrepareLoginResult mcp_oauth_probe_needs_auth_reason: MCPOauthProbeNeedsAuthReason mcp_oauth_probe_request: MCPOauthProbeRequest mcp_oauth_probe_result: MCPOauthProbeResult mcp_oauth_respond_request: MCPOauthRespondRequest mcp_oauth_respond_result: MCPOauthRespondResult + mcp_owned_oauth_login_status: MCPOwnedOauthLoginStatus mcp_plan_configuration_change: MCPPlanConfigurationChange mcp_plan_configuration_operation: MCPPlanConfigurationOperation mcp_plan_enum_value_type: MCPPlan @@ -42735,11 +49632,23 @@ class RPC: mcp_plan_scope: MCPPlanScope mcp_plan_secret_placeholder: MCPPlanSecretPlaceholder mcp_plan_secret_reference: str - mcp_plan_target: MCPPlanTarget + mcp_plan_target: MCPPlanTargetClass mcp_plan_transport_choice: MCPPlanTransportChoice mcp_plan_transport_choice_package: MCPPlanTransportChoicePackage mcp_plan_transport_choice_remote: MCPPlanTransportChoiceRemote + mcp_plan_uninstall_request: MCPPlanUninstallRequest 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 @@ -42782,6 +49691,7 @@ class RPC: mcp_server_failure_info: MCPServerFailureInfo mcp_server_list: MCPServerList mcp_server_needs_auth_info: MCPServerNeedsAuthInfo + mcp_server_ownership: MCPServerOwnership mcp_set_env_value_mode_details: MCPSetEnvValueModeDetails mcp_set_env_value_mode_params: MCPSetEnvValueModeParams mcp_set_env_value_mode_result: MCPSetEnvValueModeResult @@ -42795,6 +49705,7 @@ class RPC: mcp_tools: MCPTools mcp_tool_ui: MCPToolUI mcp_tool_ui_visibility: MCPToolUIVisibility + mcp_uninstall_plan: MCPUninstallPlan mcp_unregister_external_client_request: MCPUnregisterExternalClientRequest memory_configuration: MemoryConfiguration metadata_context_attribution_result: MetadataContextAttributionResult @@ -42816,6 +49727,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 @@ -42837,6 +49749,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 @@ -42879,11 +49816,11 @@ class RPC: permission_decision_approve_for_location_approval_extension_env_access: PermissionDecisionApproveForLocationApprovalExtensionEnvAccess permission_decision_approve_for_location_approval_extension_management: PermissionDecisionApproveForLocationApprovalExtensionManagement permission_decision_approve_for_location_approval_extension_permission_access: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess - permission_decision_approve_for_location_approval_factory: PermissionDecisionApproveForLocationApprovalFactory permission_decision_approve_for_location_approval_mcp: PermissionDecisionApproveForLocationApprovalMCP permission_decision_approve_for_location_approval_mcp_sampling: PermissionDecisionApproveForLocationApprovalMCPSampling permission_decision_approve_for_location_approval_memory: PermissionDecisionApproveForLocationApprovalMemory permission_decision_approve_for_location_approval_read: PermissionDecisionApproveForLocationApprovalRead + permission_decision_approve_for_location_approval_workflow: PermissionDecisionApproveForLocationApprovalWorkflow permission_decision_approve_for_location_approval_write: PermissionDecisionApproveForLocationApprovalWrite permission_decision_approve_for_session: PermissionDecisionApproveForSession permission_decision_approve_for_session_approval: PermissionDecisionApproveForSessionApproval @@ -42892,14 +49829,15 @@ class RPC: permission_decision_approve_for_session_approval_extension_env_access: PermissionDecisionApproveForSessionApprovalExtensionEnvAccess permission_decision_approve_for_session_approval_extension_management: PermissionDecisionApproveForSessionApprovalExtensionManagement permission_decision_approve_for_session_approval_extension_permission_access: PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess - permission_decision_approve_for_session_approval_factory: PermissionDecisionApproveForSessionApprovalFactory permission_decision_approve_for_session_approval_mcp: PermissionDecisionApproveForSessionApprovalMCP permission_decision_approve_for_session_approval_mcp_sampling: PermissionDecisionApproveForSessionApprovalMCPSampling permission_decision_approve_for_session_approval_memory: PermissionDecisionApproveForSessionApprovalMemory permission_decision_approve_for_session_approval_read: PermissionDecisionApproveForSessionApprovalRead + permission_decision_approve_for_session_approval_workflow: PermissionDecisionApproveForSessionApprovalWorkflow permission_decision_approve_for_session_approval_write: PermissionDecisionApproveForSessionApprovalWrite permission_decision_approve_once: PermissionDecisionApproveOnce permission_decision_approve_permanently: PermissionDecisionApprovePermanently + permission_decision_approve_read_only_for_session: PermissionDecisionApproveReadOnlyForSession permission_decision_cancelled: PermissionDecisionCancelled permission_decision_context: PermissionDecisionContext permission_decision_denied_by_content_exclusion_policy: PermissionDecisionDeniedByContentExclusionPolicy @@ -42946,11 +49884,11 @@ class RPC: permissions_locations_add_tool_approval_details_extension_env_access: PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess permissions_locations_add_tool_approval_details_extension_management: PermissionsLocationsAddToolApprovalDetailsExtensionManagement permissions_locations_add_tool_approval_details_extension_permission_access: PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess - permissions_locations_add_tool_approval_details_factory: PermissionsLocationsAddToolApprovalDetailsFactory permissions_locations_add_tool_approval_details_mcp: PermissionsLocationsAddToolApprovalDetailsMCP permissions_locations_add_tool_approval_details_mcp_sampling: PermissionsLocationsAddToolApprovalDetailsMCPSampling permissions_locations_add_tool_approval_details_memory: PermissionsLocationsAddToolApprovalDetailsMemory permissions_locations_add_tool_approval_details_read: PermissionsLocationsAddToolApprovalDetailsRead + permissions_locations_add_tool_approval_details_workflow: PermissionsLocationsAddToolApprovalDetailsWorkflow permissions_locations_add_tool_approval_details_write: PermissionsLocationsAddToolApprovalDetailsWrite permissions_locations_add_tool_approval_result: PermissionsLocationsAddToolApprovalResult permissions_modify_rules_params: PermissionsModifyRulesParams @@ -43017,9 +49955,11 @@ 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 + provider_descriptor: ProviderDescriptor provider_endpoint: ProviderEndpoint provider_endpoint_transport: ProviderTransport provider_endpoint_type: ProviderType @@ -43031,6 +49971,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 @@ -43112,10 +50054,9 @@ class RPC: remote_session_mode: RemoteSessionMode remote_session_repository: RemoteSessionRepository response_format: ResponseFormat - run_options: RunOptions sandbox_config: SandboxConfig sandbox_config_auth: SandboxConfigAuth - sandbox_config_source: _SandboxConfigSource + sandbox_config_source: SandboxConfigSource sandbox_config_user_policy: SandboxConfigUserPolicy sandbox_config_user_policy_experimental: SandboxConfigUserPolicyExperimental sandbox_config_user_policy_experimental_seatbelt: SandboxConfigUserPolicyExperimentalSeatbelt @@ -43123,12 +50064,20 @@ class RPC: sandbox_config_user_policy_network: SandboxConfigUserPolicyNetwork sandbox_config_user_policy_network_proxy: SandboxConfigUserPolicyNetworkProxy sandbox_config_user_policy_seatbelt: SandboxConfigUserPolicySeatbelt + sandbox_credentials_config: SandboxCredentialsConfig sandbox_disable_for_session_request: SandboxDisableForSessionRequest sandbox_disable_for_session_result: SandboxDisableForSessionResult sandbox_enforcement_status: SandboxEnforcementStatus + sandbox_grant_path_for_request_request: SandboxGrantPathForRequestRequest + sandbox_grant_path_for_request_result: SandboxGrantPathForRequestResult sandbox_host_capability: SandboxHostCapability 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 @@ -43167,10 +50116,10 @@ class RPC: session_capability: SessionCapability session_commands_list_request: SessionCommandsListRequest session_completion_item: SessionCompletionItem + session_connected_ide_info: SessionConnectedIDEInfo session_context: SessionContext session_context_host_type: HostType session_enrich_metadata_result: SessionEnrichMetadataResult - session_factory_pause_at_checkpoint_result: SessionFactoryPauseAtCheckpointResult session_fs_append_file_request: SessionFSAppendFileRequest session_fs_error: SessionFSError session_fs_error_code: SessionFSErrorCode @@ -43183,6 +50132,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 @@ -43203,6 +50154,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 @@ -43231,6 +50183,11 @@ class RPC: session_managed_permissions: SessionManagedPermissions session_managed_settings: SessionManagedSettings session_mcp_apps_call_tool_result: dict[str, Any] + session_mcp_oauth_cancel_login_request: SessionMCPOauthCancelLoginRequest + session_mcp_oauth_cancel_login_result: SessionMCPOauthCancelLoginResult + session_mcp_oauth_prepare_login_request: SessionMCPOauthPrepareLoginRequest + session_mcp_oauth_prepare_login_result: SessionMCPOauthPrepareLoginResult + session_mcp_set_connected_ide_info_params: SessionMCPSetConnectedIDEInfoParams session_metadata_snapshot: SessionMetadataSnapshot session_mode: SessionMode session_model_list: SessionModelList @@ -43258,6 +50215,8 @@ class RPC: sessions_client_metadata_entry: SessionsClientMetadataEntry sessions_close_request: SessionsCloseRequest sessions_close_result: SessionsCloseResult + sessions_create_workspace_request: SessionsCreateWorkspaceRequest + sessions_create_workspace_result: SessionsCreateWorkspaceResult sessions_delete_request: SessionsDeleteRequest sessions_enrich_metadata_request: SessionsEnrichMetadataRequest session_set_credentials_params: SessionSetCredentialsParams @@ -43295,6 +50254,8 @@ class RPC: sessions_list_non_empty_session_ids_result: SessionsListNonEmptySessionIDSResult sessions_list_request: SessionsListRequest sessions_load_deferred_repo_hooks_request: SessionsLoadDeferredRepoHooksRequest + sessions_load_workspace_request: SessionsLoadWorkspaceRequest + sessions_load_workspace_result: SessionsLoadWorkspaceResult sessions_open_attach: SessionsOpenAttach sessions_open_cloud: SessionsOpenCloud sessions_open_create: SessionsOpenCreate @@ -43322,6 +50283,8 @@ class RPC: sessions_start_remote_control_request: SessionsStartRemoteControlRequest sessions_stop_remote_control_request: SessionsStopRemoteControlRequest sessions_transfer_remote_control_request: SessionsTransferRemoteControlRequest + sessions_update_workspace_fields_request: SessionsUpdateWorkspaceFieldsRequest + sessions_update_workspace_fields_result: SessionsUpdateWorkspaceFieldsResult session_telemetry_engagement: SessionTelemetryEngagement session_update_options_params: SessionUpdateOptionsParams session_update_options_result: SessionUpdateOptionsResult @@ -43329,6 +50292,7 @@ class RPC: session_workflow_pause_at_checkpoint_result: SessionWorkflowPauseAtCheckpointResult session_working_directory_context: SessionWorkingDirectoryContext session_working_directory_context_host_type: HostType + session_working_directory_context_with_client: SessionWorkingDirectoryContextWithClient settable_auth_info: SettableAuthInfo settable_token_auth_info: SettableTokenAuthInfo shell_cancel_user_requested_request: ShellCancelUserRequestedRequest @@ -43345,10 +50309,31 @@ class RPC: shell_options: ShellOptions shutdown_request: ShutdownRequest skill: Skill + skill_apply_install_request: SkillApplyInstallRequest + skill_apply_uninstall_request: SkillApplyUninstallRequest skill_discovery_path: SkillDiscoveryPath skill_discovery_path_list: SkillDiscoveryPathList skill_discovery_scope: SkillDiscoveryScope + skill_installation_failure_reason: SkillInstallationFailureReason + skill_installation_file_review: SkillInstallationFileReview + skill_installation_location: SkillInstallationLocation + skill_installation_management_outcome: SkillInstallationManagementOutcome + skill_installation_management_result: SkillInstallationManagementResult + skill_installation_operation_request: SkillInstallationOperationRequest + skill_installation_operation_status: SkillInstallationOperationStatus + skill_installation_outcome: SkillInstallationOutcome + skill_installation_ownership_state: SkillInstallationOwnershipState + skill_installation_result: SkillInstallationResult + skill_installation_review: SkillInstallationReview + skill_installation_scope: SkillInstallationScope + skill_installation_session_state: SkillInstallationSessionState + skill_installation_source: SkillInstallationSource + skill_installations_request: SkillInstallationsRequest + skill_installation_summary: SkillInstallationSummary + skill_install_plan: SkillInstallPlan skill_list: SkillList + skill_plan_install_request: SkillPlanInstallRequest + skill_plan_uninstall_request: SkillPlanUninstallRequest skill_provider_descriptor: SkillProviderDescriptor skill_provider_list_request: SkillProviderListRequest skill_provider_list_result: _SkillProviderListResult @@ -43359,10 +50344,12 @@ class RPC: skills_disable_request: SkillsDisableRequest skills_discover_request: SkillsDiscoverRequest skills_enable_request: SkillsEnableRequest + skill_set_enabled_request: SkillSetEnabledRequest skills_get_discovery_paths_request: SkillsGetDiscoveryPathsRequest skills_get_invoked_result: SkillsGetInvokedResult skills_invoked_skill: SkillsInvokedSkill skills_load_diagnostics: SkillsLoadDiagnostics + skill_uninstall_plan: SkillUninstallPlan slash_command_add_timeline_entry_result: SlashCommandAddTimelineEntryResult slash_command_agent_prompt_result: SlashCommandAgentPromptResult slash_command_completed_result: SlashCommandCompletedResult @@ -43501,7 +50488,6 @@ class RPC: user_setting_metadata: UserSettingMetadata user_settings_get_result: UserSettingsGetResult user_settings_set_request: UserSettingsSetRequest - user_settings_set_result: UserSettingsSetResult visibility_get_result: VisibilityGetResult visibility_set_request: VisibilitySetRequest visibility_set_result: VisibilitySetResult @@ -43514,7 +50500,7 @@ class RPC: workflow_cancel_request: WorkflowCancelRequest workflow_current_phase: WorkflowCurrentPhase workflow_declared_limits: WorkflowDeclaredLimits - workflow_durable_operation: DurableOperation + workflow_durable_operation: WorkflowDurableOperation workflow_execute_request: WorkflowExecuteRequest workflow_execute_result: WorkflowExecuteResult workflow_get_run_progress_request: WorkflowGetRunProgressRequest @@ -43525,15 +50511,15 @@ class RPC: workflow_list_runs_request: WorkflowListRunsRequest workflow_list_runs_result: WorkflowListRunsResult workflow_log_line: WorkflowLogLine - workflow_log_line_kind: LogLineKind + workflow_log_line_kind: WorkflowLogLineKind workflow_log_request: WorkflowLogRequest - workflow_pause_checkpoint_action: PauseCheckpointAction + workflow_pause_checkpoint_action: WorkflowPauseCheckpointAction workflow_pause_checkpoint_request: WorkflowPauseCheckpointRequest workflow_pause_checkpoint_result: WorkflowPauseCheckpointResult workflow_pause_info: WorkflowPauseInfo workflow_pause_request: WorkflowPauseRequest workflow_phase_observation: WorkflowPhaseObservation - workflow_phase_status: PhaseStatus + workflow_phase_status: WorkflowPhaseStatus workflow_progress_line: WorkflowProgressLine workflow_progress_page: WorkflowProgressPage workflow_resume_request: WorkflowResumeRequest @@ -43541,12 +50527,12 @@ class RPC: workflow_run_consumed: WorkflowRunConsumed workflow_run_detail: WorkflowRunDetail workflow_run_failure: WorkflowRunFailure - workflow_run_failure_kind: RunFailureKind + workflow_run_failure_kind: WorkflowRunFailureKind workflow_run_limits: WorkflowRunLimits workflow_run_options: WorkflowRunOptions workflow_run_request: WorkflowRunRequest workflow_run_result: WorkflowRunResult - workflow_run_status: RunStatus + workflow_run_status: WorkflowRunStatus workflow_run_summary: WorkflowRunSummary workflow_run_terminal: WorkflowRunTerminal workflow_tool_resume_request: _WorkflowToolResumeRequest @@ -43585,6 +50571,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 @@ -43599,15 +50586,21 @@ 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")) account_get_quota_result = AccountGetQuotaResult.from_dict(obj.get("AccountGetQuotaResult")) + account_kind = AccountKind(obj.get("AccountKind")) account_login_request = AccountLoginRequest.from_dict(obj.get("AccountLoginRequest")) account_login_result = AccountLoginResult.from_dict(obj.get("AccountLoginResult")) account_logout_request = AccountLogoutRequest.from_dict(obj.get("AccountLogoutRequest")) account_logout_result = AccountLogoutResult.from_dict(obj.get("AccountLogoutResult")) account_quota_snapshot = AccountQuotaSnapshot.from_dict(obj.get("AccountQuotaSnapshot")) + accounts_enumerate_request = AccountsEnumerateRequest.from_dict(obj.get("AccountsEnumerateRequest")) + accounts_get_request = AccountsGetRequest.from_dict(obj.get("AccountsGetRequest")) + accounts_set_request = AccountsSetRequest.from_dict(obj.get("AccountsSetRequest")) + account_status = AccountStatus.from_dict(obj.get("AccountStatus")) adaptive_thinking_support = AdaptiveThinkingSupport(obj.get("AdaptiveThinkingSupport")) agent_discovery_path = AgentDiscoveryPath.from_dict(obj.get("AgentDiscoveryPath")) agent_discovery_path_list = AgentDiscoveryPathList.from_dict(obj.get("AgentDiscoveryPathList")) @@ -43634,21 +50627,47 @@ def from_dict(obj: Any) -> 'RPC': agent_registry_spawn_validation_error_field = AgentRegistrySpawnValidationErrorField(obj.get("AgentRegistrySpawnValidationErrorField")) agent_registry_spawn_validation_error_reason = AgentRegistrySpawnValidationErrorReason(obj.get("AgentRegistrySpawnValidationErrorReason")) agent_reload_result = AgentReloadResult.from_dict(obj.get("AgentReloadResult")) + agents_custom_agent_initial_model_decision_params = _AgentsCustomAgentInitialModelDecisionParams.from_dict(obj.get("AgentsCustomAgentInitialModelDecisionParams")) + agents_custom_agent_initial_model_decision_result = _AgentsCustomAgentInitialModelDecisionResult.from_dict(obj.get("AgentsCustomAgentInitialModelDecisionResult")) agents_discover_request = AgentsDiscoverRequest.from_dict(obj.get("AgentsDiscoverRequest")) agent_select_request = AgentSelectRequest.from_dict(obj.get("AgentSelectRequest")) agent_select_result = AgentSelectResult.from_dict(obj.get("AgentSelectResult")) agent_set_prompt_request = AgentSetPromptRequest.from_dict(obj.get("AgentSetPromptRequest")) + agents_get_available_builtins_request = _AgentsGetAvailableBuiltinsRequest.from_dict(obj.get("AgentsGetAvailableBuiltinsRequest")) + agents_get_available_builtins_result = _AgentsGetAvailableBuiltinsResult.from_dict(obj.get("AgentsGetAvailableBuiltinsResult")) + agents_get_builtin_definition_request = _AgentsGetBuiltinDefinitionRequest.from_dict(obj.get("AgentsGetBuiltinDefinitionRequest")) + agents_get_builtin_definition_result = _AgentsGetBuiltinDefinitionResult.from_dict(obj.get("AgentsGetBuiltinDefinitionResult")) + agents_get_builtin_listing_definition_request = _AgentsGetBuiltinListingDefinitionRequest.from_dict(obj.get("AgentsGetBuiltinListingDefinitionRequest")) + agents_get_builtin_listing_definition_result = _AgentsGetBuiltinListingDefinitionResult.from_dict(obj.get("AgentsGetBuiltinListingDefinitionResult")) + agents_get_builtins_result = _AgentsGetBuiltinsResult.from_dict(obj.get("AgentsGetBuiltinsResult")) agents_get_discovery_paths_request = AgentsGetDiscoveryPathsRequest.from_dict(obj.get("AgentsGetDiscoveryPathsRequest")) api_key_auth_info = APIKeyAuthInfo.from_dict(obj.get("ApiKeyAuthInfo")) + 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")) + auth_login_cancel_request = AuthLoginCancelRequest.from_dict(obj.get("AuthLoginCancelRequest")) + auth_login_result_dto = AuthLoginResultDto.from_dict(obj.get("AuthLoginResultDto")) + auth_login_result_status = AuthLoginResultStatus(obj.get("AuthLoginResultStatus")) + auth_login_step = AuthLoginStep.from_dict(obj.get("AuthLoginStep")) + auth_read_query = AuthReadQuery.from_dict(obj.get("AuthReadQuery")) + auth_read_value = AuthReadValue.from_dict(obj.get("AuthReadValue")) + auth_status_dto = AuthStatusDto.from_dict(obj.get("AuthStatusDto")) auth_validation_error = AuthValidationError.from_dict(obj.get("AuthValidationError")) auth_validation_errors = from_list(AuthValidationError.from_dict, obj.get("AuthValidationErrors")) + auth_write = AuthWrite.from_dict(obj.get("AuthWrite")) + auth_write_result = AuthWriteResult.from_dict(obj.get("AuthWriteResult")) autopilot_objective_credit_limit = AutopilotObjectiveCreditLimit.from_dict(obj.get("AutopilotObjectiveCreditLimit")) autopilot_objective_get_state_result = AutopilotObjectiveGetStateResult.from_dict(obj.get("AutopilotObjectiveGetStateResult")) autopilot_objective_state = AutopilotObjectiveState.from_dict(obj.get("AutopilotObjectiveState")) autopilot_objective_status = AutopilotObjectiveStatus(obj.get("AutopilotObjectiveStatus")) + builtin_agent_summary = _BuiltinAgentSummary.from_dict(obj.get("BuiltinAgentSummary")) built_in_model_catalog = BuiltInModelCatalog.from_dict(obj.get("BuiltInModelCatalog")) built_in_model_catalog_entry = BuiltInModelCatalogEntry.from_dict(obj.get("BuiltInModelCatalogEntry")) builtin_tool_descriptor = BuiltinToolDescriptor.from_dict(obj.get("BuiltinToolDescriptor")) @@ -43712,7 +50731,7 @@ def from_dict(obj: Any) -> 'RPC': catalog_mcp_server_candidate = CatalogMCPServerCandidate.from_dict(obj.get("CatalogMcpServerCandidate")) catalog_mcp_server_candidate_kind = CatalogMCPServerCandidateKind(obj.get("CatalogMcpServerCandidateKind")) catalog_mcp_server_candidate_provenance = CatalogMCPServerCandidateProvenance.from_dict(obj.get("CatalogMcpServerCandidateProvenance")) - catalog_mcp_server_installability = CatalogMCPServerInstallabilityEnum(obj.get("CatalogMcpServerInstallability")) + catalog_mcp_server_installability = CatalogMCPServerInstallability(obj.get("CatalogMcpServerInstallability")) catalog_media_type = CatalogMediaType(obj.get("CatalogMediaType")) catalog_negotiated_contract = CatalogNegotiatedContract.from_dict(obj.get("CatalogNegotiatedContract")) catalog_negotiation_refused_error = CatalogNegotiationRefusedError.from_dict(obj.get("CatalogNegotiationRefusedError")) @@ -43805,9 +50824,18 @@ def from_dict(obj: Any) -> 'RPC': connector_connect_result = ConnectorConnectResult.from_dict(obj.get("ConnectorConnectResult")) connector_continue_request = ConnectorContinueRequest.from_dict(obj.get("ConnectorContinueRequest")) connector_disconnect_result = ConnectorDisconnectResult.from_dict(obj.get("ConnectorDisconnectResult")) + connector_discovery_account = ConnectorDiscoveryAccount.from_dict(obj.get("ConnectorDiscoveryAccount")) + connector_discovery_account_list = ConnectorDiscoveryAccountList.from_dict(obj.get("ConnectorDiscoveryAccountList")) + connector_discovery_account_request = ConnectorDiscoveryAccountRequest.from_dict(obj.get("ConnectorDiscoveryAccountRequest")) + connector_discovery_auth_info = ConnectorDiscoveryAuthInfo.from_dict(obj.get("ConnectorDiscoveryAuthInfo")) + connector_discovery_availability = ConnectorDiscoveryAvailability(obj.get("ConnectorDiscoveryAvailability")) + connector_discovery_capabilities = ConnectorDiscoveryCapabilities.from_dict(obj.get("ConnectorDiscoveryCapabilities")) + connector_discovery_catalog_entry = ConnectorDiscoveryCatalogEntry.from_dict(obj.get("ConnectorDiscoveryCatalogEntry")) + connector_discovery_catalog_result = ConnectorDiscoveryCatalogResult.from_dict(obj.get("ConnectorDiscoveryCatalogResult")) 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")) @@ -43826,6 +50854,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")) @@ -43858,9 +50890,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")) 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")) @@ -43896,52 +50938,6 @@ def from_dict(obj: Any) -> 'RPC': external_tool_text_result_for_llm_content_shell_exit = ExternalToolTextResultForLlmContentShellExit.from_dict(obj.get("ExternalToolTextResultForLlmContentShellExit")) external_tool_text_result_for_llm_content_terminal = ExternalToolTextResultForLlmContentTerminal.from_dict(obj.get("ExternalToolTextResultForLlmContentTerminal")) external_tool_text_result_for_llm_content_text = ExternalToolTextResultForLlmContentText.from_dict(obj.get("ExternalToolTextResultForLlmContentText")) - factory_abort_request = FactoryAbortRequest.from_dict(obj.get("FactoryAbortRequest")) - factory_ack_result = FactoryACKResult.from_dict(obj.get("FactoryAckResult")) - factory_agent_options = FactoryAgentOptions.from_dict(obj.get("FactoryAgentOptions")) - factory_agent_request = FactoryAgentRequest.from_dict(obj.get("FactoryAgentRequest")) - factory_agent_result = FactoryAgentResult.from_dict(obj.get("FactoryAgentResult")) - factory_agent_summary = FactoryAgentSummary.from_dict(obj.get("FactoryAgentSummary")) - factory_cancel_request = FactoryCancelRequest.from_dict(obj.get("FactoryCancelRequest")) - factory_current_phase = FactoryCurrentPhase.from_dict(obj.get("FactoryCurrentPhase")) - factory_declared_limits = FactoryDeclaredLimits.from_dict(obj.get("FactoryDeclaredLimits")) - factory_durable_operation = DurableOperation(obj.get("FactoryDurableOperation")) - factory_execute_request = FactoryExecuteRequest.from_dict(obj.get("FactoryExecuteRequest")) - factory_execute_result = FactoryExecuteResult.from_dict(obj.get("FactoryExecuteResult")) - factory_get_run_progress_request = FactoryGetRunProgressRequest.from_dict(obj.get("FactoryGetRunProgressRequest")) - factory_get_run_request = FactoryGetRunRequest.from_dict(obj.get("FactoryGetRunRequest")) - factory_journal_get_request = FactoryJournalGetRequest.from_dict(obj.get("FactoryJournalGetRequest")) - factory_journal_get_result = FactoryJournalGetResult.from_dict(obj.get("FactoryJournalGetResult")) - factory_journal_put_request = FactoryJournalPutRequest.from_dict(obj.get("FactoryJournalPutRequest")) - factory_list_runs_request = FactoryListRunsRequest.from_dict(obj.get("FactoryListRunsRequest")) - factory_list_runs_result = FactoryListRunsResult.from_dict(obj.get("FactoryListRunsResult")) - factory_log_line = FactoryLogLine.from_dict(obj.get("FactoryLogLine")) - factory_log_line_kind = LogLineKind(obj.get("FactoryLogLineKind")) - factory_log_request = FactoryLogRequest.from_dict(obj.get("FactoryLogRequest")) - factory_pause_checkpoint_action = PauseCheckpointAction(obj.get("FactoryPauseCheckpointAction")) - factory_pause_checkpoint_request = FactoryPauseCheckpointRequest.from_dict(obj.get("FactoryPauseCheckpointRequest")) - factory_pause_checkpoint_result = FactoryPauseCheckpointResult.from_dict(obj.get("FactoryPauseCheckpointResult")) - factory_pause_info = FactoryPauseInfo.from_dict(obj.get("FactoryPauseInfo")) - factory_pause_request = FactoryPauseRequest.from_dict(obj.get("FactoryPauseRequest")) - factory_phase_observation = FactoryPhaseObservation.from_dict(obj.get("FactoryPhaseObservation")) - factory_phase_status = PhaseStatus(obj.get("FactoryPhaseStatus")) - factory_progress_line = FactoryProgressLine.from_dict(obj.get("FactoryProgressLine")) - factory_progress_page = FactoryProgressPage.from_dict(obj.get("FactoryProgressPage")) - factory_resume_request = FactoryResumeRequest.from_dict(obj.get("FactoryResumeRequest")) - factory_resume_result = FactoryResumeResult.from_dict(obj.get("FactoryResumeResult")) - factory_run_consumed = FactoryRunConsumed.from_dict(obj.get("FactoryRunConsumed")) - factory_run_detail = FactoryRunDetail.from_dict(obj.get("FactoryRunDetail")) - factory_run_failure = FactoryRunFailure.from_dict(obj.get("FactoryRunFailure")) - factory_run_failure_kind = RunFailureKind(obj.get("FactoryRunFailureKind")) - factory_run_limits = FactoryRunLimits.from_dict(obj.get("FactoryRunLimits")) - factory_run_request = FactoryRunRequest.from_dict(obj.get("FactoryRunRequest")) - factory_run_result = FactoryRunResult.from_dict(obj.get("FactoryRunResult")) - factory_run_status = RunStatus(obj.get("FactoryRunStatus")) - factory_run_summary = FactoryRunSummary.from_dict(obj.get("FactoryRunSummary")) - factory_run_terminal = FactoryRunTerminal.from_dict(obj.get("FactoryRunTerminal")) - factory_tool_resume_request = _FactoryToolResumeRequest.from_dict(obj.get("FactoryToolResumeRequest")) - factory_tool_run_options = _FactoryToolRunOptions.from_dict(obj.get("FactoryToolRunOptions")) - factory_tool_run_request = _FactoryToolRunRequest.from_dict(obj.get("FactoryToolRunRequest")) filter_mapping = from_union([lambda x: from_dict(ContentFilterMode, x), ContentFilterMode], obj.get("FilterMapping")) fleet_start_request = FleetStartRequest.from_dict(obj.get("FleetStartRequest")) fleet_start_result = FleetStartResult.from_dict(obj.get("FleetStartResult")) @@ -43949,12 +50945,30 @@ 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_current_branch_remote_result = _GitCurrentBranchRemoteResult.from_dict(obj.get("GitCurrentBranchRemoteResult")) + git_cwd_request = _GitCwdRequest.from_dict(obj.get("GitCwdRequest")) + git_hub_environment = GitHubEnvironment.from_dict(obj.get("GitHubEnvironment")) + git_hub_owner_option = _GitHubOwnerOption.from_dict(obj.get("GitHubOwnerOption")) + git_hub_owners_cancel_request = _GitHubOwnersCancelRequest.from_dict(obj.get("GitHubOwnersCancelRequest")) + git_hub_owners_cancel_result = _GitHubOwnersCancelResult.from_dict(obj.get("GitHubOwnersCancelResult")) + git_hub_owners_list_request = _GitHubOwnersListRequest.from_dict(obj.get("GitHubOwnersListRequest")) + git_hub_owners_list_result = _GitHubOwnersListResult.from_dict(obj.get("GitHubOwnersListResult")) + git_hub_owners_request_id_result = GitHubOwnersRequestIDResult.from_dict(obj.get("GitHubOwnersRequestIdResult")) + git_hub_repository_at_path_request = _GitHubRepositoryAtPathRequest.from_dict(obj.get("GitHubRepositoryAtPathRequest")) + git_hub_repository_at_path_result = _GitHubRepositoryAtPathResult.from_dict(obj.get("GitHubRepositoryAtPathResult")) + git_hub_repository_identity = GitHubRepositoryIdentity.from_dict(obj.get("GitHubRepositoryIdentity")) 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")) git_hub_token_acquire_reason = GitHubTokenAcquireReason(obj.get("GitHubTokenAcquireReason")) git_hub_token_acquire_request = GitHubTokenAcquireRequest.from_dict(obj.get("GitHubTokenAcquireRequest")) git_hub_token_acquire_result = GitHubTokenAcquireResult.from_dict(obj.get("GitHubTokenAcquireResult")) + git_remote_repository = _GitRemoteRepository.from_dict(obj.get("GitRemoteRepository")) + git_repos_from_remotes_request = _GitReposFromRemotesRequest.from_dict(obj.get("GitReposFromRemotesRequest")) + git_repos_from_remotes_result = _GitReposFromRemotesResult.from_dict(obj.get("GitReposFromRemotesResult")) + global_state_load_for_config_dir_request = _GlobalStateLoadForConfigDirRequest.from_dict(obj.get("GlobalStateLoadForConfigDirRequest")) + global_state_load_result = _GlobalStateLoadResult.from_dict(obj.get("GlobalStateLoadResult")) + global_state_write_key_request = _GlobalStateWriteKeyRequest.from_dict(obj.get("GlobalStateWriteKeyRequest")) handle_pending_tool_call_request = HandlePendingToolCallRequest.from_dict(obj.get("HandlePendingToolCallRequest")) handle_pending_tool_call_result = HandlePendingToolCallResult.from_dict(obj.get("HandlePendingToolCallResult")) history_abort_manual_compaction_result = HistoryAbortManualCompactionResult.from_dict(obj.get("HistoryAbortManualCompactionResult")) @@ -43987,6 +51001,31 @@ def from_dict(obj: Any) -> 'RPC': hooks_discover_request = HooksDiscoverRequest.from_dict(obj.get("HooksDiscoverRequest")) hooks_discover_result = HooksDiscoverResult.from_dict(obj.get("HooksDiscoverResult")) hook_type = HookType(obj.get("HookType")) + 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")) + host_register_session_request = HostRegisterSessionRequest.from_dict(obj.get("HostRegisterSessionRequest")) + host_session_create_callback = HostSessionCreateCallback.from_dict(obj.get("HostSessionCreateCallback")) + host_session_create_request = HostSessionCreateRequest.from_dict(obj.get("HostSessionCreateRequest")) + host_session_create_result = HostSessionCreateResult.from_dict(obj.get("HostSessionCreateResult")) + host_session_released_notification = HostSessionReleasedNotification.from_dict(obj.get("HostSessionReleasedNotification")) + host_session_release_request = HostSessionReleaseRequest.from_dict(obj.get("HostSessionReleaseRequest")) + host_start_request = HostStartRequest.from_dict(obj.get("HostStartRequest")) + host_start_result = HostStartResult.from_dict(obj.get("HostStartResult")) + installation_catalogue_identity = InstallationCatalogueIdentity.from_dict(obj.get("InstallationCatalogueIdentity")) + installation_confirmation_request = InstallationConfirmationRequest.from_dict(obj.get("InstallationConfirmationRequest")) + installation_confirmation_response = InstallationConfirmationResponse.from_dict(obj.get("InstallationConfirmationResponse")) + installation_decision = InstallationDecision(obj.get("InstallationDecision")) + installation_review = InstallationReview.from_dict(obj.get("InstallationReview")) installed_plugin = InstalledPlugin.from_dict(obj.get("InstalledPlugin")) installed_plugin_info = InstalledPluginInfo.from_dict(obj.get("InstalledPluginInfo")) installed_plugin_source = from_union([InstalledPluginSource.from_dict, from_str], obj.get("InstalledPluginSource")) @@ -44019,12 +51058,29 @@ def from_dict(obj: Any) -> 'RPC': llm_inference_http_response_start_result = LlmInferenceHTTPResponseStartResult.from_dict(obj.get("LlmInferenceHttpResponseStartResult")) llm_inference_set_provider_result = LlmInferenceSetProviderResult.from_dict(obj.get("LlmInferenceSetProviderResult")) local_session_metadata_value = LocalSessionMetadataValue.from_dict(obj.get("LocalSessionMetadataValue")) + logged_in_user = _LoggedInUser.from_dict(obj.get("LoggedInUser")) + login_provider_kind = LoginProviderKind(obj.get("LoginProviderKind")) log_request = LogRequest.from_dict(obj.get("LogRequest")) 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")) @@ -44034,6 +51090,8 @@ def from_dict(obj: Any) -> 'RPC': marketplace_refresh_result = MarketplaceRefreshResult.from_dict(obj.get("MarketplaceRefreshResult")) marketplace_remove_result = MarketplaceRemoveResult.from_dict(obj.get("MarketplaceRemoveResult")) mcp_allowed_server = MCPAllowedServer.from_dict(obj.get("McpAllowedServer")) + mcp_apply_install_request = MCPApplyInstallRequest.from_dict(obj.get("McpApplyInstallRequest")) + mcp_apply_uninstall_request = MCPApplyUninstallRequest.from_dict(obj.get("McpApplyUninstallRequest")) mcp_apps_call_tool_request = MCPAppsCallToolRequest.from_dict(obj.get("McpAppsCallToolRequest")) mcp_apps_diagnose_capability = MCPAppsDiagnoseCapability.from_dict(obj.get("McpAppsDiagnoseCapability")) mcp_apps_diagnose_request = MCPAppsDiagnoseRequest.from_dict(obj.get("McpAppsDiagnoseRequest")) @@ -44064,6 +51122,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")) @@ -44084,23 +51145,44 @@ def from_dict(obj: Any) -> 'RPC': mcp_headers_handle_pending_headers_refresh_request_request = MCPHeadersHandlePendingHeadersRefreshRequestRequest.from_dict(obj.get("McpHeadersHandlePendingHeadersRefreshRequestRequest")) mcp_headers_handle_pending_headers_refresh_request_result = MCPHeadersHandlePendingHeadersRefreshRequestResult.from_dict(obj.get("McpHeadersHandlePendingHeadersRefreshRequestResult")) mcp_host_state = MCPHostState.from_dict(obj.get("McpHostState")) + mcp_installation_failure_reason = MCPInstallationFailureReason(obj.get("McpInstallationFailureReason")) + mcp_installation_input = MCPInstallationInput.from_dict(obj.get("McpInstallationInput")) + mcp_installation_management_outcome = MCPInstallationManagementOutcome.from_dict(obj.get("McpInstallationManagementOutcome")) + mcp_installation_management_result = MCPInstallationManagementResult.from_dict(obj.get("McpInstallationManagementResult")) + mcp_installation_operation_request = MCPInstallationOperationRequest.from_dict(obj.get("McpInstallationOperationRequest")) + mcp_installation_operation_status = MCPInstallationOperationStatus.from_dict(obj.get("McpInstallationOperationStatus")) + mcp_installation_outcome = MCPInstallationOutcome.from_dict(obj.get("McpInstallationOutcome")) + mcp_installation_remote_configuration = MCPInstallationRemoteConfiguration.from_dict(obj.get("McpInstallationRemoteConfiguration")) + mcp_installation_result = MCPInstallationResult.from_dict(obj.get("McpInstallationResult")) + mcp_installation_review = MCPInstallationReview.from_dict(obj.get("McpInstallationReview")) + mcp_installation_secret = MCPInstallationSecret.from_dict(obj.get("McpInstallationSecret")) + mcp_installation_secret_storage = MCPInstallationSecretStorage(obj.get("McpInstallationSecretStorage")) + mcp_installations_request = MCPInstallationsRequest.from_dict(obj.get("McpInstallationsRequest")) + mcp_installation_state = MCPInstallationState(obj.get("McpInstallationState")) + mcp_installation_summary = MCPInstallationSummary.from_dict(obj.get("McpInstallationSummary")) mcp_install_plan = MCPInstallPlan.from_dict(obj.get("McpInstallPlan")) mcp_is_server_running_request = MCPIsServerRunningRequest.from_dict(obj.get("McpIsServerRunningRequest")) mcp_is_server_running_result = MCPIsServerRunningResult.from_dict(obj.get("McpIsServerRunningResult")) mcp_list_tools_request = MCPListToolsRequest.from_dict(obj.get("McpListToolsRequest")) mcp_list_tools_result = MCPListToolsResult.from_dict(obj.get("McpListToolsResult")) 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")) mcp_oauth_login_request = MCPOauthLoginRequest.from_dict(obj.get("McpOauthLoginRequest")) mcp_oauth_login_result = MCPOauthLoginResult.from_dict(obj.get("McpOauthLoginResult")) mcp_oauth_pending_request_response = MCPOauthPendingRequestResponse.from_dict(obj.get("McpOauthPendingRequestResponse")) + mcp_oauth_prepare_login_request = MCPOauthPrepareLoginRequest.from_dict(obj.get("McpOauthPrepareLoginRequest")) + mcp_oauth_prepare_login_result = MCPOauthPrepareLoginResult.from_dict(obj.get("McpOauthPrepareLoginResult")) mcp_oauth_probe_needs_auth_reason = MCPOauthProbeNeedsAuthReason(obj.get("McpOauthProbeNeedsAuthReason")) mcp_oauth_probe_request = MCPOauthProbeRequest.from_dict(obj.get("McpOauthProbeRequest")) mcp_oauth_probe_result = MCPOauthProbeResult.from_dict(obj.get("McpOauthProbeResult")) mcp_oauth_respond_request = MCPOauthRespondRequest.from_dict(obj.get("McpOauthRespondRequest")) mcp_oauth_respond_result = MCPOauthRespondResult.from_dict(obj.get("McpOauthRespondResult")) + mcp_owned_oauth_login_status = MCPOwnedOauthLoginStatus(obj.get("McpOwnedOauthLoginStatus")) mcp_plan_configuration_change = MCPPlanConfigurationChange.from_dict(obj.get("McpPlanConfigurationChange")) mcp_plan_configuration_operation = MCPPlanConfigurationOperation(obj.get("McpPlanConfigurationOperation")) mcp_plan_enum_value_type = MCPPlan(obj.get("McpPlanEnumValueType")) @@ -44130,11 +51212,23 @@ def from_dict(obj: Any) -> 'RPC': mcp_plan_scope = MCPPlanScope(obj.get("McpPlanScope")) mcp_plan_secret_placeholder = MCPPlanSecretPlaceholder.from_dict(obj.get("McpPlanSecretPlaceholder")) mcp_plan_secret_reference = from_str(obj.get("McpPlanSecretReference")) - mcp_plan_target = MCPPlanTarget.from_dict(obj.get("McpPlanTarget")) + mcp_plan_target = MCPPlanTargetClass.from_dict(obj.get("McpPlanTarget")) mcp_plan_transport_choice = _load_MCPPlanTransportChoice(obj.get("McpPlanTransportChoice")) mcp_plan_transport_choice_package = MCPPlanTransportChoicePackage.from_dict(obj.get("McpPlanTransportChoicePackage")) mcp_plan_transport_choice_remote = MCPPlanTransportChoiceRemote.from_dict(obj.get("McpPlanTransportChoiceRemote")) + mcp_plan_uninstall_request = MCPPlanUninstallRequest.from_dict(obj.get("McpPlanUninstallRequest")) 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")) @@ -44177,6 +51271,7 @@ def from_dict(obj: Any) -> 'RPC': mcp_server_failure_info = MCPServerFailureInfo.from_dict(obj.get("McpServerFailureInfo")) mcp_server_list = MCPServerList.from_dict(obj.get("McpServerList")) mcp_server_needs_auth_info = MCPServerNeedsAuthInfo.from_dict(obj.get("McpServerNeedsAuthInfo")) + mcp_server_ownership = MCPServerOwnership.from_dict(obj.get("McpServerOwnership")) mcp_set_env_value_mode_details = MCPSetEnvValueModeDetails(obj.get("McpSetEnvValueModeDetails")) mcp_set_env_value_mode_params = MCPSetEnvValueModeParams.from_dict(obj.get("McpSetEnvValueModeParams")) mcp_set_env_value_mode_result = MCPSetEnvValueModeResult.from_dict(obj.get("McpSetEnvValueModeResult")) @@ -44190,6 +51285,7 @@ def from_dict(obj: Any) -> 'RPC': mcp_tools = MCPTools.from_dict(obj.get("McpTools")) mcp_tool_ui = MCPToolUI.from_dict(obj.get("McpToolUi")) mcp_tool_ui_visibility = MCPToolUIVisibility(obj.get("McpToolUiVisibility")) + mcp_uninstall_plan = MCPUninstallPlan.from_dict(obj.get("McpUninstallPlan")) mcp_unregister_external_client_request = MCPUnregisterExternalClientRequest.from_dict(obj.get("McpUnregisterExternalClientRequest")) memory_configuration = MemoryConfiguration.from_dict(obj.get("MemoryConfiguration")) metadata_context_attribution_result = MetadataContextAttributionResult.from_dict(obj.get("MetadataContextAttributionResult")) @@ -44211,6 +51307,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")) @@ -44232,6 +51329,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")) @@ -44274,11 +51396,11 @@ def from_dict(obj: Any) -> 'RPC': permission_decision_approve_for_location_approval_extension_env_access = PermissionDecisionApproveForLocationApprovalExtensionEnvAccess.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalExtensionEnvAccess")) permission_decision_approve_for_location_approval_extension_management = PermissionDecisionApproveForLocationApprovalExtensionManagement.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalExtensionManagement")) permission_decision_approve_for_location_approval_extension_permission_access = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess")) - permission_decision_approve_for_location_approval_factory = PermissionDecisionApproveForLocationApprovalFactory.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalFactory")) permission_decision_approve_for_location_approval_mcp = PermissionDecisionApproveForLocationApprovalMCP.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalMcp")) permission_decision_approve_for_location_approval_mcp_sampling = PermissionDecisionApproveForLocationApprovalMCPSampling.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalMcpSampling")) permission_decision_approve_for_location_approval_memory = PermissionDecisionApproveForLocationApprovalMemory.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalMemory")) permission_decision_approve_for_location_approval_read = PermissionDecisionApproveForLocationApprovalRead.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalRead")) + permission_decision_approve_for_location_approval_workflow = PermissionDecisionApproveForLocationApprovalWorkflow.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalWorkflow")) permission_decision_approve_for_location_approval_write = PermissionDecisionApproveForLocationApprovalWrite.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalWrite")) permission_decision_approve_for_session = PermissionDecisionApproveForSession.from_dict(obj.get("PermissionDecisionApproveForSession")) permission_decision_approve_for_session_approval = _load_PermissionDecisionApproveForSessionApproval(obj.get("PermissionDecisionApproveForSessionApproval")) @@ -44287,14 +51409,15 @@ def from_dict(obj: Any) -> 'RPC': permission_decision_approve_for_session_approval_extension_env_access = PermissionDecisionApproveForSessionApprovalExtensionEnvAccess.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalExtensionEnvAccess")) permission_decision_approve_for_session_approval_extension_management = PermissionDecisionApproveForSessionApprovalExtensionManagement.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalExtensionManagement")) permission_decision_approve_for_session_approval_extension_permission_access = PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess")) - permission_decision_approve_for_session_approval_factory = PermissionDecisionApproveForSessionApprovalFactory.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalFactory")) permission_decision_approve_for_session_approval_mcp = PermissionDecisionApproveForSessionApprovalMCP.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalMcp")) permission_decision_approve_for_session_approval_mcp_sampling = PermissionDecisionApproveForSessionApprovalMCPSampling.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalMcpSampling")) permission_decision_approve_for_session_approval_memory = PermissionDecisionApproveForSessionApprovalMemory.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalMemory")) permission_decision_approve_for_session_approval_read = PermissionDecisionApproveForSessionApprovalRead.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalRead")) + permission_decision_approve_for_session_approval_workflow = PermissionDecisionApproveForSessionApprovalWorkflow.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalWorkflow")) permission_decision_approve_for_session_approval_write = PermissionDecisionApproveForSessionApprovalWrite.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalWrite")) permission_decision_approve_once = PermissionDecisionApproveOnce.from_dict(obj.get("PermissionDecisionApproveOnce")) permission_decision_approve_permanently = PermissionDecisionApprovePermanently.from_dict(obj.get("PermissionDecisionApprovePermanently")) + permission_decision_approve_read_only_for_session = PermissionDecisionApproveReadOnlyForSession.from_dict(obj.get("PermissionDecisionApproveReadOnlyForSession")) permission_decision_cancelled = PermissionDecisionCancelled.from_dict(obj.get("PermissionDecisionCancelled")) permission_decision_context = PermissionDecisionContext.from_dict(obj.get("PermissionDecisionContext")) permission_decision_denied_by_content_exclusion_policy = PermissionDecisionDeniedByContentExclusionPolicy.from_dict(obj.get("PermissionDecisionDeniedByContentExclusionPolicy")) @@ -44341,11 +51464,11 @@ def from_dict(obj: Any) -> 'RPC': permissions_locations_add_tool_approval_details_extension_env_access = PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess")) permissions_locations_add_tool_approval_details_extension_management = PermissionsLocationsAddToolApprovalDetailsExtensionManagement.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsExtensionManagement")) permissions_locations_add_tool_approval_details_extension_permission_access = PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess")) - permissions_locations_add_tool_approval_details_factory = PermissionsLocationsAddToolApprovalDetailsFactory.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsFactory")) permissions_locations_add_tool_approval_details_mcp = PermissionsLocationsAddToolApprovalDetailsMCP.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsMcp")) permissions_locations_add_tool_approval_details_mcp_sampling = PermissionsLocationsAddToolApprovalDetailsMCPSampling.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsMcpSampling")) permissions_locations_add_tool_approval_details_memory = PermissionsLocationsAddToolApprovalDetailsMemory.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsMemory")) permissions_locations_add_tool_approval_details_read = PermissionsLocationsAddToolApprovalDetailsRead.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsRead")) + permissions_locations_add_tool_approval_details_workflow = PermissionsLocationsAddToolApprovalDetailsWorkflow.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsWorkflow")) permissions_locations_add_tool_approval_details_write = PermissionsLocationsAddToolApprovalDetailsWrite.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsWrite")) permissions_locations_add_tool_approval_result = PermissionsLocationsAddToolApprovalResult.from_dict(obj.get("PermissionsLocationsAddToolApprovalResult")) permissions_modify_rules_params = PermissionsModifyRulesParams.from_dict(obj.get("PermissionsModifyRulesParams")) @@ -44412,9 +51535,11 @@ 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")) + provider_descriptor = ProviderDescriptor.from_dict(obj.get("ProviderDescriptor")) provider_endpoint = ProviderEndpoint.from_dict(obj.get("ProviderEndpoint")) provider_endpoint_transport = ProviderTransport(obj.get("ProviderEndpointTransport")) provider_endpoint_type = ProviderType(obj.get("ProviderEndpointType")) @@ -44426,6 +51551,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")) @@ -44507,10 +51634,9 @@ def from_dict(obj: Any) -> 'RPC': remote_session_mode = RemoteSessionMode(obj.get("RemoteSessionMode")) remote_session_repository = RemoteSessionRepository.from_dict(obj.get("RemoteSessionRepository")) response_format = ResponseFormat.from_dict(obj.get("ResponseFormat")) - run_options = RunOptions.from_dict(obj.get("RunOptions")) sandbox_config = SandboxConfig.from_dict(obj.get("SandboxConfig")) sandbox_config_auth = SandboxConfigAuth.from_dict(obj.get("SandboxConfigAuth")) - sandbox_config_source = _SandboxConfigSource(obj.get("SandboxConfigSource")) + sandbox_config_source = SandboxConfigSource(obj.get("SandboxConfigSource")) sandbox_config_user_policy = SandboxConfigUserPolicy.from_dict(obj.get("SandboxConfigUserPolicy")) sandbox_config_user_policy_experimental = SandboxConfigUserPolicyExperimental.from_dict(obj.get("SandboxConfigUserPolicyExperimental")) sandbox_config_user_policy_experimental_seatbelt = SandboxConfigUserPolicyExperimentalSeatbelt.from_dict(obj.get("SandboxConfigUserPolicyExperimentalSeatbelt")) @@ -44518,12 +51644,20 @@ def from_dict(obj: Any) -> 'RPC': sandbox_config_user_policy_network = SandboxConfigUserPolicyNetwork.from_dict(obj.get("SandboxConfigUserPolicyNetwork")) sandbox_config_user_policy_network_proxy = SandboxConfigUserPolicyNetworkProxy.from_dict(obj.get("SandboxConfigUserPolicyNetworkProxy")) sandbox_config_user_policy_seatbelt = SandboxConfigUserPolicySeatbelt.from_dict(obj.get("SandboxConfigUserPolicySeatbelt")) + sandbox_credentials_config = SandboxCredentialsConfig.from_dict(obj.get("SandboxCredentialsConfig")) sandbox_disable_for_session_request = SandboxDisableForSessionRequest.from_dict(obj.get("SandboxDisableForSessionRequest")) sandbox_disable_for_session_result = SandboxDisableForSessionResult.from_dict(obj.get("SandboxDisableForSessionResult")) sandbox_enforcement_status = SandboxEnforcementStatus.from_dict(obj.get("SandboxEnforcementStatus")) + sandbox_grant_path_for_request_request = SandboxGrantPathForRequestRequest.from_dict(obj.get("SandboxGrantPathForRequestRequest")) + sandbox_grant_path_for_request_result = SandboxGrantPathForRequestResult.from_dict(obj.get("SandboxGrantPathForRequestResult")) sandbox_host_capability = SandboxHostCapability.from_dict(obj.get("SandboxHostCapability")) 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")) @@ -44562,10 +51696,10 @@ def from_dict(obj: Any) -> 'RPC': session_capability = SessionCapability(obj.get("SessionCapability")) session_commands_list_request = SessionCommandsListRequest.from_dict(obj.get("SessionCommandsListRequest")) session_completion_item = SessionCompletionItem.from_dict(obj.get("SessionCompletionItem")) + session_connected_ide_info = SessionConnectedIDEInfo.from_dict(obj.get("SessionConnectedIdeInfo")) session_context = SessionContext.from_dict(obj.get("SessionContext")) session_context_host_type = HostType(obj.get("SessionContextHostType")) session_enrich_metadata_result = SessionEnrichMetadataResult.from_dict(obj.get("SessionEnrichMetadataResult")) - session_factory_pause_at_checkpoint_result = SessionFactoryPauseAtCheckpointResult.from_dict(obj.get("SessionFactoryPauseAtCheckpointResult")) session_fs_append_file_request = SessionFSAppendFileRequest.from_dict(obj.get("SessionFsAppendFileRequest")) session_fs_error = SessionFSError.from_dict(obj.get("SessionFsError")) session_fs_error_code = SessionFSErrorCode(obj.get("SessionFsErrorCode")) @@ -44578,6 +51712,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")) @@ -44598,6 +51734,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")) @@ -44626,6 +51763,11 @@ def from_dict(obj: Any) -> 'RPC': session_managed_permissions = SessionManagedPermissions.from_dict(obj.get("SessionManagedPermissions")) session_managed_settings = SessionManagedSettings.from_dict(obj.get("SessionManagedSettings")) session_mcp_apps_call_tool_result = from_dict(lambda x: x, obj.get("SessionMcpAppsCallToolResult")) + session_mcp_oauth_cancel_login_request = SessionMCPOauthCancelLoginRequest.from_dict(obj.get("SessionMcpOauthCancelLoginRequest")) + session_mcp_oauth_cancel_login_result = SessionMCPOauthCancelLoginResult.from_dict(obj.get("SessionMcpOauthCancelLoginResult")) + session_mcp_oauth_prepare_login_request = SessionMCPOauthPrepareLoginRequest.from_dict(obj.get("SessionMcpOauthPrepareLoginRequest")) + session_mcp_oauth_prepare_login_result = SessionMCPOauthPrepareLoginResult.from_dict(obj.get("SessionMcpOauthPrepareLoginResult")) + session_mcp_set_connected_ide_info_params = SessionMCPSetConnectedIDEInfoParams.from_dict(obj.get("SessionMcpSetConnectedIdeInfoParams")) session_metadata_snapshot = SessionMetadataSnapshot.from_dict(obj.get("SessionMetadataSnapshot")) session_mode = SessionMode(obj.get("SessionMode")) session_model_list = SessionModelList.from_dict(obj.get("SessionModelList")) @@ -44653,6 +51795,8 @@ def from_dict(obj: Any) -> 'RPC': sessions_client_metadata_entry = SessionsClientMetadataEntry.from_dict(obj.get("SessionsClientMetadataEntry")) sessions_close_request = SessionsCloseRequest.from_dict(obj.get("SessionsCloseRequest")) sessions_close_result = SessionsCloseResult.from_dict(obj.get("SessionsCloseResult")) + sessions_create_workspace_request = SessionsCreateWorkspaceRequest.from_dict(obj.get("SessionsCreateWorkspaceRequest")) + sessions_create_workspace_result = SessionsCreateWorkspaceResult.from_dict(obj.get("SessionsCreateWorkspaceResult")) sessions_delete_request = SessionsDeleteRequest.from_dict(obj.get("SessionsDeleteRequest")) sessions_enrich_metadata_request = SessionsEnrichMetadataRequest.from_dict(obj.get("SessionsEnrichMetadataRequest")) session_set_credentials_params = SessionSetCredentialsParams.from_dict(obj.get("SessionSetCredentialsParams")) @@ -44690,6 +51834,8 @@ def from_dict(obj: Any) -> 'RPC': sessions_list_non_empty_session_ids_result = SessionsListNonEmptySessionIDSResult.from_dict(obj.get("SessionsListNonEmptySessionIdsResult")) sessions_list_request = SessionsListRequest.from_dict(obj.get("SessionsListRequest")) sessions_load_deferred_repo_hooks_request = SessionsLoadDeferredRepoHooksRequest.from_dict(obj.get("SessionsLoadDeferredRepoHooksRequest")) + sessions_load_workspace_request = SessionsLoadWorkspaceRequest.from_dict(obj.get("SessionsLoadWorkspaceRequest")) + sessions_load_workspace_result = SessionsLoadWorkspaceResult.from_dict(obj.get("SessionsLoadWorkspaceResult")) sessions_open_attach = SessionsOpenAttach.from_dict(obj.get("SessionsOpenAttach")) sessions_open_cloud = SessionsOpenCloud.from_dict(obj.get("SessionsOpenCloud")) sessions_open_create = SessionsOpenCreate.from_dict(obj.get("SessionsOpenCreate")) @@ -44717,6 +51863,8 @@ def from_dict(obj: Any) -> 'RPC': sessions_start_remote_control_request = SessionsStartRemoteControlRequest.from_dict(obj.get("SessionsStartRemoteControlRequest")) sessions_stop_remote_control_request = SessionsStopRemoteControlRequest.from_dict(obj.get("SessionsStopRemoteControlRequest")) sessions_transfer_remote_control_request = SessionsTransferRemoteControlRequest.from_dict(obj.get("SessionsTransferRemoteControlRequest")) + sessions_update_workspace_fields_request = SessionsUpdateWorkspaceFieldsRequest.from_dict(obj.get("SessionsUpdateWorkspaceFieldsRequest")) + sessions_update_workspace_fields_result = SessionsUpdateWorkspaceFieldsResult.from_dict(obj.get("SessionsUpdateWorkspaceFieldsResult")) session_telemetry_engagement = SessionTelemetryEngagement.from_dict(obj.get("SessionTelemetryEngagement")) session_update_options_params = SessionUpdateOptionsParams.from_dict(obj.get("SessionUpdateOptionsParams")) session_update_options_result = SessionUpdateOptionsResult.from_dict(obj.get("SessionUpdateOptionsResult")) @@ -44724,6 +51872,7 @@ def from_dict(obj: Any) -> 'RPC': session_workflow_pause_at_checkpoint_result = SessionWorkflowPauseAtCheckpointResult.from_dict(obj.get("SessionWorkflowPauseAtCheckpointResult")) session_working_directory_context = SessionWorkingDirectoryContext.from_dict(obj.get("SessionWorkingDirectoryContext")) session_working_directory_context_host_type = HostType(obj.get("SessionWorkingDirectoryContextHostType")) + session_working_directory_context_with_client = SessionWorkingDirectoryContextWithClient.from_dict(obj.get("SessionWorkingDirectoryContextWithClient")) settable_auth_info = _load_SettableAuthInfo(obj.get("SettableAuthInfo")) settable_token_auth_info = SettableTokenAuthInfo.from_dict(obj.get("SettableTokenAuthInfo")) shell_cancel_user_requested_request = ShellCancelUserRequestedRequest.from_dict(obj.get("ShellCancelUserRequestedRequest")) @@ -44740,10 +51889,31 @@ def from_dict(obj: Any) -> 'RPC': shell_options = ShellOptions.from_dict(obj.get("ShellOptions")) shutdown_request = ShutdownRequest.from_dict(obj.get("ShutdownRequest")) skill = Skill.from_dict(obj.get("Skill")) + skill_apply_install_request = SkillApplyInstallRequest.from_dict(obj.get("SkillApplyInstallRequest")) + skill_apply_uninstall_request = SkillApplyUninstallRequest.from_dict(obj.get("SkillApplyUninstallRequest")) skill_discovery_path = SkillDiscoveryPath.from_dict(obj.get("SkillDiscoveryPath")) skill_discovery_path_list = SkillDiscoveryPathList.from_dict(obj.get("SkillDiscoveryPathList")) skill_discovery_scope = SkillDiscoveryScope(obj.get("SkillDiscoveryScope")) + skill_installation_failure_reason = SkillInstallationFailureReason(obj.get("SkillInstallationFailureReason")) + skill_installation_file_review = SkillInstallationFileReview.from_dict(obj.get("SkillInstallationFileReview")) + skill_installation_location = SkillInstallationLocation.from_dict(obj.get("SkillInstallationLocation")) + skill_installation_management_outcome = SkillInstallationManagementOutcome.from_dict(obj.get("SkillInstallationManagementOutcome")) + skill_installation_management_result = SkillInstallationManagementResult.from_dict(obj.get("SkillInstallationManagementResult")) + skill_installation_operation_request = SkillInstallationOperationRequest.from_dict(obj.get("SkillInstallationOperationRequest")) + skill_installation_operation_status = SkillInstallationOperationStatus.from_dict(obj.get("SkillInstallationOperationStatus")) + skill_installation_outcome = SkillInstallationOutcome.from_dict(obj.get("SkillInstallationOutcome")) + skill_installation_ownership_state = SkillInstallationOwnershipState(obj.get("SkillInstallationOwnershipState")) + skill_installation_result = SkillInstallationResult.from_dict(obj.get("SkillInstallationResult")) + skill_installation_review = SkillInstallationReview.from_dict(obj.get("SkillInstallationReview")) + skill_installation_scope = SkillInstallationScope(obj.get("SkillInstallationScope")) + skill_installation_session_state = SkillInstallationSessionState(obj.get("SkillInstallationSessionState")) + skill_installation_source = SkillInstallationSource.from_dict(obj.get("SkillInstallationSource")) + skill_installations_request = SkillInstallationsRequest.from_dict(obj.get("SkillInstallationsRequest")) + skill_installation_summary = SkillInstallationSummary.from_dict(obj.get("SkillInstallationSummary")) + skill_install_plan = SkillInstallPlan.from_dict(obj.get("SkillInstallPlan")) skill_list = SkillList.from_dict(obj.get("SkillList")) + skill_plan_install_request = SkillPlanInstallRequest.from_dict(obj.get("SkillPlanInstallRequest")) + skill_plan_uninstall_request = SkillPlanUninstallRequest.from_dict(obj.get("SkillPlanUninstallRequest")) skill_provider_descriptor = SkillProviderDescriptor.from_dict(obj.get("SkillProviderDescriptor")) skill_provider_list_request = SkillProviderListRequest.from_dict(obj.get("SkillProviderListRequest")) skill_provider_list_result = _SkillProviderListResult.from_dict(obj.get("SkillProviderListResult")) @@ -44754,10 +51924,12 @@ def from_dict(obj: Any) -> 'RPC': skills_disable_request = SkillsDisableRequest.from_dict(obj.get("SkillsDisableRequest")) skills_discover_request = SkillsDiscoverRequest.from_dict(obj.get("SkillsDiscoverRequest")) skills_enable_request = SkillsEnableRequest.from_dict(obj.get("SkillsEnableRequest")) + skill_set_enabled_request = SkillSetEnabledRequest.from_dict(obj.get("SkillSetEnabledRequest")) skills_get_discovery_paths_request = SkillsGetDiscoveryPathsRequest.from_dict(obj.get("SkillsGetDiscoveryPathsRequest")) skills_get_invoked_result = SkillsGetInvokedResult.from_dict(obj.get("SkillsGetInvokedResult")) skills_invoked_skill = SkillsInvokedSkill.from_dict(obj.get("SkillsInvokedSkill")) skills_load_diagnostics = SkillsLoadDiagnostics.from_dict(obj.get("SkillsLoadDiagnostics")) + skill_uninstall_plan = SkillUninstallPlan.from_dict(obj.get("SkillUninstallPlan")) slash_command_add_timeline_entry_result = SlashCommandAddTimelineEntryResult.from_dict(obj.get("SlashCommandAddTimelineEntryResult")) slash_command_agent_prompt_result = SlashCommandAgentPromptResult.from_dict(obj.get("SlashCommandAgentPromptResult")) slash_command_completed_result = SlashCommandCompletedResult.from_dict(obj.get("SlashCommandCompletedResult")) @@ -44896,7 +52068,6 @@ def from_dict(obj: Any) -> 'RPC': user_setting_metadata = UserSettingMetadata.from_dict(obj.get("UserSettingMetadata")) user_settings_get_result = UserSettingsGetResult.from_dict(obj.get("UserSettingsGetResult")) user_settings_set_request = UserSettingsSetRequest.from_dict(obj.get("UserSettingsSetRequest")) - user_settings_set_result = UserSettingsSetResult.from_dict(obj.get("UserSettingsSetResult")) visibility_get_result = VisibilityGetResult.from_dict(obj.get("VisibilityGetResult")) visibility_set_request = VisibilitySetRequest.from_dict(obj.get("VisibilitySetRequest")) visibility_set_result = VisibilitySetResult.from_dict(obj.get("VisibilitySetResult")) @@ -44909,7 +52080,7 @@ def from_dict(obj: Any) -> 'RPC': workflow_cancel_request = WorkflowCancelRequest.from_dict(obj.get("WorkflowCancelRequest")) workflow_current_phase = WorkflowCurrentPhase.from_dict(obj.get("WorkflowCurrentPhase")) workflow_declared_limits = WorkflowDeclaredLimits.from_dict(obj.get("WorkflowDeclaredLimits")) - workflow_durable_operation = DurableOperation(obj.get("WorkflowDurableOperation")) + workflow_durable_operation = WorkflowDurableOperation(obj.get("WorkflowDurableOperation")) workflow_execute_request = WorkflowExecuteRequest.from_dict(obj.get("WorkflowExecuteRequest")) workflow_execute_result = WorkflowExecuteResult.from_dict(obj.get("WorkflowExecuteResult")) workflow_get_run_progress_request = WorkflowGetRunProgressRequest.from_dict(obj.get("WorkflowGetRunProgressRequest")) @@ -44920,15 +52091,15 @@ def from_dict(obj: Any) -> 'RPC': workflow_list_runs_request = WorkflowListRunsRequest.from_dict(obj.get("WorkflowListRunsRequest")) workflow_list_runs_result = WorkflowListRunsResult.from_dict(obj.get("WorkflowListRunsResult")) workflow_log_line = WorkflowLogLine.from_dict(obj.get("WorkflowLogLine")) - workflow_log_line_kind = LogLineKind(obj.get("WorkflowLogLineKind")) + workflow_log_line_kind = WorkflowLogLineKind(obj.get("WorkflowLogLineKind")) workflow_log_request = WorkflowLogRequest.from_dict(obj.get("WorkflowLogRequest")) - workflow_pause_checkpoint_action = PauseCheckpointAction(obj.get("WorkflowPauseCheckpointAction")) + workflow_pause_checkpoint_action = WorkflowPauseCheckpointAction(obj.get("WorkflowPauseCheckpointAction")) workflow_pause_checkpoint_request = WorkflowPauseCheckpointRequest.from_dict(obj.get("WorkflowPauseCheckpointRequest")) workflow_pause_checkpoint_result = WorkflowPauseCheckpointResult.from_dict(obj.get("WorkflowPauseCheckpointResult")) workflow_pause_info = WorkflowPauseInfo.from_dict(obj.get("WorkflowPauseInfo")) workflow_pause_request = WorkflowPauseRequest.from_dict(obj.get("WorkflowPauseRequest")) workflow_phase_observation = WorkflowPhaseObservation.from_dict(obj.get("WorkflowPhaseObservation")) - workflow_phase_status = PhaseStatus(obj.get("WorkflowPhaseStatus")) + workflow_phase_status = WorkflowPhaseStatus(obj.get("WorkflowPhaseStatus")) workflow_progress_line = WorkflowProgressLine.from_dict(obj.get("WorkflowProgressLine")) workflow_progress_page = WorkflowProgressPage.from_dict(obj.get("WorkflowProgressPage")) workflow_resume_request = WorkflowResumeRequest.from_dict(obj.get("WorkflowResumeRequest")) @@ -44936,12 +52107,12 @@ def from_dict(obj: Any) -> 'RPC': workflow_run_consumed = WorkflowRunConsumed.from_dict(obj.get("WorkflowRunConsumed")) workflow_run_detail = WorkflowRunDetail.from_dict(obj.get("WorkflowRunDetail")) workflow_run_failure = WorkflowRunFailure.from_dict(obj.get("WorkflowRunFailure")) - workflow_run_failure_kind = RunFailureKind(obj.get("WorkflowRunFailureKind")) + workflow_run_failure_kind = WorkflowRunFailureKind(obj.get("WorkflowRunFailureKind")) workflow_run_limits = WorkflowRunLimits.from_dict(obj.get("WorkflowRunLimits")) workflow_run_options = WorkflowRunOptions.from_dict(obj.get("WorkflowRunOptions")) workflow_run_request = WorkflowRunRequest.from_dict(obj.get("WorkflowRunRequest")) workflow_run_result = WorkflowRunResult.from_dict(obj.get("WorkflowRunResult")) - workflow_run_status = RunStatus(obj.get("WorkflowRunStatus")) + workflow_run_status = WorkflowRunStatus(obj.get("WorkflowRunStatus")) workflow_run_summary = WorkflowRunSummary.from_dict(obj.get("WorkflowRunSummary")) workflow_run_terminal = WorkflowRunTerminal.from_dict(obj.get("WorkflowRunTerminal")) workflow_tool_resume_request = _WorkflowToolResumeRequest.from_dict(obj.get("WorkflowToolResumeRequest")) @@ -44980,13 +52151,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_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, 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_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, 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, 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, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_pause_checkpoint_action, factory_pause_checkpoint_request, factory_pause_checkpoint_result, factory_pause_info, factory_pause_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, 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, 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, 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_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_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_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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, 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_value_category, 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_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_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_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_factory, 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_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_factory, 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_write, permission_decision_approve_once, permission_decision_approve_permanently, 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_factory, 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_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_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, run_options, 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_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_host_capability, sandbox_host_capability_name, sandbox_host_support, 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_factory_pause_at_checkpoint_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_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_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, 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, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, 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_custom_agent_initial_model_decision_params, agents_custom_agent_initial_model_decision_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_available_builtins_request, agents_get_available_builtins_result, agents_get_builtin_definition_request, agents_get_builtin_definition_result, agents_get_builtin_listing_definition_request, agents_get_builtin_listing_definition_result, agents_get_builtins_result, 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, builtin_agent_summary, 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_discovery_account, connector_discovery_account_list, connector_discovery_account_request, connector_discovery_auth_info, connector_discovery_availability, connector_discovery_capabilities, connector_discovery_catalog_entry, connector_discovery_catalog_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_current_branch_remote_result, git_cwd_request, git_hub_environment, git_hub_owner_option, git_hub_owners_cancel_request, git_hub_owners_cancel_result, git_hub_owners_list_request, git_hub_owners_list_result, git_hub_owners_request_id_result, git_hub_repository_at_path_request, git_hub_repository_at_path_result, git_hub_repository_identity, 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, git_remote_repository, git_repos_from_remotes_request, git_repos_from_remotes_result, global_state_load_for_config_dir_request, global_state_load_result, global_state_write_key_request, 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, logged_in_user, 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_connected_ide_info, 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_mcp_set_connected_ide_info_params, 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_create_workspace_request, sessions_create_workspace_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_load_workspace_request, sessions_load_workspace_result, 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, sessions_update_workspace_fields_request, sessions_update_workspace_fields_result, 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, session_working_directory_context_with_client, 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, 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 = {} @@ -44994,15 +52166,21 @@ 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) result["AccountGetQuotaResult"] = to_class(AccountGetQuotaResult, self.account_get_quota_result) + result["AccountKind"] = to_enum(AccountKind, self.account_kind) result["AccountLoginRequest"] = to_class(AccountLoginRequest, self.account_login_request) result["AccountLoginResult"] = to_class(AccountLoginResult, self.account_login_result) result["AccountLogoutRequest"] = to_class(AccountLogoutRequest, self.account_logout_request) result["AccountLogoutResult"] = to_class(AccountLogoutResult, self.account_logout_result) result["AccountQuotaSnapshot"] = to_class(AccountQuotaSnapshot, self.account_quota_snapshot) + result["AccountsEnumerateRequest"] = to_class(AccountsEnumerateRequest, self.accounts_enumerate_request) + result["AccountsGetRequest"] = to_class(AccountsGetRequest, self.accounts_get_request) + result["AccountsSetRequest"] = to_class(AccountsSetRequest, self.accounts_set_request) + result["AccountStatus"] = to_class(AccountStatus, self.account_status) result["AdaptiveThinkingSupport"] = to_enum(AdaptiveThinkingSupport, self.adaptive_thinking_support) result["AgentDiscoveryPath"] = to_class(AgentDiscoveryPath, self.agent_discovery_path) result["AgentDiscoveryPathList"] = to_class(AgentDiscoveryPathList, self.agent_discovery_path_list) @@ -45029,21 +52207,47 @@ def to_dict(self) -> dict: result["AgentRegistrySpawnValidationErrorField"] = to_enum(AgentRegistrySpawnValidationErrorField, self.agent_registry_spawn_validation_error_field) result["AgentRegistrySpawnValidationErrorReason"] = to_enum(AgentRegistrySpawnValidationErrorReason, self.agent_registry_spawn_validation_error_reason) result["AgentReloadResult"] = to_class(AgentReloadResult, self.agent_reload_result) + result["AgentsCustomAgentInitialModelDecisionParams"] = to_class(_AgentsCustomAgentInitialModelDecisionParams, self.agents_custom_agent_initial_model_decision_params) + result["AgentsCustomAgentInitialModelDecisionResult"] = to_class(_AgentsCustomAgentInitialModelDecisionResult, self.agents_custom_agent_initial_model_decision_result) result["AgentsDiscoverRequest"] = to_class(AgentsDiscoverRequest, self.agents_discover_request) result["AgentSelectRequest"] = to_class(AgentSelectRequest, self.agent_select_request) result["AgentSelectResult"] = to_class(AgentSelectResult, self.agent_select_result) result["AgentSetPromptRequest"] = to_class(AgentSetPromptRequest, self.agent_set_prompt_request) + result["AgentsGetAvailableBuiltinsRequest"] = to_class(_AgentsGetAvailableBuiltinsRequest, self.agents_get_available_builtins_request) + result["AgentsGetAvailableBuiltinsResult"] = to_class(_AgentsGetAvailableBuiltinsResult, self.agents_get_available_builtins_result) + result["AgentsGetBuiltinDefinitionRequest"] = to_class(_AgentsGetBuiltinDefinitionRequest, self.agents_get_builtin_definition_request) + result["AgentsGetBuiltinDefinitionResult"] = to_class(_AgentsGetBuiltinDefinitionResult, self.agents_get_builtin_definition_result) + result["AgentsGetBuiltinListingDefinitionRequest"] = to_class(_AgentsGetBuiltinListingDefinitionRequest, self.agents_get_builtin_listing_definition_request) + result["AgentsGetBuiltinListingDefinitionResult"] = to_class(_AgentsGetBuiltinListingDefinitionResult, self.agents_get_builtin_listing_definition_result) + result["AgentsGetBuiltinsResult"] = to_class(_AgentsGetBuiltinsResult, self.agents_get_builtins_result) result["AgentsGetDiscoveryPathsRequest"] = to_class(AgentsGetDiscoveryPathsRequest, self.agents_get_discovery_paths_request) result["ApiKeyAuthInfo"] = to_class(APIKeyAuthInfo, self.api_key_auth_info) + 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) + result["AuthLoginCancelRequest"] = to_class(AuthLoginCancelRequest, self.auth_login_cancel_request) + result["AuthLoginResultDto"] = to_class(AuthLoginResultDto, self.auth_login_result_dto) + result["AuthLoginResultStatus"] = to_enum(AuthLoginResultStatus, self.auth_login_result_status) + result["AuthLoginStep"] = to_class(AuthLoginStep, self.auth_login_step) + result["AuthReadQuery"] = to_class(AuthReadQuery, self.auth_read_query) + result["AuthReadValue"] = to_class(AuthReadValue, self.auth_read_value) + result["AuthStatusDto"] = to_class(AuthStatusDto, self.auth_status_dto) result["AuthValidationError"] = to_class(AuthValidationError, self.auth_validation_error) result["AuthValidationErrors"] = from_list(lambda x: to_class(AuthValidationError, x), self.auth_validation_errors) + result["AuthWrite"] = to_class(AuthWrite, self.auth_write) + result["AuthWriteResult"] = to_class(AuthWriteResult, self.auth_write_result) result["AutopilotObjectiveCreditLimit"] = to_class(AutopilotObjectiveCreditLimit, self.autopilot_objective_credit_limit) result["AutopilotObjectiveGetStateResult"] = to_class(AutopilotObjectiveGetStateResult, self.autopilot_objective_get_state_result) result["AutopilotObjectiveState"] = to_class(AutopilotObjectiveState, self.autopilot_objective_state) result["AutopilotObjectiveStatus"] = to_enum(AutopilotObjectiveStatus, self.autopilot_objective_status) + result["BuiltinAgentSummary"] = to_class(_BuiltinAgentSummary, self.builtin_agent_summary) result["BuiltInModelCatalog"] = to_class(BuiltInModelCatalog, self.built_in_model_catalog) result["BuiltInModelCatalogEntry"] = to_class(BuiltInModelCatalogEntry, self.built_in_model_catalog_entry) result["BuiltinToolDescriptor"] = to_class(BuiltinToolDescriptor, self.builtin_tool_descriptor) @@ -45107,7 +52311,7 @@ def to_dict(self) -> dict: result["CatalogMcpServerCandidate"] = to_class(CatalogMCPServerCandidate, self.catalog_mcp_server_candidate) result["CatalogMcpServerCandidateKind"] = to_enum(CatalogMCPServerCandidateKind, self.catalog_mcp_server_candidate_kind) result["CatalogMcpServerCandidateProvenance"] = to_class(CatalogMCPServerCandidateProvenance, self.catalog_mcp_server_candidate_provenance) - result["CatalogMcpServerInstallability"] = to_enum(CatalogMCPServerInstallabilityEnum, self.catalog_mcp_server_installability) + result["CatalogMcpServerInstallability"] = to_enum(CatalogMCPServerInstallability, self.catalog_mcp_server_installability) result["CatalogMediaType"] = to_enum(CatalogMediaType, self.catalog_media_type) result["CatalogNegotiatedContract"] = to_class(CatalogNegotiatedContract, self.catalog_negotiated_contract) result["CatalogNegotiationRefusedError"] = to_class(CatalogNegotiationRefusedError, self.catalog_negotiation_refused_error) @@ -45200,9 +52404,18 @@ def to_dict(self) -> dict: result["ConnectorConnectResult"] = to_class(ConnectorConnectResult, self.connector_connect_result) result["ConnectorContinueRequest"] = to_class(ConnectorContinueRequest, self.connector_continue_request) result["ConnectorDisconnectResult"] = to_class(ConnectorDisconnectResult, self.connector_disconnect_result) + result["ConnectorDiscoveryAccount"] = to_class(ConnectorDiscoveryAccount, self.connector_discovery_account) + result["ConnectorDiscoveryAccountList"] = to_class(ConnectorDiscoveryAccountList, self.connector_discovery_account_list) + result["ConnectorDiscoveryAccountRequest"] = to_class(ConnectorDiscoveryAccountRequest, self.connector_discovery_account_request) + result["ConnectorDiscoveryAuthInfo"] = to_class(ConnectorDiscoveryAuthInfo, self.connector_discovery_auth_info) + result["ConnectorDiscoveryAvailability"] = to_enum(ConnectorDiscoveryAvailability, self.connector_discovery_availability) + result["ConnectorDiscoveryCapabilities"] = to_class(ConnectorDiscoveryCapabilities, self.connector_discovery_capabilities) + result["ConnectorDiscoveryCatalogEntry"] = to_class(ConnectorDiscoveryCatalogEntry, self.connector_discovery_catalog_entry) + result["ConnectorDiscoveryCatalogResult"] = to_class(ConnectorDiscoveryCatalogResult, self.connector_discovery_catalog_result) 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) @@ -45221,6 +52434,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) @@ -45253,9 +52470,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["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) @@ -45291,52 +52518,6 @@ def to_dict(self) -> dict: result["ExternalToolTextResultForLlmContentShellExit"] = to_class(ExternalToolTextResultForLlmContentShellExit, self.external_tool_text_result_for_llm_content_shell_exit) result["ExternalToolTextResultForLlmContentTerminal"] = to_class(ExternalToolTextResultForLlmContentTerminal, self.external_tool_text_result_for_llm_content_terminal) result["ExternalToolTextResultForLlmContentText"] = to_class(ExternalToolTextResultForLlmContentText, self.external_tool_text_result_for_llm_content_text) - result["FactoryAbortRequest"] = to_class(FactoryAbortRequest, self.factory_abort_request) - result["FactoryAckResult"] = to_class(FactoryACKResult, self.factory_ack_result) - result["FactoryAgentOptions"] = to_class(FactoryAgentOptions, self.factory_agent_options) - result["FactoryAgentRequest"] = to_class(FactoryAgentRequest, self.factory_agent_request) - result["FactoryAgentResult"] = to_class(FactoryAgentResult, self.factory_agent_result) - result["FactoryAgentSummary"] = to_class(FactoryAgentSummary, self.factory_agent_summary) - result["FactoryCancelRequest"] = to_class(FactoryCancelRequest, self.factory_cancel_request) - result["FactoryCurrentPhase"] = to_class(FactoryCurrentPhase, self.factory_current_phase) - result["FactoryDeclaredLimits"] = to_class(FactoryDeclaredLimits, self.factory_declared_limits) - result["FactoryDurableOperation"] = to_enum(DurableOperation, self.factory_durable_operation) - result["FactoryExecuteRequest"] = to_class(FactoryExecuteRequest, self.factory_execute_request) - result["FactoryExecuteResult"] = to_class(FactoryExecuteResult, self.factory_execute_result) - result["FactoryGetRunProgressRequest"] = to_class(FactoryGetRunProgressRequest, self.factory_get_run_progress_request) - result["FactoryGetRunRequest"] = to_class(FactoryGetRunRequest, self.factory_get_run_request) - result["FactoryJournalGetRequest"] = to_class(FactoryJournalGetRequest, self.factory_journal_get_request) - result["FactoryJournalGetResult"] = to_class(FactoryJournalGetResult, self.factory_journal_get_result) - result["FactoryJournalPutRequest"] = to_class(FactoryJournalPutRequest, self.factory_journal_put_request) - result["FactoryListRunsRequest"] = to_class(FactoryListRunsRequest, self.factory_list_runs_request) - result["FactoryListRunsResult"] = to_class(FactoryListRunsResult, self.factory_list_runs_result) - result["FactoryLogLine"] = to_class(FactoryLogLine, self.factory_log_line) - result["FactoryLogLineKind"] = to_enum(LogLineKind, self.factory_log_line_kind) - result["FactoryLogRequest"] = to_class(FactoryLogRequest, self.factory_log_request) - result["FactoryPauseCheckpointAction"] = to_enum(PauseCheckpointAction, self.factory_pause_checkpoint_action) - result["FactoryPauseCheckpointRequest"] = to_class(FactoryPauseCheckpointRequest, self.factory_pause_checkpoint_request) - result["FactoryPauseCheckpointResult"] = to_class(FactoryPauseCheckpointResult, self.factory_pause_checkpoint_result) - result["FactoryPauseInfo"] = to_class(FactoryPauseInfo, self.factory_pause_info) - result["FactoryPauseRequest"] = to_class(FactoryPauseRequest, self.factory_pause_request) - result["FactoryPhaseObservation"] = to_class(FactoryPhaseObservation, self.factory_phase_observation) - result["FactoryPhaseStatus"] = to_enum(PhaseStatus, self.factory_phase_status) - result["FactoryProgressLine"] = to_class(FactoryProgressLine, self.factory_progress_line) - result["FactoryProgressPage"] = to_class(FactoryProgressPage, self.factory_progress_page) - result["FactoryResumeRequest"] = to_class(FactoryResumeRequest, self.factory_resume_request) - result["FactoryResumeResult"] = to_class(FactoryResumeResult, self.factory_resume_result) - result["FactoryRunConsumed"] = to_class(FactoryRunConsumed, self.factory_run_consumed) - result["FactoryRunDetail"] = to_class(FactoryRunDetail, self.factory_run_detail) - result["FactoryRunFailure"] = to_class(FactoryRunFailure, self.factory_run_failure) - result["FactoryRunFailureKind"] = to_enum(RunFailureKind, self.factory_run_failure_kind) - result["FactoryRunLimits"] = to_class(FactoryRunLimits, self.factory_run_limits) - result["FactoryRunRequest"] = to_class(FactoryRunRequest, self.factory_run_request) - result["FactoryRunResult"] = to_class(FactoryRunResult, self.factory_run_result) - result["FactoryRunStatus"] = to_enum(RunStatus, self.factory_run_status) - result["FactoryRunSummary"] = to_class(FactoryRunSummary, self.factory_run_summary) - result["FactoryRunTerminal"] = to_class(FactoryRunTerminal, self.factory_run_terminal) - result["FactoryToolResumeRequest"] = to_class(_FactoryToolResumeRequest, self.factory_tool_resume_request) - result["FactoryToolRunOptions"] = to_class(_FactoryToolRunOptions, self.factory_tool_run_options) - result["FactoryToolRunRequest"] = to_class(_FactoryToolRunRequest, self.factory_tool_run_request) result["FilterMapping"] = from_union([lambda x: from_dict(lambda x: to_enum(ContentFilterMode, x), x), lambda x: to_enum(ContentFilterMode, x)], self.filter_mapping) result["FleetStartRequest"] = to_class(FleetStartRequest, self.fleet_start_request) result["FleetStartResult"] = to_class(FleetStartResult, self.fleet_start_result) @@ -45344,12 +52525,30 @@ 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["GitCurrentBranchRemoteResult"] = to_class(_GitCurrentBranchRemoteResult, self.git_current_branch_remote_result) + result["GitCwdRequest"] = to_class(_GitCwdRequest, self.git_cwd_request) + result["GitHubEnvironment"] = to_class(GitHubEnvironment, self.git_hub_environment) + result["GitHubOwnerOption"] = to_class(_GitHubOwnerOption, self.git_hub_owner_option) + result["GitHubOwnersCancelRequest"] = to_class(_GitHubOwnersCancelRequest, self.git_hub_owners_cancel_request) + result["GitHubOwnersCancelResult"] = to_class(_GitHubOwnersCancelResult, self.git_hub_owners_cancel_result) + result["GitHubOwnersListRequest"] = to_class(_GitHubOwnersListRequest, self.git_hub_owners_list_request) + result["GitHubOwnersListResult"] = to_class(_GitHubOwnersListResult, self.git_hub_owners_list_result) + result["GitHubOwnersRequestIdResult"] = to_class(GitHubOwnersRequestIDResult, self.git_hub_owners_request_id_result) + result["GitHubRepositoryAtPathRequest"] = to_class(_GitHubRepositoryAtPathRequest, self.git_hub_repository_at_path_request) + result["GitHubRepositoryAtPathResult"] = to_class(_GitHubRepositoryAtPathResult, self.git_hub_repository_at_path_result) + result["GitHubRepositoryIdentity"] = to_class(GitHubRepositoryIdentity, self.git_hub_repository_identity) 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) result["GitHubTokenAcquireReason"] = to_enum(GitHubTokenAcquireReason, self.git_hub_token_acquire_reason) result["GitHubTokenAcquireRequest"] = to_class(GitHubTokenAcquireRequest, self.git_hub_token_acquire_request) result["GitHubTokenAcquireResult"] = to_class(GitHubTokenAcquireResult, self.git_hub_token_acquire_result) + result["GitRemoteRepository"] = to_class(_GitRemoteRepository, self.git_remote_repository) + result["GitReposFromRemotesRequest"] = to_class(_GitReposFromRemotesRequest, self.git_repos_from_remotes_request) + result["GitReposFromRemotesResult"] = to_class(_GitReposFromRemotesResult, self.git_repos_from_remotes_result) + result["GlobalStateLoadForConfigDirRequest"] = to_class(_GlobalStateLoadForConfigDirRequest, self.global_state_load_for_config_dir_request) + result["GlobalStateLoadResult"] = to_class(_GlobalStateLoadResult, self.global_state_load_result) + result["GlobalStateWriteKeyRequest"] = to_class(_GlobalStateWriteKeyRequest, self.global_state_write_key_request) result["HandlePendingToolCallRequest"] = to_class(HandlePendingToolCallRequest, self.handle_pending_tool_call_request) result["HandlePendingToolCallResult"] = to_class(HandlePendingToolCallResult, self.handle_pending_tool_call_result) result["HistoryAbortManualCompactionResult"] = to_class(HistoryAbortManualCompactionResult, self.history_abort_manual_compaction_result) @@ -45382,6 +52581,31 @@ def to_dict(self) -> dict: result["HooksDiscoverRequest"] = to_class(HooksDiscoverRequest, self.hooks_discover_request) result["HooksDiscoverResult"] = to_class(HooksDiscoverResult, self.hooks_discover_result) result["HookType"] = to_enum(HookType, self.hook_type) + 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) + result["HostRegisterSessionRequest"] = to_class(HostRegisterSessionRequest, self.host_register_session_request) + result["HostSessionCreateCallback"] = to_class(HostSessionCreateCallback, self.host_session_create_callback) + result["HostSessionCreateRequest"] = to_class(HostSessionCreateRequest, self.host_session_create_request) + result["HostSessionCreateResult"] = to_class(HostSessionCreateResult, self.host_session_create_result) + result["HostSessionReleasedNotification"] = to_class(HostSessionReleasedNotification, self.host_session_released_notification) + result["HostSessionReleaseRequest"] = to_class(HostSessionReleaseRequest, self.host_session_release_request) + result["HostStartRequest"] = to_class(HostStartRequest, self.host_start_request) + result["HostStartResult"] = to_class(HostStartResult, self.host_start_result) + result["InstallationCatalogueIdentity"] = to_class(InstallationCatalogueIdentity, self.installation_catalogue_identity) + result["InstallationConfirmationRequest"] = to_class(InstallationConfirmationRequest, self.installation_confirmation_request) + result["InstallationConfirmationResponse"] = to_class(InstallationConfirmationResponse, self.installation_confirmation_response) + result["InstallationDecision"] = to_enum(InstallationDecision, self.installation_decision) + result["InstallationReview"] = to_class(InstallationReview, self.installation_review) result["InstalledPlugin"] = to_class(InstalledPlugin, self.installed_plugin) result["InstalledPluginInfo"] = to_class(InstalledPluginInfo, self.installed_plugin_info) result["InstalledPluginSource"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str], self.installed_plugin_source) @@ -45414,12 +52638,29 @@ def to_dict(self) -> dict: result["LlmInferenceHttpResponseStartResult"] = to_class(LlmInferenceHTTPResponseStartResult, self.llm_inference_http_response_start_result) result["LlmInferenceSetProviderResult"] = to_class(LlmInferenceSetProviderResult, self.llm_inference_set_provider_result) result["LocalSessionMetadataValue"] = to_class(LocalSessionMetadataValue, self.local_session_metadata_value) + result["LoggedInUser"] = to_class(_LoggedInUser, self.logged_in_user) + result["LoginProviderKind"] = to_enum(LoginProviderKind, self.login_provider_kind) result["LogRequest"] = to_class(LogRequest, self.log_request) 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) @@ -45429,6 +52670,8 @@ def to_dict(self) -> dict: result["MarketplaceRefreshResult"] = to_class(MarketplaceRefreshResult, self.marketplace_refresh_result) result["MarketplaceRemoveResult"] = to_class(MarketplaceRemoveResult, self.marketplace_remove_result) result["McpAllowedServer"] = to_class(MCPAllowedServer, self.mcp_allowed_server) + result["McpApplyInstallRequest"] = to_class(MCPApplyInstallRequest, self.mcp_apply_install_request) + result["McpApplyUninstallRequest"] = to_class(MCPApplyUninstallRequest, self.mcp_apply_uninstall_request) result["McpAppsCallToolRequest"] = to_class(MCPAppsCallToolRequest, self.mcp_apps_call_tool_request) result["McpAppsDiagnoseCapability"] = to_class(MCPAppsDiagnoseCapability, self.mcp_apps_diagnose_capability) result["McpAppsDiagnoseRequest"] = to_class(MCPAppsDiagnoseRequest, self.mcp_apps_diagnose_request) @@ -45459,6 +52702,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) @@ -45479,23 +52725,44 @@ def to_dict(self) -> dict: result["McpHeadersHandlePendingHeadersRefreshRequestRequest"] = to_class(MCPHeadersHandlePendingHeadersRefreshRequestRequest, self.mcp_headers_handle_pending_headers_refresh_request_request) result["McpHeadersHandlePendingHeadersRefreshRequestResult"] = to_class(MCPHeadersHandlePendingHeadersRefreshRequestResult, self.mcp_headers_handle_pending_headers_refresh_request_result) result["McpHostState"] = to_class(MCPHostState, self.mcp_host_state) + result["McpInstallationFailureReason"] = to_enum(MCPInstallationFailureReason, self.mcp_installation_failure_reason) + result["McpInstallationInput"] = to_class(MCPInstallationInput, self.mcp_installation_input) + result["McpInstallationManagementOutcome"] = to_class(MCPInstallationManagementOutcome, self.mcp_installation_management_outcome) + result["McpInstallationManagementResult"] = to_class(MCPInstallationManagementResult, self.mcp_installation_management_result) + result["McpInstallationOperationRequest"] = to_class(MCPInstallationOperationRequest, self.mcp_installation_operation_request) + result["McpInstallationOperationStatus"] = to_class(MCPInstallationOperationStatus, self.mcp_installation_operation_status) + result["McpInstallationOutcome"] = to_class(MCPInstallationOutcome, self.mcp_installation_outcome) + result["McpInstallationRemoteConfiguration"] = to_class(MCPInstallationRemoteConfiguration, self.mcp_installation_remote_configuration) + result["McpInstallationResult"] = to_class(MCPInstallationResult, self.mcp_installation_result) + result["McpInstallationReview"] = to_class(MCPInstallationReview, self.mcp_installation_review) + result["McpInstallationSecret"] = to_class(MCPInstallationSecret, self.mcp_installation_secret) + result["McpInstallationSecretStorage"] = to_enum(MCPInstallationSecretStorage, self.mcp_installation_secret_storage) + result["McpInstallationsRequest"] = to_class(MCPInstallationsRequest, self.mcp_installations_request) + result["McpInstallationState"] = to_enum(MCPInstallationState, self.mcp_installation_state) + result["McpInstallationSummary"] = to_class(MCPInstallationSummary, self.mcp_installation_summary) result["McpInstallPlan"] = to_class(MCPInstallPlan, self.mcp_install_plan) result["McpIsServerRunningRequest"] = to_class(MCPIsServerRunningRequest, self.mcp_is_server_running_request) result["McpIsServerRunningResult"] = to_class(MCPIsServerRunningResult, self.mcp_is_server_running_result) result["McpListToolsRequest"] = to_class(MCPListToolsRequest, self.mcp_list_tools_request) result["McpListToolsResult"] = to_class(MCPListToolsResult, self.mcp_list_tools_result) 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) result["McpOauthLoginRequest"] = to_class(MCPOauthLoginRequest, self.mcp_oauth_login_request) result["McpOauthLoginResult"] = to_class(MCPOauthLoginResult, self.mcp_oauth_login_result) result["McpOauthPendingRequestResponse"] = to_class(MCPOauthPendingRequestResponse, self.mcp_oauth_pending_request_response) + result["McpOauthPrepareLoginRequest"] = to_class(MCPOauthPrepareLoginRequest, self.mcp_oauth_prepare_login_request) + result["McpOauthPrepareLoginResult"] = to_class(MCPOauthPrepareLoginResult, self.mcp_oauth_prepare_login_result) result["McpOauthProbeNeedsAuthReason"] = to_enum(MCPOauthProbeNeedsAuthReason, self.mcp_oauth_probe_needs_auth_reason) result["McpOauthProbeRequest"] = to_class(MCPOauthProbeRequest, self.mcp_oauth_probe_request) result["McpOauthProbeResult"] = to_class(MCPOauthProbeResult, self.mcp_oauth_probe_result) result["McpOauthRespondRequest"] = to_class(MCPOauthRespondRequest, self.mcp_oauth_respond_request) result["McpOauthRespondResult"] = to_class(MCPOauthRespondResult, self.mcp_oauth_respond_result) + result["McpOwnedOauthLoginStatus"] = to_enum(MCPOwnedOauthLoginStatus, self.mcp_owned_oauth_login_status) result["McpPlanConfigurationChange"] = to_class(MCPPlanConfigurationChange, self.mcp_plan_configuration_change) result["McpPlanConfigurationOperation"] = to_enum(MCPPlanConfigurationOperation, self.mcp_plan_configuration_operation) result["McpPlanEnumValueType"] = to_enum(MCPPlan, self.mcp_plan_enum_value_type) @@ -45525,11 +52792,23 @@ def to_dict(self) -> dict: result["McpPlanScope"] = to_enum(MCPPlanScope, self.mcp_plan_scope) result["McpPlanSecretPlaceholder"] = to_class(MCPPlanSecretPlaceholder, self.mcp_plan_secret_placeholder) result["McpPlanSecretReference"] = from_str(self.mcp_plan_secret_reference) - result["McpPlanTarget"] = to_class(MCPPlanTarget, self.mcp_plan_target) + result["McpPlanTarget"] = to_class(MCPPlanTargetClass, self.mcp_plan_target) result["McpPlanTransportChoice"] = (self.mcp_plan_transport_choice).to_dict() result["McpPlanTransportChoicePackage"] = to_class(MCPPlanTransportChoicePackage, self.mcp_plan_transport_choice_package) result["McpPlanTransportChoiceRemote"] = to_class(MCPPlanTransportChoiceRemote, self.mcp_plan_transport_choice_remote) + result["McpPlanUninstallRequest"] = to_class(MCPPlanUninstallRequest, self.mcp_plan_uninstall_request) 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) @@ -45572,6 +52851,7 @@ def to_dict(self) -> dict: result["McpServerFailureInfo"] = to_class(MCPServerFailureInfo, self.mcp_server_failure_info) result["McpServerList"] = to_class(MCPServerList, self.mcp_server_list) result["McpServerNeedsAuthInfo"] = to_class(MCPServerNeedsAuthInfo, self.mcp_server_needs_auth_info) + result["McpServerOwnership"] = to_class(MCPServerOwnership, self.mcp_server_ownership) result["McpSetEnvValueModeDetails"] = to_enum(MCPSetEnvValueModeDetails, self.mcp_set_env_value_mode_details) result["McpSetEnvValueModeParams"] = to_class(MCPSetEnvValueModeParams, self.mcp_set_env_value_mode_params) result["McpSetEnvValueModeResult"] = to_class(MCPSetEnvValueModeResult, self.mcp_set_env_value_mode_result) @@ -45585,6 +52865,7 @@ def to_dict(self) -> dict: result["McpTools"] = to_class(MCPTools, self.mcp_tools) result["McpToolUi"] = to_class(MCPToolUI, self.mcp_tool_ui) result["McpToolUiVisibility"] = to_enum(MCPToolUIVisibility, self.mcp_tool_ui_visibility) + result["McpUninstallPlan"] = to_class(MCPUninstallPlan, self.mcp_uninstall_plan) result["McpUnregisterExternalClientRequest"] = to_class(MCPUnregisterExternalClientRequest, self.mcp_unregister_external_client_request) result["MemoryConfiguration"] = to_class(MemoryConfiguration, self.memory_configuration) result["MetadataContextAttributionResult"] = to_class(MetadataContextAttributionResult, self.metadata_context_attribution_result) @@ -45606,6 +52887,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) @@ -45627,6 +52909,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) @@ -45669,11 +52976,11 @@ def to_dict(self) -> dict: result["PermissionDecisionApproveForLocationApprovalExtensionEnvAccess"] = to_class(PermissionDecisionApproveForLocationApprovalExtensionEnvAccess, self.permission_decision_approve_for_location_approval_extension_env_access) result["PermissionDecisionApproveForLocationApprovalExtensionManagement"] = to_class(PermissionDecisionApproveForLocationApprovalExtensionManagement, self.permission_decision_approve_for_location_approval_extension_management) result["PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess"] = to_class(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess, self.permission_decision_approve_for_location_approval_extension_permission_access) - result["PermissionDecisionApproveForLocationApprovalFactory"] = to_class(PermissionDecisionApproveForLocationApprovalFactory, self.permission_decision_approve_for_location_approval_factory) result["PermissionDecisionApproveForLocationApprovalMcp"] = to_class(PermissionDecisionApproveForLocationApprovalMCP, self.permission_decision_approve_for_location_approval_mcp) result["PermissionDecisionApproveForLocationApprovalMcpSampling"] = to_class(PermissionDecisionApproveForLocationApprovalMCPSampling, self.permission_decision_approve_for_location_approval_mcp_sampling) result["PermissionDecisionApproveForLocationApprovalMemory"] = to_class(PermissionDecisionApproveForLocationApprovalMemory, self.permission_decision_approve_for_location_approval_memory) result["PermissionDecisionApproveForLocationApprovalRead"] = to_class(PermissionDecisionApproveForLocationApprovalRead, self.permission_decision_approve_for_location_approval_read) + result["PermissionDecisionApproveForLocationApprovalWorkflow"] = to_class(PermissionDecisionApproveForLocationApprovalWorkflow, self.permission_decision_approve_for_location_approval_workflow) result["PermissionDecisionApproveForLocationApprovalWrite"] = to_class(PermissionDecisionApproveForLocationApprovalWrite, self.permission_decision_approve_for_location_approval_write) result["PermissionDecisionApproveForSession"] = to_class(PermissionDecisionApproveForSession, self.permission_decision_approve_for_session) result["PermissionDecisionApproveForSessionApproval"] = (self.permission_decision_approve_for_session_approval).to_dict() @@ -45682,14 +52989,15 @@ def to_dict(self) -> dict: result["PermissionDecisionApproveForSessionApprovalExtensionEnvAccess"] = to_class(PermissionDecisionApproveForSessionApprovalExtensionEnvAccess, self.permission_decision_approve_for_session_approval_extension_env_access) result["PermissionDecisionApproveForSessionApprovalExtensionManagement"] = to_class(PermissionDecisionApproveForSessionApprovalExtensionManagement, self.permission_decision_approve_for_session_approval_extension_management) result["PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess"] = to_class(PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess, self.permission_decision_approve_for_session_approval_extension_permission_access) - result["PermissionDecisionApproveForSessionApprovalFactory"] = to_class(PermissionDecisionApproveForSessionApprovalFactory, self.permission_decision_approve_for_session_approval_factory) result["PermissionDecisionApproveForSessionApprovalMcp"] = to_class(PermissionDecisionApproveForSessionApprovalMCP, self.permission_decision_approve_for_session_approval_mcp) result["PermissionDecisionApproveForSessionApprovalMcpSampling"] = to_class(PermissionDecisionApproveForSessionApprovalMCPSampling, self.permission_decision_approve_for_session_approval_mcp_sampling) result["PermissionDecisionApproveForSessionApprovalMemory"] = to_class(PermissionDecisionApproveForSessionApprovalMemory, self.permission_decision_approve_for_session_approval_memory) result["PermissionDecisionApproveForSessionApprovalRead"] = to_class(PermissionDecisionApproveForSessionApprovalRead, self.permission_decision_approve_for_session_approval_read) + result["PermissionDecisionApproveForSessionApprovalWorkflow"] = to_class(PermissionDecisionApproveForSessionApprovalWorkflow, self.permission_decision_approve_for_session_approval_workflow) result["PermissionDecisionApproveForSessionApprovalWrite"] = to_class(PermissionDecisionApproveForSessionApprovalWrite, self.permission_decision_approve_for_session_approval_write) result["PermissionDecisionApproveOnce"] = to_class(PermissionDecisionApproveOnce, self.permission_decision_approve_once) result["PermissionDecisionApprovePermanently"] = to_class(PermissionDecisionApprovePermanently, self.permission_decision_approve_permanently) + result["PermissionDecisionApproveReadOnlyForSession"] = to_class(PermissionDecisionApproveReadOnlyForSession, self.permission_decision_approve_read_only_for_session) result["PermissionDecisionCancelled"] = to_class(PermissionDecisionCancelled, self.permission_decision_cancelled) result["PermissionDecisionContext"] = to_class(PermissionDecisionContext, self.permission_decision_context) result["PermissionDecisionDeniedByContentExclusionPolicy"] = to_class(PermissionDecisionDeniedByContentExclusionPolicy, self.permission_decision_denied_by_content_exclusion_policy) @@ -45736,11 +53044,11 @@ def to_dict(self) -> dict: result["PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess"] = to_class(PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess, self.permissions_locations_add_tool_approval_details_extension_env_access) result["PermissionsLocationsAddToolApprovalDetailsExtensionManagement"] = to_class(PermissionsLocationsAddToolApprovalDetailsExtensionManagement, self.permissions_locations_add_tool_approval_details_extension_management) result["PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess"] = to_class(PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess, self.permissions_locations_add_tool_approval_details_extension_permission_access) - result["PermissionsLocationsAddToolApprovalDetailsFactory"] = to_class(PermissionsLocationsAddToolApprovalDetailsFactory, self.permissions_locations_add_tool_approval_details_factory) result["PermissionsLocationsAddToolApprovalDetailsMcp"] = to_class(PermissionsLocationsAddToolApprovalDetailsMCP, self.permissions_locations_add_tool_approval_details_mcp) result["PermissionsLocationsAddToolApprovalDetailsMcpSampling"] = to_class(PermissionsLocationsAddToolApprovalDetailsMCPSampling, self.permissions_locations_add_tool_approval_details_mcp_sampling) result["PermissionsLocationsAddToolApprovalDetailsMemory"] = to_class(PermissionsLocationsAddToolApprovalDetailsMemory, self.permissions_locations_add_tool_approval_details_memory) result["PermissionsLocationsAddToolApprovalDetailsRead"] = to_class(PermissionsLocationsAddToolApprovalDetailsRead, self.permissions_locations_add_tool_approval_details_read) + result["PermissionsLocationsAddToolApprovalDetailsWorkflow"] = to_class(PermissionsLocationsAddToolApprovalDetailsWorkflow, self.permissions_locations_add_tool_approval_details_workflow) result["PermissionsLocationsAddToolApprovalDetailsWrite"] = to_class(PermissionsLocationsAddToolApprovalDetailsWrite, self.permissions_locations_add_tool_approval_details_write) result["PermissionsLocationsAddToolApprovalResult"] = to_class(PermissionsLocationsAddToolApprovalResult, self.permissions_locations_add_tool_approval_result) result["PermissionsModifyRulesParams"] = to_class(PermissionsModifyRulesParams, self.permissions_modify_rules_params) @@ -45807,9 +53115,11 @@ 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) + result["ProviderDescriptor"] = to_class(ProviderDescriptor, self.provider_descriptor) result["ProviderEndpoint"] = to_class(ProviderEndpoint, self.provider_endpoint) result["ProviderEndpointTransport"] = to_enum(ProviderTransport, self.provider_endpoint_transport) result["ProviderEndpointType"] = to_enum(ProviderType, self.provider_endpoint_type) @@ -45821,6 +53131,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) @@ -45902,10 +53214,9 @@ def to_dict(self) -> dict: result["RemoteSessionMode"] = to_enum(RemoteSessionMode, self.remote_session_mode) result["RemoteSessionRepository"] = to_class(RemoteSessionRepository, self.remote_session_repository) result["ResponseFormat"] = to_class(ResponseFormat, self.response_format) - result["RunOptions"] = to_class(RunOptions, self.run_options) result["SandboxConfig"] = to_class(SandboxConfig, self.sandbox_config) result["SandboxConfigAuth"] = to_class(SandboxConfigAuth, self.sandbox_config_auth) - result["SandboxConfigSource"] = to_enum(_SandboxConfigSource, self.sandbox_config_source) + result["SandboxConfigSource"] = to_enum(SandboxConfigSource, self.sandbox_config_source) result["SandboxConfigUserPolicy"] = to_class(SandboxConfigUserPolicy, self.sandbox_config_user_policy) result["SandboxConfigUserPolicyExperimental"] = to_class(SandboxConfigUserPolicyExperimental, self.sandbox_config_user_policy_experimental) result["SandboxConfigUserPolicyExperimentalSeatbelt"] = to_class(SandboxConfigUserPolicyExperimentalSeatbelt, self.sandbox_config_user_policy_experimental_seatbelt) @@ -45913,12 +53224,20 @@ def to_dict(self) -> dict: result["SandboxConfigUserPolicyNetwork"] = to_class(SandboxConfigUserPolicyNetwork, self.sandbox_config_user_policy_network) result["SandboxConfigUserPolicyNetworkProxy"] = to_class(SandboxConfigUserPolicyNetworkProxy, self.sandbox_config_user_policy_network_proxy) result["SandboxConfigUserPolicySeatbelt"] = to_class(SandboxConfigUserPolicySeatbelt, self.sandbox_config_user_policy_seatbelt) + result["SandboxCredentialsConfig"] = to_class(SandboxCredentialsConfig, self.sandbox_credentials_config) result["SandboxDisableForSessionRequest"] = to_class(SandboxDisableForSessionRequest, self.sandbox_disable_for_session_request) result["SandboxDisableForSessionResult"] = to_class(SandboxDisableForSessionResult, self.sandbox_disable_for_session_result) result["SandboxEnforcementStatus"] = to_class(SandboxEnforcementStatus, self.sandbox_enforcement_status) + result["SandboxGrantPathForRequestRequest"] = to_class(SandboxGrantPathForRequestRequest, self.sandbox_grant_path_for_request_request) + result["SandboxGrantPathForRequestResult"] = to_class(SandboxGrantPathForRequestResult, self.sandbox_grant_path_for_request_result) result["SandboxHostCapability"] = to_class(SandboxHostCapability, self.sandbox_host_capability) 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) @@ -45957,10 +53276,10 @@ def to_dict(self) -> dict: result["SessionCapability"] = to_enum(SessionCapability, self.session_capability) result["SessionCommandsListRequest"] = to_class(SessionCommandsListRequest, self.session_commands_list_request) result["SessionCompletionItem"] = to_class(SessionCompletionItem, self.session_completion_item) + result["SessionConnectedIdeInfo"] = to_class(SessionConnectedIDEInfo, self.session_connected_ide_info) result["SessionContext"] = to_class(SessionContext, self.session_context) result["SessionContextHostType"] = to_enum(HostType, self.session_context_host_type) result["SessionEnrichMetadataResult"] = to_class(SessionEnrichMetadataResult, self.session_enrich_metadata_result) - result["SessionFactoryPauseAtCheckpointResult"] = to_class(SessionFactoryPauseAtCheckpointResult, self.session_factory_pause_at_checkpoint_result) result["SessionFsAppendFileRequest"] = to_class(SessionFSAppendFileRequest, self.session_fs_append_file_request) result["SessionFsError"] = to_class(SessionFSError, self.session_fs_error) result["SessionFsErrorCode"] = to_enum(SessionFSErrorCode, self.session_fs_error_code) @@ -45973,6 +53292,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) @@ -45993,6 +53314,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) @@ -46021,6 +53343,11 @@ def to_dict(self) -> dict: result["SessionManagedPermissions"] = to_class(SessionManagedPermissions, self.session_managed_permissions) result["SessionManagedSettings"] = to_class(SessionManagedSettings, self.session_managed_settings) result["SessionMcpAppsCallToolResult"] = from_dict(lambda x: x, self.session_mcp_apps_call_tool_result) + result["SessionMcpOauthCancelLoginRequest"] = to_class(SessionMCPOauthCancelLoginRequest, self.session_mcp_oauth_cancel_login_request) + result["SessionMcpOauthCancelLoginResult"] = to_class(SessionMCPOauthCancelLoginResult, self.session_mcp_oauth_cancel_login_result) + result["SessionMcpOauthPrepareLoginRequest"] = to_class(SessionMCPOauthPrepareLoginRequest, self.session_mcp_oauth_prepare_login_request) + result["SessionMcpOauthPrepareLoginResult"] = to_class(SessionMCPOauthPrepareLoginResult, self.session_mcp_oauth_prepare_login_result) + result["SessionMcpSetConnectedIdeInfoParams"] = to_class(SessionMCPSetConnectedIDEInfoParams, self.session_mcp_set_connected_ide_info_params) result["SessionMetadataSnapshot"] = to_class(SessionMetadataSnapshot, self.session_metadata_snapshot) result["SessionMode"] = to_enum(SessionMode, self.session_mode) result["SessionModelList"] = to_class(SessionModelList, self.session_model_list) @@ -46048,6 +53375,8 @@ def to_dict(self) -> dict: result["SessionsClientMetadataEntry"] = to_class(SessionsClientMetadataEntry, self.sessions_client_metadata_entry) result["SessionsCloseRequest"] = to_class(SessionsCloseRequest, self.sessions_close_request) result["SessionsCloseResult"] = to_class(SessionsCloseResult, self.sessions_close_result) + result["SessionsCreateWorkspaceRequest"] = to_class(SessionsCreateWorkspaceRequest, self.sessions_create_workspace_request) + result["SessionsCreateWorkspaceResult"] = to_class(SessionsCreateWorkspaceResult, self.sessions_create_workspace_result) result["SessionsDeleteRequest"] = to_class(SessionsDeleteRequest, self.sessions_delete_request) result["SessionsEnrichMetadataRequest"] = to_class(SessionsEnrichMetadataRequest, self.sessions_enrich_metadata_request) result["SessionSetCredentialsParams"] = to_class(SessionSetCredentialsParams, self.session_set_credentials_params) @@ -46085,6 +53414,8 @@ def to_dict(self) -> dict: result["SessionsListNonEmptySessionIdsResult"] = to_class(SessionsListNonEmptySessionIDSResult, self.sessions_list_non_empty_session_ids_result) result["SessionsListRequest"] = to_class(SessionsListRequest, self.sessions_list_request) result["SessionsLoadDeferredRepoHooksRequest"] = to_class(SessionsLoadDeferredRepoHooksRequest, self.sessions_load_deferred_repo_hooks_request) + result["SessionsLoadWorkspaceRequest"] = to_class(SessionsLoadWorkspaceRequest, self.sessions_load_workspace_request) + result["SessionsLoadWorkspaceResult"] = to_class(SessionsLoadWorkspaceResult, self.sessions_load_workspace_result) result["SessionsOpenAttach"] = to_class(SessionsOpenAttach, self.sessions_open_attach) result["SessionsOpenCloud"] = to_class(SessionsOpenCloud, self.sessions_open_cloud) result["SessionsOpenCreate"] = to_class(SessionsOpenCreate, self.sessions_open_create) @@ -46112,6 +53443,8 @@ def to_dict(self) -> dict: result["SessionsStartRemoteControlRequest"] = to_class(SessionsStartRemoteControlRequest, self.sessions_start_remote_control_request) result["SessionsStopRemoteControlRequest"] = to_class(SessionsStopRemoteControlRequest, self.sessions_stop_remote_control_request) result["SessionsTransferRemoteControlRequest"] = to_class(SessionsTransferRemoteControlRequest, self.sessions_transfer_remote_control_request) + result["SessionsUpdateWorkspaceFieldsRequest"] = to_class(SessionsUpdateWorkspaceFieldsRequest, self.sessions_update_workspace_fields_request) + result["SessionsUpdateWorkspaceFieldsResult"] = to_class(SessionsUpdateWorkspaceFieldsResult, self.sessions_update_workspace_fields_result) result["SessionTelemetryEngagement"] = to_class(SessionTelemetryEngagement, self.session_telemetry_engagement) result["SessionUpdateOptionsParams"] = to_class(SessionUpdateOptionsParams, self.session_update_options_params) result["SessionUpdateOptionsResult"] = to_class(SessionUpdateOptionsResult, self.session_update_options_result) @@ -46119,6 +53452,7 @@ def to_dict(self) -> dict: result["SessionWorkflowPauseAtCheckpointResult"] = to_class(SessionWorkflowPauseAtCheckpointResult, self.session_workflow_pause_at_checkpoint_result) result["SessionWorkingDirectoryContext"] = to_class(SessionWorkingDirectoryContext, self.session_working_directory_context) result["SessionWorkingDirectoryContextHostType"] = to_enum(HostType, self.session_working_directory_context_host_type) + result["SessionWorkingDirectoryContextWithClient"] = to_class(SessionWorkingDirectoryContextWithClient, self.session_working_directory_context_with_client) result["SettableAuthInfo"] = (self.settable_auth_info).to_dict() result["SettableTokenAuthInfo"] = to_class(SettableTokenAuthInfo, self.settable_token_auth_info) result["ShellCancelUserRequestedRequest"] = to_class(ShellCancelUserRequestedRequest, self.shell_cancel_user_requested_request) @@ -46135,10 +53469,31 @@ def to_dict(self) -> dict: result["ShellOptions"] = to_class(ShellOptions, self.shell_options) result["ShutdownRequest"] = to_class(ShutdownRequest, self.shutdown_request) result["Skill"] = to_class(Skill, self.skill) + result["SkillApplyInstallRequest"] = to_class(SkillApplyInstallRequest, self.skill_apply_install_request) + result["SkillApplyUninstallRequest"] = to_class(SkillApplyUninstallRequest, self.skill_apply_uninstall_request) result["SkillDiscoveryPath"] = to_class(SkillDiscoveryPath, self.skill_discovery_path) result["SkillDiscoveryPathList"] = to_class(SkillDiscoveryPathList, self.skill_discovery_path_list) result["SkillDiscoveryScope"] = to_enum(SkillDiscoveryScope, self.skill_discovery_scope) + result["SkillInstallationFailureReason"] = to_enum(SkillInstallationFailureReason, self.skill_installation_failure_reason) + result["SkillInstallationFileReview"] = to_class(SkillInstallationFileReview, self.skill_installation_file_review) + result["SkillInstallationLocation"] = to_class(SkillInstallationLocation, self.skill_installation_location) + result["SkillInstallationManagementOutcome"] = to_class(SkillInstallationManagementOutcome, self.skill_installation_management_outcome) + result["SkillInstallationManagementResult"] = to_class(SkillInstallationManagementResult, self.skill_installation_management_result) + result["SkillInstallationOperationRequest"] = to_class(SkillInstallationOperationRequest, self.skill_installation_operation_request) + result["SkillInstallationOperationStatus"] = to_class(SkillInstallationOperationStatus, self.skill_installation_operation_status) + result["SkillInstallationOutcome"] = to_class(SkillInstallationOutcome, self.skill_installation_outcome) + result["SkillInstallationOwnershipState"] = to_enum(SkillInstallationOwnershipState, self.skill_installation_ownership_state) + result["SkillInstallationResult"] = to_class(SkillInstallationResult, self.skill_installation_result) + result["SkillInstallationReview"] = to_class(SkillInstallationReview, self.skill_installation_review) + result["SkillInstallationScope"] = to_enum(SkillInstallationScope, self.skill_installation_scope) + result["SkillInstallationSessionState"] = to_enum(SkillInstallationSessionState, self.skill_installation_session_state) + result["SkillInstallationSource"] = to_class(SkillInstallationSource, self.skill_installation_source) + result["SkillInstallationsRequest"] = to_class(SkillInstallationsRequest, self.skill_installations_request) + result["SkillInstallationSummary"] = to_class(SkillInstallationSummary, self.skill_installation_summary) + result["SkillInstallPlan"] = to_class(SkillInstallPlan, self.skill_install_plan) result["SkillList"] = to_class(SkillList, self.skill_list) + result["SkillPlanInstallRequest"] = to_class(SkillPlanInstallRequest, self.skill_plan_install_request) + result["SkillPlanUninstallRequest"] = to_class(SkillPlanUninstallRequest, self.skill_plan_uninstall_request) result["SkillProviderDescriptor"] = to_class(SkillProviderDescriptor, self.skill_provider_descriptor) result["SkillProviderListRequest"] = to_class(SkillProviderListRequest, self.skill_provider_list_request) result["SkillProviderListResult"] = to_class(_SkillProviderListResult, self.skill_provider_list_result) @@ -46149,10 +53504,12 @@ def to_dict(self) -> dict: result["SkillsDisableRequest"] = to_class(SkillsDisableRequest, self.skills_disable_request) result["SkillsDiscoverRequest"] = to_class(SkillsDiscoverRequest, self.skills_discover_request) result["SkillsEnableRequest"] = to_class(SkillsEnableRequest, self.skills_enable_request) + result["SkillSetEnabledRequest"] = to_class(SkillSetEnabledRequest, self.skill_set_enabled_request) result["SkillsGetDiscoveryPathsRequest"] = to_class(SkillsGetDiscoveryPathsRequest, self.skills_get_discovery_paths_request) result["SkillsGetInvokedResult"] = to_class(SkillsGetInvokedResult, self.skills_get_invoked_result) result["SkillsInvokedSkill"] = to_class(SkillsInvokedSkill, self.skills_invoked_skill) result["SkillsLoadDiagnostics"] = to_class(SkillsLoadDiagnostics, self.skills_load_diagnostics) + result["SkillUninstallPlan"] = to_class(SkillUninstallPlan, self.skill_uninstall_plan) result["SlashCommandAddTimelineEntryResult"] = to_class(SlashCommandAddTimelineEntryResult, self.slash_command_add_timeline_entry_result) result["SlashCommandAgentPromptResult"] = to_class(SlashCommandAgentPromptResult, self.slash_command_agent_prompt_result) result["SlashCommandCompletedResult"] = to_class(SlashCommandCompletedResult, self.slash_command_completed_result) @@ -46291,7 +53648,6 @@ def to_dict(self) -> dict: result["UserSettingMetadata"] = to_class(UserSettingMetadata, self.user_setting_metadata) result["UserSettingsGetResult"] = to_class(UserSettingsGetResult, self.user_settings_get_result) result["UserSettingsSetRequest"] = to_class(UserSettingsSetRequest, self.user_settings_set_request) - result["UserSettingsSetResult"] = to_class(UserSettingsSetResult, self.user_settings_set_result) result["VisibilityGetResult"] = to_class(VisibilityGetResult, self.visibility_get_result) result["VisibilitySetRequest"] = to_class(VisibilitySetRequest, self.visibility_set_request) result["VisibilitySetResult"] = to_class(VisibilitySetResult, self.visibility_set_result) @@ -46304,7 +53660,7 @@ def to_dict(self) -> dict: result["WorkflowCancelRequest"] = to_class(WorkflowCancelRequest, self.workflow_cancel_request) result["WorkflowCurrentPhase"] = to_class(WorkflowCurrentPhase, self.workflow_current_phase) result["WorkflowDeclaredLimits"] = to_class(WorkflowDeclaredLimits, self.workflow_declared_limits) - result["WorkflowDurableOperation"] = to_enum(DurableOperation, self.workflow_durable_operation) + result["WorkflowDurableOperation"] = to_enum(WorkflowDurableOperation, self.workflow_durable_operation) result["WorkflowExecuteRequest"] = to_class(WorkflowExecuteRequest, self.workflow_execute_request) result["WorkflowExecuteResult"] = to_class(WorkflowExecuteResult, self.workflow_execute_result) result["WorkflowGetRunProgressRequest"] = to_class(WorkflowGetRunProgressRequest, self.workflow_get_run_progress_request) @@ -46315,15 +53671,15 @@ def to_dict(self) -> dict: result["WorkflowListRunsRequest"] = to_class(WorkflowListRunsRequest, self.workflow_list_runs_request) result["WorkflowListRunsResult"] = to_class(WorkflowListRunsResult, self.workflow_list_runs_result) result["WorkflowLogLine"] = to_class(WorkflowLogLine, self.workflow_log_line) - result["WorkflowLogLineKind"] = to_enum(LogLineKind, self.workflow_log_line_kind) + result["WorkflowLogLineKind"] = to_enum(WorkflowLogLineKind, self.workflow_log_line_kind) result["WorkflowLogRequest"] = to_class(WorkflowLogRequest, self.workflow_log_request) - result["WorkflowPauseCheckpointAction"] = to_enum(PauseCheckpointAction, self.workflow_pause_checkpoint_action) + result["WorkflowPauseCheckpointAction"] = to_enum(WorkflowPauseCheckpointAction, self.workflow_pause_checkpoint_action) result["WorkflowPauseCheckpointRequest"] = to_class(WorkflowPauseCheckpointRequest, self.workflow_pause_checkpoint_request) result["WorkflowPauseCheckpointResult"] = to_class(WorkflowPauseCheckpointResult, self.workflow_pause_checkpoint_result) result["WorkflowPauseInfo"] = to_class(WorkflowPauseInfo, self.workflow_pause_info) result["WorkflowPauseRequest"] = to_class(WorkflowPauseRequest, self.workflow_pause_request) result["WorkflowPhaseObservation"] = to_class(WorkflowPhaseObservation, self.workflow_phase_observation) - result["WorkflowPhaseStatus"] = to_enum(PhaseStatus, self.workflow_phase_status) + result["WorkflowPhaseStatus"] = to_enum(WorkflowPhaseStatus, self.workflow_phase_status) result["WorkflowProgressLine"] = to_class(WorkflowProgressLine, self.workflow_progress_line) result["WorkflowProgressPage"] = to_class(WorkflowProgressPage, self.workflow_progress_page) result["WorkflowResumeRequest"] = to_class(WorkflowResumeRequest, self.workflow_resume_request) @@ -46331,12 +53687,12 @@ def to_dict(self) -> dict: result["WorkflowRunConsumed"] = to_class(WorkflowRunConsumed, self.workflow_run_consumed) result["WorkflowRunDetail"] = to_class(WorkflowRunDetail, self.workflow_run_detail) result["WorkflowRunFailure"] = to_class(WorkflowRunFailure, self.workflow_run_failure) - result["WorkflowRunFailureKind"] = to_enum(RunFailureKind, self.workflow_run_failure_kind) + result["WorkflowRunFailureKind"] = to_enum(WorkflowRunFailureKind, self.workflow_run_failure_kind) result["WorkflowRunLimits"] = to_class(WorkflowRunLimits, self.workflow_run_limits) result["WorkflowRunOptions"] = to_class(WorkflowRunOptions, self.workflow_run_options) result["WorkflowRunRequest"] = to_class(WorkflowRunRequest, self.workflow_run_request) result["WorkflowRunResult"] = to_class(WorkflowRunResult, self.workflow_run_result) - result["WorkflowRunStatus"] = to_enum(RunStatus, self.workflow_run_status) + result["WorkflowRunStatus"] = to_enum(WorkflowRunStatus, self.workflow_run_status) result["WorkflowRunSummary"] = to_class(WorkflowRunSummary, self.workflow_run_summary) result["WorkflowRunTerminal"] = to_class(WorkflowRunTerminal, self.workflow_run_terminal) result["WorkflowToolResumeRequest"] = to_class(_WorkflowToolResumeRequest, self.workflow_tool_resume_request) @@ -46375,6 +53731,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) @@ -46389,6 +53746,16 @@ def rpc_from_dict(s: Any) -> RPC: def rpc_to_dict(x: RPC) -> Any: return to_class(RPC, x) +# Experimental: this type is part of an experimental API and may change or be removed. +class ConnectorDiscoveryAvailability(Enum): + "Availability." + # Enabled. + ENABLED = "enabled" + # Disabled. + DISABLED = "disabled" + # Unavailable. + UNAVAILABLE = "unavailable" + # Outcome of an agentRegistry.spawn call. AgentRegistrySpawnResult = AgentRegistrySpawnSpawned | AgentRegistrySpawnError | AgentRegistrySpawnRegistryTimeout | AgentRegistrySpawnValidationError @@ -46403,7 +53770,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) @@ -46415,6 +53782,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}") @@ -46581,7 +53949,7 @@ def _load_MCPServerCardReference(obj: Any) -> "MCPServerCardReference": case _: raise ValueError(f"Unknown MCPServerCardReference kind: {kind!r}") # The client's response to the pending permission prompt -PermissionDecision = PermissionDecisionApproveOnce | PermissionDecisionApproveForSession | PermissionDecisionApproveForLocation | PermissionDecisionApprovePermanently | PermissionDecisionReject | PermissionDecisionUserNotAvailable | PermissionDecisionApproved | PermissionDecisionApprovedForSession | PermissionDecisionApprovedForLocation | PermissionDecisionCancelled | PermissionDecisionDeniedByRules | PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDecisionDeniedInteractivelyByUser | PermissionDecisionDeniedByContentExclusionPolicy | PermissionDecisionDeniedByPermissionRequestHook +PermissionDecision = PermissionDecisionApproveOnce | PermissionDecisionApproveForSession | PermissionDecisionApproveReadOnlyForSession | PermissionDecisionApproveForLocation | PermissionDecisionApprovePermanently | PermissionDecisionReject | PermissionDecisionUserNotAvailable | PermissionDecisionApproved | PermissionDecisionApprovedForSession | PermissionDecisionApprovedForLocation | PermissionDecisionCancelled | PermissionDecisionDeniedByRules | PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDecisionDeniedInteractivelyByUser | PermissionDecisionDeniedByContentExclusionPolicy | PermissionDecisionDeniedByPermissionRequestHook def _load_PermissionDecision(obj: Any) -> "PermissionDecision": assert isinstance(obj, dict) @@ -46589,6 +53957,7 @@ def _load_PermissionDecision(obj: Any) -> "PermissionDecision": match kind: case "approve-once": return PermissionDecisionApproveOnce.from_dict(obj) case "approve-for-session": return PermissionDecisionApproveForSession.from_dict(obj) + case "approve-read-only-for-session": return PermissionDecisionApproveReadOnlyForSession.from_dict(obj) case "approve-for-location": return PermissionDecisionApproveForLocation.from_dict(obj) case "approve-permanently": return PermissionDecisionApprovePermanently.from_dict(obj) case "reject": return PermissionDecisionReject.from_dict(obj) @@ -46605,7 +53974,7 @@ def _load_PermissionDecision(obj: Any) -> "PermissionDecision": case _: raise ValueError(f"Unknown PermissionDecision kind: {kind!r}") # Approval to persist for this location -PermissionDecisionApproveForLocationApproval = PermissionDecisionApproveForLocationApprovalCommands | PermissionDecisionApproveForLocationApprovalRead | PermissionDecisionApproveForLocationApprovalWrite | PermissionDecisionApproveForLocationApprovalMCP | PermissionDecisionApproveForLocationApprovalMCPSampling | PermissionDecisionApproveForLocationApprovalMemory | PermissionDecisionApproveForLocationApprovalCustomTool | PermissionDecisionApproveForLocationApprovalExtensionManagement | PermissionDecisionApproveForLocationApprovalFactory | PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess | PermissionDecisionApproveForLocationApprovalExtensionEnvAccess +PermissionDecisionApproveForLocationApproval = PermissionDecisionApproveForLocationApprovalCommands | PermissionDecisionApproveForLocationApprovalRead | PermissionDecisionApproveForLocationApprovalWrite | PermissionDecisionApproveForLocationApprovalMCP | PermissionDecisionApproveForLocationApprovalMCPSampling | PermissionDecisionApproveForLocationApprovalMemory | PermissionDecisionApproveForLocationApprovalCustomTool | PermissionDecisionApproveForLocationApprovalExtensionManagement | PermissionDecisionApproveForLocationApprovalWorkflow | PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess | PermissionDecisionApproveForLocationApprovalExtensionEnvAccess def _load_PermissionDecisionApproveForLocationApproval(obj: Any) -> "PermissionDecisionApproveForLocationApproval": assert isinstance(obj, dict) @@ -46619,13 +53988,13 @@ def _load_PermissionDecisionApproveForLocationApproval(obj: Any) -> "PermissionD case "memory": return PermissionDecisionApproveForLocationApprovalMemory.from_dict(obj) case "custom-tool": return PermissionDecisionApproveForLocationApprovalCustomTool.from_dict(obj) case "extension-management": return PermissionDecisionApproveForLocationApprovalExtensionManagement.from_dict(obj) - case "factory": return PermissionDecisionApproveForLocationApprovalFactory.from_dict(obj) + case "workflow": return PermissionDecisionApproveForLocationApprovalWorkflow.from_dict(obj) case "extension-permission-access": return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionDecisionApproveForLocationApprovalExtensionEnvAccess.from_dict(obj) case _: raise ValueError(f"Unknown PermissionDecisionApproveForLocationApproval kind: {kind!r}") # Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) -PermissionDecisionApproveForSessionApproval = PermissionDecisionApproveForSessionApprovalCommands | PermissionDecisionApproveForSessionApprovalRead | PermissionDecisionApproveForSessionApprovalWrite | PermissionDecisionApproveForSessionApprovalMCP | PermissionDecisionApproveForSessionApprovalMCPSampling | PermissionDecisionApproveForSessionApprovalMemory | PermissionDecisionApproveForSessionApprovalCustomTool | PermissionDecisionApproveForSessionApprovalExtensionManagement | PermissionDecisionApproveForSessionApprovalFactory | PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess | PermissionDecisionApproveForSessionApprovalExtensionEnvAccess +PermissionDecisionApproveForSessionApproval = PermissionDecisionApproveForSessionApprovalCommands | PermissionDecisionApproveForSessionApprovalRead | PermissionDecisionApproveForSessionApprovalWrite | PermissionDecisionApproveForSessionApprovalMCP | PermissionDecisionApproveForSessionApprovalMCPSampling | PermissionDecisionApproveForSessionApprovalMemory | PermissionDecisionApproveForSessionApprovalCustomTool | PermissionDecisionApproveForSessionApprovalExtensionManagement | PermissionDecisionApproveForSessionApprovalWorkflow | PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess | PermissionDecisionApproveForSessionApprovalExtensionEnvAccess def _load_PermissionDecisionApproveForSessionApproval(obj: Any) -> "PermissionDecisionApproveForSessionApproval": assert isinstance(obj, dict) @@ -46639,13 +54008,13 @@ def _load_PermissionDecisionApproveForSessionApproval(obj: Any) -> "PermissionDe case "memory": return PermissionDecisionApproveForSessionApprovalMemory.from_dict(obj) case "custom-tool": return PermissionDecisionApproveForSessionApprovalCustomTool.from_dict(obj) case "extension-management": return PermissionDecisionApproveForSessionApprovalExtensionManagement.from_dict(obj) - case "factory": return PermissionDecisionApproveForSessionApprovalFactory.from_dict(obj) + case "workflow": return PermissionDecisionApproveForSessionApprovalWorkflow.from_dict(obj) case "extension-permission-access": return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionDecisionApproveForSessionApprovalExtensionEnvAccess.from_dict(obj) case _: raise ValueError(f"Unknown PermissionDecisionApproveForSessionApproval kind: {kind!r}") # Tool approval to persist and apply -PermissionsLocationsAddToolApprovalDetails = PermissionsLocationsAddToolApprovalDetailsCommands | PermissionsLocationsAddToolApprovalDetailsRead | PermissionsLocationsAddToolApprovalDetailsWrite | PermissionsLocationsAddToolApprovalDetailsMCP | PermissionsLocationsAddToolApprovalDetailsMCPSampling | PermissionsLocationsAddToolApprovalDetailsMemory | PermissionsLocationsAddToolApprovalDetailsCustomTool | PermissionsLocationsAddToolApprovalDetailsExtensionManagement | PermissionsLocationsAddToolApprovalDetailsFactory | PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess | PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess +PermissionsLocationsAddToolApprovalDetails = PermissionsLocationsAddToolApprovalDetailsCommands | PermissionsLocationsAddToolApprovalDetailsRead | PermissionsLocationsAddToolApprovalDetailsWrite | PermissionsLocationsAddToolApprovalDetailsMCP | PermissionsLocationsAddToolApprovalDetailsMCPSampling | PermissionsLocationsAddToolApprovalDetailsMemory | PermissionsLocationsAddToolApprovalDetailsCustomTool | PermissionsLocationsAddToolApprovalDetailsExtensionManagement | PermissionsLocationsAddToolApprovalDetailsWorkflow | PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess | PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess def _load_PermissionsLocationsAddToolApprovalDetails(obj: Any) -> "PermissionsLocationsAddToolApprovalDetails": assert isinstance(obj, dict) @@ -46659,7 +54028,7 @@ def _load_PermissionsLocationsAddToolApprovalDetails(obj: Any) -> "PermissionsLo case "memory": return PermissionsLocationsAddToolApprovalDetailsMemory.from_dict(obj) case "custom-tool": return PermissionsLocationsAddToolApprovalDetailsCustomTool.from_dict(obj) case "extension-management": return PermissionsLocationsAddToolApprovalDetailsExtensionManagement.from_dict(obj) - case "factory": return PermissionsLocationsAddToolApprovalDetailsFactory.from_dict(obj) + case "workflow": return PermissionsLocationsAddToolApprovalDetailsWorkflow.from_dict(obj) case "extension-permission-access": return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess.from_dict(obj) case _: raise ValueError(f"Unknown PermissionsLocationsAddToolApprovalDetails kind: {kind!r}") @@ -46740,7 +54109,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) @@ -46751,6 +54120,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}") @@ -46793,21 +54163,15 @@ def _load_TaskInfo(obj: Any) -> "TaskInfo": CanvasJsonSchema = Any CardDigestValue = str CatalogCapabilityId = str -CatalogMcpServerInstallability = CatalogMCPServerInstallabilityEnum CatalogResourceIdentity = str CatalogResourceVersion = str ClientMetadata = dict CommandsListRequest = Any +ConnectorSessionAccountResult = ConnectorSessionAccount | None DiagnosticCursorStatus = CursorStatus EventsCursorStatus = CursorStatus ExternalToolResult = ExternalToolTextResultForLlm ExternalToolTextResultForLlmContentResourceLinkIconTheme = Theme -FactoryDurableOperation = DurableOperation -FactoryLogLineKind = LogLineKind -FactoryPauseCheckpointAction = PauseCheckpointAction -FactoryPhaseStatus = PhaseStatus -FactoryRunFailureKind = RunFailureKind -FactoryRunStatus = RunStatus FilterMapping = dict HistoryCompactRequest = Any InstructionDiscoveryPathKind = DebugCollectLogsEntryKind @@ -46853,7 +54217,7 @@ def _load_TaskInfo(obj: Any) -> "TaskInfo": ProviderGetEndpointRequest = Any RemoteSessionMetadataTaskType = TaskType SandboxHostCapabilityName = str -SessionAuthInfoResult = AuthIdentity +SessionAuthInfoResult = AuthIdentity | None SessionCancelAllBackgroundAgentsResult = int SessionContextHostType = HostType SessionFsReaddirWithTypesEntryType = DebugCollectLogsEntryKind @@ -46871,12 +54235,6 @@ def _load_TaskInfo(obj: Any) -> "TaskInfo": TaskInfoExecutionMode = TaskExecutionMode TaskInfoStatus = TaskStatus ToolResult = ToolResultExpanded -WorkflowDurableOperation = DurableOperation -WorkflowLogLineKind = LogLineKind -WorkflowPauseCheckpointAction = PauseCheckpointAction -WorkflowPhaseStatus = PhaseStatus -WorkflowRunFailureKind = RunFailureKind -WorkflowRunStatus = RunStatus WorkspaceSummaryHostType = HostType WorkspacesWorkspaceDetailsHostType = HostType @@ -46908,6 +54266,48 @@ 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"): + self._client = client + + async def publish_session(self, params: HostPublishSessionRequest, *, timeout: float | None = None) -> HostPublishSessionResult: + "Publishes an attached resident session for this listener's lifetime without copying it.\n\nArgs:\n params: Publishes a resident session attached to the listener's owning connection.\n\nReturns:\n The existing runtime identity and its resource on the listener." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + 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 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))) + + async def dispose(self, params: HostDisposeRequest, *, timeout: float | None = None) -> HostEmptyResult: + "Stops a listener owned by this SDK connection and joins its cleanup without deleting sessions.\n\nArgs:\n params: Stops a connection-owned listener and joins its teardown.\n\nReturns:\n Empty acknowledgement for a completed host lifecycle operation." + 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.dispose", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerHooksApi: def __init__(self, client: "JsonRpcClient"): @@ -46934,10 +54334,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." @@ -47034,11 +54465,38 @@ async def reload(self, *, timeout: float | None = None) -> None: await self._client.request("mcp.config.reload", {}, **_timeout_kwargs(timeout)) +# Experimental: this API group is experimental and may change or be removed. +class ServerMcpInstallationsApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def list(self, params: MCPInstallationsRequest, *, timeout: float | None = None) -> MCPInstallationManagementResult: + "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.\n\nArgs:\n params: New-work inventory or recovery request under an explicitly selected existing session.\n\nReturns:\n Management result with contract receipt, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationManagementResult.from_dict(await self._client.request("mcp.installations.list", params_dict, **_timeout_kwargs(timeout))) + + async def recover(self, params: MCPInstallationsRequest, *, timeout: float | None = None) -> MCPInstallationManagementResult: + "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.\n\nArgs:\n params: New-work inventory or recovery request under an explicitly selected existing session.\n\nReturns:\n Management result with contract receipt, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationManagementResult.from_dict(await self._client.request("mcp.installations.recover", params_dict, **_timeout_kwargs(timeout))) + + async def status(self, params: MCPInstallationOperationRequest, *, timeout: float | None = None) -> MCPInstallationManagementResult: + "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.\n\nArgs:\n params: Existing-operation control. A new session selector is deliberately not accepted.\n\nReturns:\n Management result with contract receipt, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationManagementResult.from_dict(await self._client.request("mcp.installations.status", params_dict, **_timeout_kwargs(timeout))) + + async def cancel(self, params: MCPInstallationOperationRequest, *, timeout: float | None = None) -> MCPInstallationManagementResult: + "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.\n\nArgs:\n params: Existing-operation control. A new session selector is deliberately not accepted.\n\nReturns:\n Management result with contract receipt, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationManagementResult.from_dict(await self._client.request("mcp.installations.cancel", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerMcpApi: def __init__(self, client: "JsonRpcClient"): self._client = client self.config = ServerMcpConfigApi(client) + self.installations = ServerMcpInstallationsApi(client) async def discover(self, params: MCPDiscoverRequest, *, timeout: float | None = None) -> MCPDiscoverResult: "Discovers MCP servers from user, workspace, plugin, and builtin sources.\n\nArgs:\n params: Optional working directory used as context for MCP server discovery.\n\nReturns:\n MCP servers discovered from user, workspace, plugin, and built-in sources." @@ -47050,6 +54508,26 @@ async def plan_install(self, params: MCPPlanInstallRequest, *, timeout: float | params_dict = {k: v for k, v in params.to_dict().items() if v is not None} return _load_MCPPlanInstallResult(await self._client.request("mcp.planInstall", params_dict, **_timeout_kwargs(timeout))) + async def prepare_install(self, params: MCPPrepareInstallRequest, *, timeout: float | None = None) -> MCPInstallationManagementResult: + "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.\n\nArgs:\n params: Side-effect-free preparation of one original bound remote MCP choice.\n\nReturns:\n Management result with contract receipt, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationManagementResult.from_dict(await self._client.request("mcp.prepareInstall", params_dict, **_timeout_kwargs(timeout))) + + async def apply_install(self, params: MCPApplyInstallRequest, *, timeout: float | None = None) -> MCPInstallationResult: + "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.\n\nArgs:\n params: Applies exactly one previously prepared operation on its original connection.\n\nReturns:\n An installation result together with the exact honoured contract, or a negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationResult.from_dict(await self._client.request("mcp.applyInstall", params_dict, **_timeout_kwargs(timeout))) + + async def plan_uninstall(self, params: MCPPlanUninstallRequest, *, timeout: float | None = None) -> MCPInstallationManagementResult: + "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.\n\nArgs:\n params: Read-only preparation of one owned removal under fresh selected-session authority.\n\nReturns:\n Management result with contract receipt, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationManagementResult.from_dict(await self._client.request("mcp.planUninstall", params_dict, **_timeout_kwargs(timeout))) + + async def apply_uninstall(self, params: MCPApplyUninstallRequest, *, timeout: float | None = None) -> MCPInstallationResult: + "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.\n\nArgs:\n params: One-use application of the exact retained removal plan.\n\nReturns:\n An installation result together with the exact honoured contract, or a negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return MCPInstallationResult.from_dict(await self._client.request("mcp.applyUninstall", params_dict, **_timeout_kwargs(timeout))) + # Experimental: this API group is experimental and may change or be removed. class ServerExtensionsApi: @@ -47071,6 +54549,91 @@ async def disable(self, params: DiscoveredExtensionsDisableRequest, *, timeout: await self._client.request("extensions.disable", params_dict, **_timeout_kwargs(timeout)) +# Experimental: this API group is experimental and may change or be removed. +class ServerSkillsInstallationsApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def list(self, params: SkillInstallationsRequest, *, timeout: float | None = None) -> SkillInstallationManagementResult: + "Lists owned verified Agent Finder Skill installations for the selected existing session. Listing is never gated by the Skill-install feature flag.\n\nArgs:\n params: Inventory request under an explicitly selected existing session.\n\nReturns:\n Skill installation management result with the honoured contract, or a typed refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationManagementResult.from_dict(await self._client.request("skills.installations.list", params_dict, **_timeout_kwargs(timeout))) + + async def recover(self, params: SkillInstallationsRequest, *, timeout: float | None = None) -> SkillInstallationManagementResult: + "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.\n\nArgs:\n params: Inventory request under an explicitly selected existing session.\n\nReturns:\n Skill installation management result with the honoured contract, or a typed refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationManagementResult.from_dict(await self._client.request("skills.installations.recover", params_dict, **_timeout_kwargs(timeout))) + + async def status(self, params: SkillInstallationOperationRequest, *, timeout: float | None = None) -> SkillInstallationManagementResult: + "Inspects a known Skill installation operation on its original runtime connection. Status is never gated by the Skill-install feature flag.\n\nArgs:\n params: Existing-operation control. A new session selector is deliberately not accepted.\n\nReturns:\n Skill installation management result with the honoured contract, or a typed refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationManagementResult.from_dict(await self._client.request("skills.installations.status", params_dict, **_timeout_kwargs(timeout))) + + async def cancel(self, params: SkillInstallationOperationRequest, *, timeout: float | None = None) -> SkillInstallationManagementResult: + "Requests cancellation of a known Skill installation operation before commit. Already-started durable work requires recovery instead of silent replay.\n\nArgs:\n params: Existing-operation control. A new session selector is deliberately not accepted.\n\nReturns:\n Skill installation management result with the honoured contract, or a typed refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationManagementResult.from_dict(await self._client.request("skills.installations.cancel", params_dict, **_timeout_kwargs(timeout))) + + async def set_enabled(self, params: SkillSetEnabledRequest, *, timeout: float | None = None) -> SkillInstallationManagementResult: + "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.\n\nArgs:\n params: Persisted enablement update for one owned Skill installation.\n\nReturns:\n Skill installation management result with the honoured contract, or a typed refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationManagementResult.from_dict(await self._client.request("skills.installations.setEnabled", params_dict, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class ServerSkillsConfigApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def set_disabled_skills(self, params: SkillsConfigSetDisabledSkillsRequest, *, timeout: float | None = None) -> None: + "Replaces the global list of disabled skills.\n\nArgs:\n params: Skill names to mark as disabled in global configuration, replacing any previous list." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + await self._client.request("skills.config.setDisabledSkills", params_dict, **_timeout_kwargs(timeout)) + + async def set_skill_disabled(self, params: SkillsConfigSetSkillDisabledRequest, *, timeout: float | None = None) -> None: + "Atomically adds or removes one skill from the disabled list.\n\nArgs:\n params: Adds or removes a single skill from the global disabled list, leaving every other entry untouched." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + await self._client.request("skills.config.setSkillDisabled", params_dict, **_timeout_kwargs(timeout)) + + +# Experimental: this API group is experimental and may change or be removed. +class ServerSkillsApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + self.installations = ServerSkillsInstallationsApi(client) + self.config = ServerSkillsConfigApi(client) + + async def plan_install(self, params: SkillPlanInstallRequest, *, timeout: float | None = None) -> SkillInstallationManagementResult: + "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.\n\nArgs:\n params: Side-effect-free planning of one verified Agent Finder Skill candidate.\n\nReturns:\n Skill installation management result with the honoured contract, or a typed refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationManagementResult.from_dict(await self._client.request("skills.planInstall", params_dict, **_timeout_kwargs(timeout))) + + async def apply_install(self, params: SkillApplyInstallRequest, *, timeout: float | None = None) -> SkillInstallationResult: + "Consumes one verified Skill installation plan, requests explicit human consent through installations.confirm on the original connection, then revalidates and installs the Skill disabled.\n\nArgs:\n params: Applies exactly one retained verified Skill installation plan.\n\nReturns:\n Skill installation result with the honoured contract, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationResult.from_dict(await self._client.request("skills.applyInstall", params_dict, **_timeout_kwargs(timeout))) + + async def plan_uninstall(self, params: SkillPlanUninstallRequest, *, timeout: float | None = None) -> SkillInstallationManagementResult: + "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.\n\nArgs:\n params: Read-only preparation of one owned Skill removal under fresh selected-session authority.\n\nReturns:\n Skill installation management result with the honoured contract, or a typed refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationManagementResult.from_dict(await self._client.request("skills.planUninstall", params_dict, **_timeout_kwargs(timeout))) + + async def apply_uninstall(self, params: SkillApplyUninstallRequest, *, timeout: float | None = None) -> SkillInstallationResult: + "Consumes an owned Skill removal plan, requests explicit human consent through installations.confirm, refuses drift, and removes the exact owned files through quarantine.\n\nArgs:\n params: One-use application of the exact retained Skill removal plan.\n\nReturns:\n Skill installation result with the honoured contract, or a typed request/negotiation refusal." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillInstallationResult.from_dict(await self._client.request("skills.applyUninstall", params_dict, **_timeout_kwargs(timeout))) + + async def discover(self, params: SkillsDiscoverRequest, *, timeout: float | None = None) -> ServerSkillList: + "Discovers skills across global and project sources.\n\nArgs:\n params: Optional project paths and additional skill directories to include in discovery.\n\nReturns:\n Skills discovered across global and project sources." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return ServerSkillList.from_dict(await self._client.request("skills.discover", params_dict, **_timeout_kwargs(timeout))) + + async def get_discovery_paths(self, params: SkillsGetDiscoveryPathsRequest, *, timeout: float | None = None) -> SkillDiscoveryPathList: + "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.\n\nArgs:\n params: Optional project paths to enumerate.\n\nReturns:\n Canonical locations where skills can be created so the runtime will recognize them." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SkillDiscoveryPathList.from_dict(await self._client.request("skills.getDiscoveryPaths", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerCatalogApi: def __init__(self, client: "JsonRpcClient"): @@ -47169,39 +54732,6 @@ async def disable(self, params: PluginsDisableRequest, *, timeout: float | None await self._client.request("plugins.disable", params_dict, **_timeout_kwargs(timeout)) -# Experimental: this API group is experimental and may change or be removed. -class ServerSkillsConfigApi: - def __init__(self, client: "JsonRpcClient"): - self._client = client - - async def set_disabled_skills(self, params: SkillsConfigSetDisabledSkillsRequest, *, timeout: float | None = None) -> None: - "Replaces the global list of disabled skills.\n\nArgs:\n params: Skill names to mark as disabled in global configuration, replacing any previous list." - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - await self._client.request("skills.config.setDisabledSkills", params_dict, **_timeout_kwargs(timeout)) - - async def set_skill_disabled(self, params: SkillsConfigSetSkillDisabledRequest, *, timeout: float | None = None) -> None: - "Atomically adds or removes one skill from the disabled list.\n\nArgs:\n params: Adds or removes a single skill from the global disabled list, leaving every other entry untouched." - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - await self._client.request("skills.config.setSkillDisabled", params_dict, **_timeout_kwargs(timeout)) - - -# Experimental: this API group is experimental and may change or be removed. -class ServerSkillsApi: - def __init__(self, client: "JsonRpcClient"): - self._client = client - self.config = ServerSkillsConfigApi(client) - - async def discover(self, params: SkillsDiscoverRequest, *, timeout: float | None = None) -> ServerSkillList: - "Discovers skills across global and project sources.\n\nArgs:\n params: Optional project paths and additional skill directories to include in discovery.\n\nReturns:\n Skills discovered across global and project sources." - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - return ServerSkillList.from_dict(await self._client.request("skills.discover", params_dict, **_timeout_kwargs(timeout))) - - async def get_discovery_paths(self, params: SkillsGetDiscoveryPathsRequest, *, timeout: float | None = None) -> SkillDiscoveryPathList: - "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.\n\nArgs:\n params: Optional project paths to enumerate.\n\nReturns:\n Canonical locations where skills can be created so the runtime will recognize them." - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - return SkillDiscoveryPathList.from_dict(await self._client.request("skills.getDiscoveryPaths", params_dict, **_timeout_kwargs(timeout))) - - # Experimental: this API group is experimental and may change or be removed. class ServerAgentsApi: def __init__(self, client: "JsonRpcClient"): @@ -47249,18 +54779,14 @@ class ServerUserSettingsApi: def __init__(self, client: "JsonRpcClient"): self._client = client - async def reload(self, *, timeout: float | None = None) -> None: - "Drops this runtime process's in-memory user settings cache so the next settings read observes disk." - await self._client.request("user.settings.reload", {}, **_timeout_kwargs(timeout)) - async def get(self, *, timeout: float | None = None) -> UserSettingsGetResult: - "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.\n\nReturns:\n 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 from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time.\n\nReturns:\n Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides." return UserSettingsGetResult.from_dict(await self._client.request("user.settings.get", {}, **_timeout_kwargs(timeout))) - async def set(self, params: UserSettingsSetRequest, *, timeout: float | None = None) -> UserSettingsSetResult: - "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.\n\nArgs:\n params: Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed.\n\nReturns:\n Outcome of writing user settings." + async def set(self, params: UserSettingsSetRequest, *, timeout: float | None = None) -> None: + "Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed.\n\nArgs:\n params: Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - return UserSettingsSetResult.from_dict(await self._client.request("user.settings.set", params_dict, **_timeout_kwargs(timeout))) + await self._client.request("user.settings.set", params_dict, **_timeout_kwargs(timeout)) # Experimental: this API group is experimental and may change or be removed. @@ -47276,13 +54802,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: @@ -47465,10 +55010,36 @@ async def spawn(self, params: AgentRegistrySpawnRequest, *, timeout: float | Non return _load_AgentRegistrySpawnResult(await self._client.request("agentRegistry.spawn", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class ServerConnectorsApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def get_capabilities(self, *, timeout: float | None = None) -> ConnectorDiscoveryCapabilities: + "Returns feature availability.\n\nReturns:\n Feature availability." + return ConnectorDiscoveryCapabilities.from_dict(await self._client.request("connectors.getCapabilities", {}, **_timeout_kwargs(timeout))) + + async def get_accounts(self, *, timeout: float | None = None) -> ConnectorDiscoveryAccountList: + "Returns eligible accounts.\n\nReturns:\n Eligible accounts." + return ConnectorDiscoveryAccountList.from_dict(await self._client.request("connectors.getAccounts", {}, **_timeout_kwargs(timeout))) + + async def list(self, params: ConnectorDiscoveryAccountRequest, *, timeout: float | None = None) -> ConnectorDiscoveryCatalogResult: + "Lists entries for the selected account.\n\nArgs:\n params: Selected account.\n\nReturns:\n Entries for the selected account." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return ConnectorDiscoveryCatalogResult.from_dict(await self._client.request("connectors.list", params_dict, **_timeout_kwargs(timeout))) + + async def refresh(self, params: ConnectorDiscoveryAccountRequest, *, timeout: float | None = None) -> ConnectorDiscoveryCatalogResult: + "Refreshes entries for the selected account.\n\nArgs:\n params: Selected account.\n\nReturns:\n Entries for the selected account." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return ConnectorDiscoveryCatalogResult.from_dict(await self._client.request("connectors.refresh", params_dict, **_timeout_kwargs(timeout))) + + 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) self.sandbox = ServerSandboxApi(client) @@ -47477,9 +55048,9 @@ def __init__(self, client: "JsonRpcClient"): self.secrets = ServerSecretsApi(client) self.mcp = ServerMcpApi(client) self.extensions = ServerExtensionsApi(client) + self.skills = ServerSkillsApi(client) self.catalog = ServerCatalogApi(client) self.plugins = ServerPluginsApi(client) - self.skills = ServerSkillsApi(client) self.agents = ServerAgentsApi(client) self.instructions = ServerInstructionsApi(client) self.commands = ServerCommandsApi(client) @@ -47490,6 +55061,7 @@ def __init__(self, client: "JsonRpcClient"): self.llm_inference = ServerLlmInferenceApi(client) self.sessions = ServerSessionsApi(client) self.agent_registry = ServerAgentRegistryApi(client) + self.connectors = ServerConnectorsApi(client) async def ping(self, params: PingRequest, *, timeout: float | None = None) -> PingResult: "Checks server responsiveness and returns protocol information.\n\nArgs:\n params: Optional message to echo back to the caller.\n\nReturns:\n Server liveness response, including the echoed message, current server timestamp, and protocol version.\n\n.. warning:: This API is experimental and may change or be removed in future versions." @@ -47501,6 +55073,137 @@ async def register_extension_launch_provider(self, *, timeout: float | None = No await self._client.request("registerExtensionLaunchProvider", {}, **_timeout_kwargs(timeout)) +# Experimental: this API group is experimental and may change or be removed. +class _InternalServerHostApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def _create_session(self, params: HostSessionCreateRequest, *, timeout: float | None = None) -> HostSessionCreateResult: + "Requests app-owned materialization over the owning SDK participant.\n\nArgs:\n params: One application-owned session handoff, requested by the supervised hosting participant.\n\nReturns:\n The resident session the application has materialized on its own connection.\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 HostSessionCreateResult.from_dict(await self._client.request("host.createSession", params_dict, **_timeout_kwargs(timeout))) + + async def _release_session(self, params: HostSessionReleaseRequest, *, timeout: float | None = None) -> HostEmptyResult: + "Releases app ownership retention after AHP detaches.\n\nArgs:\n params: Ends one participation, not the application's session lifetime.\n\nReturns:\n Empty acknowledgement for a completed host lifecycle operation.\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 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))) + + async def _ready(self, params: HostReadyRequest, *, timeout: float | None = None) -> HostEmptyResult: + "Reports a supervised hosting participant's bound AHP endpoint after its SDK handshake.\n\nArgs:\n params: Readiness reported by the supervised hosting participant on its own SDK connection.\n\nReturns:\n Empty acknowledgement for a completed host lifecycle operation.\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 HostEmptyResult.from_dict(await self._client.request("host.ready", params_dict, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class _InternalServerAgentsApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def _get_builtins(self, *, timeout: float | None = None) -> _AgentsGetBuiltinsResult: + "Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one.\n\nReturns:\n The agents this runtime ships, named so a consumer can tell them apart from authored ones.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + return _AgentsGetBuiltinsResult.from_dict(await self._client.request("agents.getBuiltins", {}, **_timeout_kwargs(timeout))) + + async def _get_available_builtins(self, params: _AgentsGetAvailableBuiltinsRequest, *, timeout: float | None = None) -> _AgentsGetAvailableBuiltinsResult: + "Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name.\n\nArgs:\n params: The feature flags to evaluate shipped agents against.\n\nReturns:\n The shipped agents available under the requested flags.\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 _AgentsGetAvailableBuiltinsResult.from_dict(await self._client.request("agents.getAvailableBuiltins", params_dict, **_timeout_kwargs(timeout))) + + async def _get_builtin_definition(self, params: _AgentsGetBuiltinDefinitionRequest, *, timeout: float | None = None) -> _AgentsGetBuiltinDefinitionResult: + "Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition.\n\nArgs:\n params: The shipped agent whose definition to load.\n\nReturns:\n One shipped agent's definition.\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 _AgentsGetBuiltinDefinitionResult.from_dict(await self._client.request("agents.getBuiltinDefinition", params_dict, **_timeout_kwargs(timeout))) + + async def _get_builtin_listing_definition(self, params: _AgentsGetBuiltinListingDefinitionRequest, *, timeout: float | None = None) -> _AgentsGetBuiltinListingDefinitionResult: + "Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`.\n\nArgs:\n params: The shipped agent whose listing entry to load.\n\nReturns:\n One shipped agent, projected for a listing.\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 _AgentsGetBuiltinListingDefinitionResult.from_dict(await self._client.request("agents.getBuiltinListingDefinition", params_dict, **_timeout_kwargs(timeout))) + + async def _custom_agent_initial_model_decision(self, params: _AgentsCustomAgentInitialModelDecisionParams, *, timeout: float | None = None) -> _AgentsCustomAgentInitialModelDecisionResult: + "Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's.\n\nArgs:\n params: The models a custom agent asks for, and the models actually available.\n\nReturns:\n The model to switch to, and the warning to show when the agent's preference could not be met.\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 _AgentsCustomAgentInitialModelDecisionResult.from_dict(await self._client.request("agents.customAgentInitialModelDecision", params_dict, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class _InternalServerGlobalStateApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def _load(self, *, timeout: float | None = None) -> _GlobalStateLoadResult: + "Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet.\n\nReturns:\n The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + return _GlobalStateLoadResult.from_dict(await self._client.request("globalState.load", {}, **_timeout_kwargs(timeout))) + + async def _load_for_config_dir(self, params: _GlobalStateLoadForConfigDirRequest, *, timeout: float | None = None) -> _GlobalStateLoadResult: + "Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it.\n\nArgs:\n params: Selects the configuration directory whose machine-wide state to read.\n\nReturns:\n The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape.\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 _GlobalStateLoadResult.from_dict(await self._client.request("globalState.loadForConfigDir", params_dict, **_timeout_kwargs(timeout))) + + async def _write_key(self, params: _GlobalStateWriteKeyRequest, *, timeout: float | None = None) -> None: + "Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks.\n\nArgs:\n params: A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method.\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} + await self._client.request("globalState.writeKey", params_dict, **_timeout_kwargs(timeout)) + + +# Experimental: this API group is experimental and may change or be removed. +class _InternalServerGitHubRepositoryApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def _at_path(self, params: _GitHubRepositoryAtPathRequest, *, timeout: float | None = None) -> _GitHubRepositoryAtPathResult: + "Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'.\n\nArgs:\n params: Working-tree path whose owning GitHub repository should be resolved.\n\nReturns:\n The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host.\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 _GitHubRepositoryAtPathResult.from_dict(await self._client.request("gitHubRepository.atPath", params_dict, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class _InternalServerGitHubOwnersApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def _next_request_id(self, *, timeout: float | None = None) -> GitHubOwnersRequestIDResult: + "Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations.\n\nReturns:\n A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + return GitHubOwnersRequestIDResult.from_dict(await self._client.request("gitHubOwners.nextRequestId", {}, **_timeout_kwargs(timeout))) + + async def _list(self, params: _GitHubOwnersListRequest, *, timeout: float | None = None) -> _GitHubOwnersListResult: + "Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`.\n\nArgs:\n params: Credential to list owners under, and the request id that makes the listing cancellable.\n\nReturns:\n Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render.\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 _GitHubOwnersListResult.from_dict(await self._client.request("gitHubOwners.list", params_dict, **_timeout_kwargs(timeout))) + + async def _cancel(self, params: _GitHubOwnersCancelRequest, *, timeout: float | None = None) -> _GitHubOwnersCancelResult: + "Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host.\n\nArgs:\n params: The owner listing to abandon.\n\nReturns:\n Whether the id named a running owner listing.\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 _GitHubOwnersCancelResult.from_dict(await self._client.request("gitHubOwners.cancel", params_dict, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class _InternalServerGitApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def _current_branch_remote(self, params: _GitCwdRequest, *, timeout: float | None = None) -> _GitCurrentBranchRemoteResult: + "Reads the remote that the branch checked out in a working tree tracks, as `branch..remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on.\n\nArgs:\n params: Working-tree path a git query applies to.\n\nReturns:\n The remote the checked-out branch tracks.\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 _GitCurrentBranchRemoteResult.from_dict(await self._client.request("git.currentBranchRemote", params_dict, **_timeout_kwargs(timeout))) + + async def _working_directory_context(self, params: _GitCwdRequest, *, timeout: float | None = None) -> SessionWorkingDirectoryContext: + "Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself.\n\nArgs:\n params: Working-tree path a git query applies to.\n\nReturns:\n Updated working directory and git context. Emitted as the new payload of `session.context_changed`.\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 SessionWorkingDirectoryContext.from_dict(await self._client.request("git.workingDirectoryContext", params_dict, **_timeout_kwargs(timeout))) + + async def _repos_from_remotes(self, params: _GitReposFromRemotesRequest, *, timeout: float | None = None) -> _GitReposFromRemotesResult: + "Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on.\n\nArgs:\n params: Git working tree whose GitHub remotes should be listed.\n\nReturns:\n The GitHub repositories a working tree's remotes point at.\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 _GitReposFromRemotesResult.from_dict(await self._client.request("git.reposFromRemotes", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class _InternalServerSessionsApi: def __init__(self, client: "JsonRpcClient"): @@ -47531,6 +55234,21 @@ async def _delete(self, params: SessionsDeleteRequest, *, timeout: float | None params_dict = {k: v for k, v in params.to_dict().items() if v is not None} await self._client.request("sessions.delete", params_dict, **_timeout_kwargs(timeout)) + async def _create_workspace(self, params: SessionsCreateWorkspaceRequest, *, timeout: float | None = None) -> SessionsCreateWorkspaceResult: + "Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer.\n\nArgs:\n params: Identity, state location and starting context for a workspace record.\n\nReturns:\n The workspace record that was written.\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 SessionsCreateWorkspaceResult.from_dict(await self._client.request("sessions.createWorkspace", params_dict, **_timeout_kwargs(timeout))) + + async def _load_workspace(self, params: SessionsLoadWorkspaceRequest, *, timeout: float | None = None) -> SessionsLoadWorkspaceResult: + "Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent.\n\nArgs:\n params: Where the session's state lives, as a root directory and the session ID under it.\n\nReturns:\n The workspace record on disk, omitted when the session has none.\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 SessionsLoadWorkspaceResult.from_dict(await self._client.request("sessions.loadWorkspace", params_dict, **_timeout_kwargs(timeout))) + + async def _update_workspace_fields(self, params: SessionsUpdateWorkspaceFieldsRequest, *, timeout: float | None = None) -> SessionsUpdateWorkspaceFieldsResult: + "Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`.\n\nArgs:\n params: Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced.\n\nReturns:\n The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays.\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 SessionsUpdateWorkspaceFieldsResult.from_dict(await self._client.request("sessions.updateWorkspaceFields", params_dict, **_timeout_kwargs(timeout))) + async def _get_board_entry_count(self, params: SessionsGetBoardEntryCountRequest, *, timeout: float | None = None) -> SessionsGetBoardEntryCountResult: "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.\n\nArgs:\n params: Session ID whose board entry count should be returned.\n\nReturns:\n Dynamic-context board entry count, when available.\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} @@ -47546,6 +55264,12 @@ 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.agents = _InternalServerAgentsApi(client) + self.global_state = _InternalServerGlobalStateApi(client) + self.git_hub_repository = _InternalServerGitHubRepositoryApi(client) + self.git_hub_owners = _InternalServerGitHubOwnersApi(client) + self.git = _InternalServerGitApi(client) self.sessions = _InternalServerSessionsApi(client) async def _connect(self, params: _ConnectRequest, *, timeout: float | None = None) -> _ConnectResult: @@ -47554,6 +55278,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): @@ -47570,6 +55337,12 @@ async def disable_for_session(self, params: SandboxDisableForSessionRequest, *, params_dict["sessionId"] = self._session_id return SandboxDisableForSessionResult.from_dict(await self._client.request("session.sandbox.disableForSession", params_dict, **_timeout_kwargs(timeout))) + async def grant_path_for_request(self, params: SandboxGrantPathForRequestRequest, *, timeout: float | None = None) -> SandboxGrantPathForRequestResult: + "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.\n\nArgs:\n params: Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt.\n\nReturns:\n Result of accepting a sandbox path grant." + 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 SandboxGrantPathForRequestResult.from_dict(await self._client.request("session.sandbox.grantPathForRequest", params_dict, **_timeout_kwargs(timeout))) + # Experimental: this API group is experimental and may change or be removed. class GitHubAuthApi: @@ -47589,144 +55362,108 @@ async def set_credentials(self, params: SessionSetCredentialsParams, *, timeout: # Experimental: this API group is experimental and may change or be removed. -class DebugApi: +class AccountsLoginApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - async def collect_logs(self, params: DebugCollectLogsRequest, *, timeout: float | None = None) -> DebugCollectLogsResult: - "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.\n\nArgs:\n params: Options for collecting a session debug bundle with configurable redaction.\n\nReturns:\n Result of collecting a session debug bundle." + async def begin(self, params: AuthLoginBeginRequest, *, timeout: float | None = None) -> AuthLoginBegun: + "Begin an interactive login flow for a provider kind (dispatch is kind-only) and return its opaque flow id and first step.\n\nArgs:\n params: Begin an interactive login flow for a provider kind. Dispatch is kind-only.\n\nReturns:\n A started login flow: its opaque id and first step." 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 DebugCollectLogsResult.from_dict(await self._client.request("session.debug.collectLogs", params_dict, **_timeout_kwargs(timeout))) - + return AuthLoginBegun.from_dict(await self._client.request("session.accounts.login.begin", params_dict, **_timeout_kwargs(timeout))) -# Experimental: this API group is experimental and may change or be removed. -class CanvasActionApi: - def __init__(self, client: "JsonRpcClient", session_id: str): - self._client = client - self._session_id = session_id + async def advance(self, params: AuthLoginAdvanceRequest, *, timeout: float | None = None) -> AuthLoginStep: + "Advance an in-flight login flow, optionally fulfilling an input-required step, and return the next step.\n\nArgs:\n params: Advance an in-flight login flow, optionally fulfilling an input-required step.\n\nReturns:\n 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)." + 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 AuthLoginStep.from_dict(await self._client.request("session.accounts.login.advance", params_dict, **_timeout_kwargs(timeout))) - async def invoke(self, params: CanvasActionInvokeRequest, *, timeout: float | None = None) -> Any: - "Invokes an action on an open canvas instance.\n\nArgs:\n params: Canvas action invocation parameters.\n\nReturns:\n Canvas action invocation result." + async def cancel(self, params: AuthLoginCancelRequest, *, timeout: float | None = None) -> None: + "Cancel an in-flight login flow and release its resources.\n\nArgs:\n params: Cancel an in-flight login flow." 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 await self._client.request("session.canvas.action.invoke", params_dict, **_timeout_kwargs(timeout)) + await self._client.request("session.accounts.login.cancel", params_dict, **_timeout_kwargs(timeout)) # Experimental: this API group is experimental and may change or be removed. -class CanvasApi: +class AccountsApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - self.action = CanvasActionApi(client, session_id) + self.login = AccountsLoginApi(client, session_id) - async def list(self, *, timeout: float | None = None) -> CanvasList: - "Lists canvases declared for the session.\n\nReturns:\n Declared canvases available in this session." - return CanvasList.from_dict(await self._client.request("session.canvas.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) - - async def list_open(self, *, timeout: float | None = None) -> CanvasListOpenResult: - "Lists currently open canvas instances for the live session.\n\nReturns:\n Live open-canvas snapshot." - return CanvasListOpenResult.from_dict(await self._client.request("session.canvas.listOpen", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def enumerate(self, params: AccountsEnumerateRequest, *, timeout: float | None = None) -> AuthEnumerateValue: + "Enumerate a typed accounts collection: the signed-in accounts, or the providers offered for interactive login.\n\nArgs:\n params: Enumerate request carrying the typed collection query.\n\nReturns:\n The enumerated collection, keyed by the same selector as the query." + 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 AuthEnumerateValue.from_dict(await self._client.request("session.accounts.enumerate", params_dict, **_timeout_kwargs(timeout))) - async def open(self, params: CanvasOpenRequest, *, timeout: float | None = None) -> OpenCanvasInstance: - "Opens or focuses a canvas instance.\n\nArgs:\n params: Canvas open parameters.\n\nReturns:\n Open canvas instance snapshot." + async def get(self, params: AccountsGetRequest, *, timeout: float | None = None) -> AuthReadValue: + "Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors.\n\nArgs:\n params: Read request carrying the typed datum query.\n\nReturns:\n The read result, keyed by the same selector as the query." 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 OpenCanvasInstance.from_dict(await self._client.request("session.canvas.open", params_dict, **_timeout_kwargs(timeout))) + return AuthReadValue.from_dict(await self._client.request("session.accounts.get", params_dict, **_timeout_kwargs(timeout))) - async def close(self, params: CanvasCloseRequest, *, timeout: float | None = None) -> None: - "Closes an open canvas instance.\n\nArgs:\n params: Canvas close parameters." + async def set(self, params: AccountsSetRequest, *, timeout: float | None = None) -> AuthWriteResult: + "Apply one non-interactive accounts mutation: switch the active account, log an account out, or set credentials from a token.\n\nArgs:\n params: Mutation request carrying the typed write command.\n\nReturns:\n Result of a non-interactive accounts mutation." 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.canvas.close", params_dict, **_timeout_kwargs(timeout)) + return AuthWriteResult.from_dict(await self._client.request("session.accounts.set", params_dict, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. -class FactoryJournalApi: +class DebugApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - async def get(self, params: FactoryJournalGetRequest, *, timeout: float | None = None) -> FactoryJournalGetResult: - "Reads a memoized factory journal entry.\n\nArgs:\n params: Parameters for reading a factory journal entry.\n\nReturns:\n Result of reading a factory journal entry." - 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 FactoryJournalGetResult.from_dict(await self._client.request("session.factory.journal.get", params_dict, **_timeout_kwargs(timeout))) - - async def put(self, params: FactoryJournalPutRequest, *, timeout: float | None = None) -> FactoryACKResult: - "Stores a memoized factory journal entry.\n\nArgs:\n params: Parameters for storing a factory journal entry.\n\nReturns:\n Acknowledgement that a factory request was accepted." + async def collect_logs(self, params: DebugCollectLogsRequest, *, timeout: float | None = None) -> DebugCollectLogsResult: + "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.\n\nArgs:\n params: Options for collecting a session debug bundle with configurable redaction.\n\nReturns:\n Result of collecting a session debug bundle." 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 FactoryACKResult.from_dict(await self._client.request("session.factory.journal.put", params_dict, **_timeout_kwargs(timeout))) + return DebugCollectLogsResult.from_dict(await self._client.request("session.debug.collectLogs", params_dict, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. -class FactoryApi: +class CanvasActionApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - self.journal = FactoryJournalApi(client, session_id) - - async def run(self, params: FactoryRunRequest, *, timeout: float | None = None) -> FactoryRunResult: - "Runs a registered factory by name at the top level.\n\nArgs:\n params: Parameters for invoking a registered factory.\n\nReturns:\n Complete current or terminal factory run envelope." - 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 FactoryRunResult.from_dict(await self._client.request("session.factory.run", params_dict, **_timeout_kwargs(timeout))) - - async def resume(self, params: FactoryResumeRequest, *, timeout: float | None = None) -> FactoryResumeResult: - "Resumes a factory run using its persisted name, arguments, journal, and accounting.\n\nArgs:\n params: Parameters for resuming a factory run from its persisted identity.\n\nReturns:\n Resolved persisted factory identity and resumed run envelope." - 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 FactoryResumeResult.from_dict(await self._client.request("session.factory.resume", params_dict, **_timeout_kwargs(timeout))) - - async def get_run(self, params: FactoryGetRunRequest, *, timeout: float | None = None) -> FactoryRunResult: - "Gets the current or settled envelope for a factory run.\n\nArgs:\n params: Parameters for retrieving a factory run.\n\nReturns:\n Complete current or terminal factory run envelope." - 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 FactoryRunResult.from_dict(await self._client.request("session.factory.getRun", params_dict, **_timeout_kwargs(timeout))) - async def list_runs(self, params: FactoryListRunsRequest, *, timeout: float | None = None) -> FactoryListRunsResult: - "Lists durable factory runs for this session in creation order.\n\nArgs:\n params: Parameters for paging factory runs.\n\nReturns:\n A page of factory runs in durable creation order." + async def invoke(self, params: CanvasActionInvokeRequest, *, timeout: float | None = None) -> Any: + "Invokes an action on an open canvas instance.\n\nArgs:\n params: Canvas action invocation parameters.\n\nReturns:\n Canvas action invocation result." 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 FactoryListRunsResult.from_dict(await self._client.request("session.factory.listRuns", params_dict, **_timeout_kwargs(timeout))) + return await self._client.request("session.canvas.action.invoke", params_dict, **_timeout_kwargs(timeout)) - async def get_run_detail(self, params: FactoryGetRunRequest, *, timeout: float | None = None) -> FactoryRunDetail: - "Gets durable and live observability detail for one factory run.\n\nArgs:\n params: Parameters for retrieving a factory run.\n\nReturns:\n Full factory run observability detail." - 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 FactoryRunDetail.from_dict(await self._client.request("session.factory.getRunDetail", params_dict, **_timeout_kwargs(timeout))) - async def get_run_progress(self, params: FactoryGetRunProgressRequest, *, timeout: float | None = None) -> FactoryProgressPage: - "Pages durable progress for one factory run.\n\nArgs:\n params: Parameters for paging factory progress.\n\nReturns:\n A bidirectional page of factory progress." - 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 FactoryProgressPage.from_dict(await self._client.request("session.factory.getRunProgress", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class CanvasApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + self.action = CanvasActionApi(client, session_id) - async def cancel(self, params: FactoryCancelRequest, *, timeout: float | None = None) -> FactoryRunResult: - "Requests cancellation of a factory run and returns its run envelope.\n\nArgs:\n params: Parameters for cancelling a factory run.\n\nReturns:\n Complete current or terminal factory run envelope." - 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 FactoryRunResult.from_dict(await self._client.request("session.factory.cancel", params_dict, **_timeout_kwargs(timeout))) + async def list(self, *, timeout: float | None = None) -> CanvasList: + "Lists canvases declared for the session.\n\nReturns:\n Declared canvases available in this session." + return CanvasList.from_dict(await self._client.request("session.canvas.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) - async def pause(self, params: FactoryPauseRequest, *, timeout: float | None = None) -> FactoryRunResult: - "Pauses a running factory and returns its settled run envelope.\n\nArgs:\n params: Parameters for pausing a running factory.\n\nReturns:\n Complete current or terminal factory run envelope." - 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 FactoryRunResult.from_dict(await self._client.request("session.factory.pause", params_dict, **_timeout_kwargs(timeout))) + async def list_open(self, *, timeout: float | None = None) -> CanvasListOpenResult: + "Lists currently open canvas instances for the live session.\n\nReturns:\n Live open-canvas snapshot." + return CanvasListOpenResult.from_dict(await self._client.request("session.canvas.listOpen", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) - async def log(self, params: FactoryLogRequest, *, timeout: float | None = None) -> FactoryACKResult: - "Records a batch of ordered factory progress lines.\n\nArgs:\n params: Parameters for recording factory progress.\n\nReturns:\n Acknowledgement that a factory request was accepted." + async def open(self, params: CanvasOpenRequest, *, timeout: float | None = None) -> OpenCanvasInstance: + "Opens or focuses a canvas instance.\n\nArgs:\n params: Canvas open parameters.\n\nReturns:\n Open canvas instance snapshot." 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 FactoryACKResult.from_dict(await self._client.request("session.factory.log", params_dict, **_timeout_kwargs(timeout))) + return OpenCanvasInstance.from_dict(await self._client.request("session.canvas.open", params_dict, **_timeout_kwargs(timeout))) - async def agent(self, params: FactoryAgentRequest, *, timeout: float | None = None) -> FactoryAgentResult: - "Runs one factory-scoped subagent and returns its result.\n\nArgs:\n params: Parameters for one factory-scoped subagent call.\n\nReturns:\n Result of one factory-scoped subagent call." + async def close(self, params: CanvasCloseRequest, *, timeout: float | None = None) -> None: + "Closes an open canvas instance.\n\nArgs:\n params: Canvas close parameters." 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 FactoryAgentResult.from_dict(await self._client.request("session.factory.agent", params_dict, **_timeout_kwargs(timeout))) + await self._client.request("session.canvas.close", params_dict, **_timeout_kwargs(timeout)) # Experimental: this API group is experimental and may change or be removed. @@ -48079,9 +55816,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. @@ -48090,9 +55827,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. @@ -48273,18 +56010,36 @@ async def authentication_state_changed(self, params: MCPOauthAuthenticationState params_dict["sessionId"] = self._session_id await self._client.request("session.mcp.oauth.authenticationStateChanged", params_dict, **_timeout_kwargs(timeout)) + async def prepare_login(self, params: SessionMCPOauthPrepareLoginRequest, *, timeout: float | None = None) -> SessionMCPOauthPrepareLoginResult: + "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.\n\nArgs:\n params: Effect-free preparation bound to the existing local session, requester and installation, with frozen options.\n\nReturns:\n An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work." + 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 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.\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} params_dict["sessionId"] = self._session_id return MCPOauthProbeResult.from_dict(await self._client.request("session.mcp.oauth.probe", params_dict, **_timeout_kwargs(timeout))) + async def cancel_login(self, params: SessionMCPOauthCancelLoginRequest, *, timeout: float | None = None) -> SessionMCPOauthCancelLoginResult: + "Cancels the exact owned OAuth login issued to this original session requester, without clearing shared credentials.\n\nArgs:\n params: Targets only the original prepared/applying owned login on this exact session requester.\n\nReturns:\n Honest terminal cancellation result; persistence or recovery failures remain RPC errors." + 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 SessionMCPOauthCancelLoginResult.from_dict(await self._client.request("session.mcp.oauth.cancelLogin", params_dict, **_timeout_kwargs(timeout))) + async def respond(self, params: MCPOauthRespondRequest, *, timeout: float | None = None) -> MCPOauthRespondResult: "Responds to a pending MCP OAuth authorization request by its request id.\n\nArgs:\n params: Pending MCP OAuth request id to respond to.\n\nReturns:\n Indicates whether the pending MCP OAuth response was accepted." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -48371,6 +56126,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): @@ -48380,11 +56154,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} @@ -48487,6 +56266,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))) @@ -48541,7 +56325,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))) @@ -48652,6 +56436,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: @@ -49130,7 +56920,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))) @@ -49395,11 +57185,12 @@ 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) self.debug = DebugApi(client, session_id) self.canvas = CanvasApi(client, session_id) - self.factory = FactoryApi(client, session_id) self.workflow = WorkflowApi(client, session_id) self.model = ModelApi(client, session_id) self.mode = ModeApi(client, session_id) @@ -49494,7 +57285,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." @@ -49503,7 +57294,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." @@ -49559,31 +57350,6 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.provider = _InternalCanvasProviderApi(client, session_id) -# Experimental: this API group is experimental and may change or be removed. -class _InternalFactoryApi: - def __init__(self, client: "JsonRpcClient", session_id: str): - self._client = client - self._session_id = session_id - - async def _run_from_tool(self, params: _FactoryToolRunRequest, *, timeout: float | None = None) -> FactoryRunResult: - "Internal tool-originated factory invocation.\n\nArgs:\n params: Internal parameters for invoking a registered factory from a tool.\n\nReturns:\n Complete current or terminal factory run envelope.\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 FactoryRunResult.from_dict(await self._client.request("session.factory.runFromTool", params_dict, **_timeout_kwargs(timeout))) - - async def _resume_from_tool(self, params: _FactoryToolResumeRequest, *, timeout: float | None = None) -> FactoryResumeResult: - "Internal tool-originated factory resume.\n\nArgs:\n params: Internal parameters for resuming a factory run from a tool.\n\nReturns:\n Resolved persisted factory identity and resumed run envelope.\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 FactoryResumeResult.from_dict(await self._client.request("session.factory.resumeFromTool", params_dict, **_timeout_kwargs(timeout))) - - async def _pause_at_checkpoint(self, params: FactoryPauseCheckpointRequest, *, timeout: float | None = None) -> SessionFactoryPauseAtCheckpointResult: - "Atomically pauses an owned factory attempt at a durable checkpoint.\n\nArgs:\n params: Parameters for an owned durable pause checkpoint.\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 SessionFactoryPauseAtCheckpointResult.from_dict(await self._client.request("session.factory.pauseAtCheckpoint", params_dict, **_timeout_kwargs(timeout))) - - # Experimental: this API group is experimental and may change or be removed. class _InternalWorkflowApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -49628,6 +57394,12 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + async def _set_connected_ide_info(self, params: SessionMCPSetConnectedIDEInfoParams, *, timeout: float | None = None) -> None: + "Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project.\n\nArgs:\n params: Records which IDE the host is connected to, or clears it.\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 + await self._client.request("session.mcp.setConnectedIdeInfo", params_dict, **_timeout_kwargs(timeout)) + async def _reload_with_config(self, params: MCPReloadWithConfigRequest, *, timeout: float | None = None) -> MCPStartServersResult: "Reloads MCP server connections for the session with an explicit host-provided configuration.\n\nArgs:\n params: Opaque MCP reload configuration.\n\nReturns:\n MCP server startup filtering result.\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} @@ -49683,6 +57455,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): @@ -49799,12 +57596,12 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id self.git_hub_auth = _InternalGitHubAuthApi(client, session_id) self.canvas = _InternalCanvasApi(client, session_id) - self.factory = _InternalFactoryApi(client, session_id) self.workflow = _InternalWorkflowApi(client, session_id) self.model = _InternalModelApi(client, session_id) 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) @@ -49822,15 +57619,6 @@ async def get_token(self, params: ProviderTokenAcquireRequest) -> ProviderTokenA "Asks the SDK client to get a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Session-scoped: the runtime calls it back on the connection that most recently supplied that provider's config for the session (the creating connection, or a resuming connection if the session was resumed — distinct providers may be owned by different connections), passing the provider name, and uses the returned token as the Authorization header for the outbound model request. The runtime does no caching — it calls this once per outbound request; the SDK consumer owns token acquisition, caching, and refresh.\n\nArgs:\n params: 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.\n\nReturns:\n A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh." pass -# Experimental: this API group is experimental and may change or be removed. -class FactoryHandler(Protocol): - async def execute(self, params: FactoryExecuteRequest) -> FactoryExecuteResult: - "Asks the owning extension connection to execute a registered factory closure.\n\nArgs:\n params: Parameters sent to the owning extension to execute a factory closure.\n\nReturns:\n Result returned by an extension factory closure." - pass - async def abort(self, params: FactoryAbortRequest) -> FactoryACKResult: - "Asks the owning extension connection to abort a running factory cooperatively.\n\nArgs:\n params: Parameters for cooperatively aborting a factory body.\n\nReturns:\n Acknowledgement that a factory request was accepted." - pass - # Experimental: this API group is experimental and may change or be removed. class WorkflowHandler(Protocol): async def execute(self, params: WorkflowExecuteRequest) -> WorkflowExecuteResult: @@ -49851,9 +57639,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 @@ -49903,7 +57697,6 @@ async def invoke(self, params: CanvasProviderInvokeActionRequest) -> Any: @dataclass class ClientSessionApiHandlers: provider_token: ProviderTokenHandler | None = None - factory: FactoryHandler | None = None workflow: WorkflowHandler | None = None tasks: TasksHandler | None = None session_fs: SessionFsHandler | None = None @@ -49921,20 +57714,6 @@ async def handle_provider_token_get_token(params: dict) -> dict | None: result = await handler.get_token(request) return result.to_dict() client.set_request_handler("providerToken.getToken", handle_provider_token_get_token) - async def handle_factory_execute(params: dict) -> dict | None: - request = FactoryExecuteRequest.from_dict(params) - handler = get_handlers(request.session_id).factory - if handler is None: raise RuntimeError(f"No factory handler registered for session: {request.session_id}") - result = await handler.execute(request) - return result.to_dict() - client.set_request_handler("factory.execute", handle_factory_execute) - async def handle_factory_abort(params: dict) -> dict | None: - request = FactoryAbortRequest.from_dict(params) - handler = get_handlers(request.session_id).factory - if handler is None: raise RuntimeError(f"No factory handler registered for session: {request.session_id}") - result = await handler.abort(request) - return result.to_dict() - client.set_request_handler("factory.abort", handle_factory_abort) async def handle_workflow_execute(params: dict) -> dict | None: request = WorkflowExecuteRequest.from_dict(params) handler = get_handlers(request.session_id).workflow @@ -49963,6 +57742,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 @@ -49970,6 +57756,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 @@ -50069,6 +57862,24 @@ async def handle_canvas_action_invoke(params: dict) -> dict | None: return result.value if hasattr(result, 'value') else result client.set_request_handler("canvas.action.invoke", handle_canvas_action_invoke) +# Experimental: this API group is experimental and may change or be removed. +class HostHandler(Protocol): + async def register_session(self, params: HostRegisterSessionRequest) -> HostPublishSessionResult: + "Registers an existing resident session on the supervised listener without durable adoption.\n\nArgs:\n params: Listener-scoped registration, not a copy or durable adoption of a session.\n\nReturns:\n The existing runtime identity and its resource on the listener." + pass + async def materialize_session(self, params: HostSessionCreateCallback) -> HostSessionCreateResult: + "Materializes an AHP session in the owning application.\n\nArgs:\n params: Application callback routed over its existing SDK connection.\n\nReturns:\n The resident session the application has materialized on its own connection." + pass + async def session_released(self, params: HostSessionReleasedNotification) -> None: + "Ends application object retention for an AHP participation.\n\nArgs:\n params: Releases the original application session object retained for one handoff." + pass + async def shutdown(self, params: HostEmptyResult) -> HostEmptyResult: + "Requests graceful shutdown of a supervised AHP listener and its clients.\n\nArgs:\n params: Empty acknowledgement for a completed host lifecycle operation.\n\nReturns:\n Empty acknowledgement for a completed host lifecycle operation." + pass + async def exited(self, params: HostExitedNotification) -> None: + "Reports termination of a connection-owned host listener.\n\nArgs:\n params: Reports a supervised listener's hosting-task termination and cleanup outcome." + pass + # Experimental: this API group is experimental and may change or be removed. class HooksHandler(Protocol): async def invoke(self, params: _HookInvokeRequest) -> _HookInvokeResponse: @@ -50102,13 +57913,21 @@ async def get_token(self, params: GitHubTokenAcquireRequest) -> GitHubTokenAcqui "Asks the SDK client to mint a GitHub access token for a session whose configuration supplied a GitHub token provider. The runtime acquires the initial token during bootstrap and refreshes it during expiry preflight when one hour or less remains.\n\nArgs:\n params: Asks the SDK client to acquire a GitHub access token from an opaque callback registration.\n\nReturns:\n SDK host response to a GitHub credential request." pass +# Experimental: this API group is experimental and may change or be removed. +class InstallationsHandler(Protocol): + async def confirm(self, params: InstallationConfirmationRequest) -> InstallationConfirmationResponse: + "Requests a fresh explicit human decision for one sealed installation operation on its original connection. Present the complete typed review, return the original challenge and fingerprint, and never infer approval. The expiresAt deadline, connection closure or standard JSON-RPC $/cancelRequest retires the request; late replies grant no authority.\n\nArgs:\n params: One connection-owned, expiring request for a trusted host's explicit user decision.\n\nReturns:\n A response is meaningful only on the connection and request that issued its challenge." + pass + @dataclass class ClientGlobalApiHandlers: + host: HostHandler | None = None hooks: HooksHandler | None = None extension_launch_provider: ExtensionLaunchProviderHandler | None = None llm_inference: LlmInferenceHandler | None = None git_hub_telemetry: GitHubTelemetryHandler | None = None git_hub_token: GitHubTokenHandler | None = None + installations: InstallationsHandler | None = None def register_client_global_api_handlers( client: "JsonRpcClient", @@ -50120,6 +57939,41 @@ def register_client_global_api_handlers( session_id dispatch key; a single set of handlers serves the entire connection. """ + async def handle_host_register_session(params: dict) -> dict | None: + request = HostRegisterSessionRequest.from_dict(params) + handler = handlers.host + if handler is None: raise RuntimeError("No host client-global handler registered") + result = await handler.register_session(request) + return result.to_dict() + client.set_request_handler("host.registerSession", handle_host_register_session) + async def handle_host_materialize_session(params: dict) -> dict | None: + request = HostSessionCreateCallback.from_dict(params) + handler = handlers.host + if handler is None: raise RuntimeError("No host client-global handler registered") + result = await handler.materialize_session(request) + return result.to_dict() + client.set_request_handler("host.materializeSession", handle_host_materialize_session) + async def handle_host_session_released(params: dict) -> None: + request = HostSessionReleasedNotification.from_dict(params) + handler = handlers.host + if handler is None: return None + await handler.session_released(request) + return None + client.set_notification_method_handler("host.sessionReleased", handle_host_session_released) + async def handle_host_shutdown(params: dict) -> dict | None: + request = HostEmptyResult.from_dict(params) + handler = handlers.host + if handler is None: raise RuntimeError("No host client-global handler registered") + result = await handler.shutdown(request) + return result.to_dict() + client.set_request_handler("host.shutdown", handle_host_shutdown) + async def handle_host_exited(params: dict) -> None: + request = HostExitedNotification.from_dict(params) + handler = handlers.host + if handler is None: return None + await handler.exited(request) + return None + client.set_notification_method_handler("host.exited", handle_host_exited) async def handle_hooks_invoke(params: dict) -> dict | None: request = _HookInvokeRequest.from_dict(params) handler = handlers.hooks @@ -50162,6 +58016,18 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: result = await handler.get_token(request) return result.value if hasattr(result, 'value') else result client.set_request_handler("gitHubToken.getToken", handle_git_hub_token_get_token) + async def handle_installations_confirm(params: dict) -> dict | None: + request = InstallationConfirmationRequest.from_dict(params) + handler = handlers.installations + if handler is None: raise RuntimeError("No installations client-global handler registered") + result = await handler.confirm(request) + return result.to_dict() + client.set_request_handler("installations.confirm", handle_installations_confirm) + +# Backward-compatible public aliases retained across typed-review generation. +CatalogCandidateInstallability = CatalogInstallability +CatalogMCPServerInstallabilityEnum = CatalogMCPServerInstallability +CatalogMcpServerInstallability = CatalogMCPServerInstallability __all__ = [ "APIKeyAuthInfo", @@ -50170,15 +58036,24 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "AbortResult", "AcceptedEnqueueCommandResult", "AccountAllUsers", + "AccountAuthInfo", + "AccountAuthInfoType", "AccountGetAllUsersResult", "AccountGetCurrentAuthResult", "AccountGetQuotaRequest", "AccountGetQuotaResult", + "AccountKind", "AccountLoginRequest", "AccountLoginResult", "AccountLogoutRequest", "AccountLogoutResult", "AccountQuotaSnapshot", + "AccountStatus", + "AccountsApi", + "AccountsEnumerateRequest", + "AccountsGetRequest", + "AccountsLoginApi", + "AccountsSetRequest", "Action", "AdaptiveThinkingSupport", "AdditionalContentExclusionPolicyScope", @@ -50219,11 +58094,31 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "AgentsDiscoverRequest", "AgentsGetDiscoveryPathsRequest", "ApprovalKind", + "AuthEnumerateQuery", + "AuthEnumerateQueryKind", + "AuthEnumerateValue", "AuthIdentity", + "AuthIdentityMetadata", "AuthInfo", "AuthInfoType", + "AuthLoginAccount", + "AuthLoginAdvanceRequest", + "AuthLoginBeginRequest", + "AuthLoginBegun", + "AuthLoginCancelRequest", + "AuthLoginResultDto", + "AuthLoginResultStatus", + "AuthLoginStep", + "AuthLoginStepKind", + "AuthReadQuery", + "AuthReadQueryKind", + "AuthReadValue", + "AuthStatusDto", "AuthValidationError", "AuthValidationErrors", + "AuthWrite", + "AuthWriteKind", + "AuthWriteResult", "AutopilotObjectiveApi", "AutopilotObjectiveCreditLimit", "AutopilotObjectiveGetStateResult", @@ -50294,12 +58189,14 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "CatalogHandleRejectedErrorKind", "CatalogHandleRejectionReason", "CatalogHandleType", + "CatalogInstallability", "CatalogInvalidRequestError", "CatalogInvalidRequestErrorKind", "CatalogInvalidRequestField", "CatalogMCPServerCandidate", "CatalogMCPServerCandidateKind", "CatalogMCPServerCandidateProvenance", + "CatalogMCPServerInstallability", "CatalogMCPServerInstallabilityEnum", "CatalogMalformedCardError", "CatalogMalformedCardErrorKind", @@ -50425,9 +58322,19 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ConnectorConnectResultKind", "ConnectorContinueRequest", "ConnectorDisconnectResult", + "ConnectorDiscoveryAccount", + "ConnectorDiscoveryAccountList", + "ConnectorDiscoveryAccountRequest", + "ConnectorDiscoveryAuthInfo", + "ConnectorDiscoveryAvailability", + "ConnectorDiscoveryCapabilities", + "ConnectorDiscoveryCatalogEntry", + "ConnectorDiscoveryCatalogResult", "ConnectorMCPStatus", "ConnectorReconcileRequest", "ConnectorRuntimeStatus", + "ConnectorSessionAccount", + "ConnectorSessionAccountResult", "ConnectorStatus", "ConnectorsApi", "ContentExclusionApi", @@ -50447,7 +58354,11 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "CurrentModel", "CurrentToolMetadata", "CursorStatus", + "CustomizationReloadOutcome", + "CustomizationReloadStatus", + "CustomizationReloadSubsystem", "CustomizationsApi", + "CustomizationsReloadResult", "DebugApi", "DebugCollectLogsCollectedEntry", "DebugCollectLogsDestination", @@ -50482,13 +58393,22 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "DiscoveredHook", "DiscoveredMCPServer", "DiscoveredMCPServerType", - "DurableOperation", + "DiscoveredModel", + "DiscoveredModelList", "EnqueueCommandParams", "EnqueueCommandResult", "EnterpriseList", "Entry", "EnvAuthInfo", "EnvAuthInfoType", + "EnvironmentCapabilities", + "EnvironmentKind", + "EnvironmentsDeleteRequest", + "EnvironmentsDeleteResult", + "EnvironmentsGetRequest", + "EnvironmentsGetResult", + "EnvironmentsListRequest", + "EnvironmentsListResult", "EventLogApi", "EventLogReadRequest", "EventLogReleaseInterestResult", @@ -50535,53 +58455,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ExternalToolTextResultForLlmContentTerminalType", "ExternalToolTextResultForLlmContentText", "ExternalToolTextResultForLlmContentType", - "FactoryACKResult", - "FactoryAbortRequest", - "FactoryAgentOptions", - "FactoryAgentRequest", - "FactoryAgentResult", - "FactoryAgentSummary", - "FactoryApi", - "FactoryCancelRequest", - "FactoryCurrentPhase", - "FactoryDeclaredLimits", - "FactoryDurableOperation", - "FactoryExecuteRequest", - "FactoryExecuteResult", - "FactoryGetRunProgressRequest", - "FactoryGetRunRequest", - "FactoryHandler", - "FactoryJournalApi", - "FactoryJournalGetRequest", - "FactoryJournalGetResult", - "FactoryJournalPutRequest", - "FactoryListRunsRequest", - "FactoryListRunsResult", - "FactoryLogLine", - "FactoryLogLineKind", - "FactoryLogRequest", - "FactoryPauseCheckpointAction", - "FactoryPauseCheckpointRequest", - "FactoryPauseCheckpointResult", - "FactoryPauseInfo", - "FactoryPauseRequest", - "FactoryPhaseObservation", - "FactoryPhaseStatus", - "FactoryProgressLine", - "FactoryProgressPage", - "FactoryResumeRequest", - "FactoryResumeResult", - "FactoryRunConsumed", - "FactoryRunDetail", - "FactoryRunFailure", - "FactoryRunFailureKind", - "FactoryRunFailureType", - "FactoryRunLimits", - "FactoryRunRequest", - "FactoryRunResult", - "FactoryRunStatus", - "FactoryRunSummary", - "FactoryRunTerminal", "FilterMapping", "FleetApi", "FleetStartRequest", @@ -50593,6 +58466,9 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "GhCLIAuthInfo", "GhCLIAuthInfoType", "GitHubAuthApi", + "GitHubEnvironment", + "GitHubOwnersRequestIDResult", + "GitHubRepositoryIdentity", "GitHubTelemetryClientInfo", "GitHubTelemetryEvent", "GitHubTelemetryHandler", @@ -50636,8 +58512,35 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "HooksDiscoverResult", "HooksHandler", "Host", + "HostConfiguration", + "HostDisposeRequest", + "HostEmptyResult", + "HostEnvironmentCredentials", + "HostExitReason", + "HostExitedNotification", + "HostGitHubEnvironmentOptions", + "HostHandler", + "HostLocalServerConfiguration", + "HostLocalServerOptions", + "HostPublishSessionRequest", + "HostPublishSessionResult", + "HostReadyRequest", + "HostRegisterSessionRequest", + "HostSessionCreateCallback", + "HostSessionCreateRequest", + "HostSessionCreateResult", + "HostSessionReleaseRequest", + "HostSessionReleasedNotification", + "HostStartRequest", + "HostStartResult", "HostType", "InstallMethod", + "InstallationCatalogueIdentity", + "InstallationConfirmationRequest", + "InstallationConfirmationResponse", + "InstallationDecision", + "InstallationReview", + "InstallationsHandler", "InstalledPlugin", "InstalledPluginInfo", "InstalledPluginSource", @@ -50659,6 +58562,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "InstructionsGetSourcesResult", "InterruptMainTurnRequest", "InterruptMainTurnResult", + "ItemElement", "JSONSchemaResponseFormat", "KindEnum", "LimitPredictionApi", @@ -50676,12 +58580,14 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "LlmInferenceHeaders", "LlmInferenceSetProviderResult", "LocalSessionMetadataValue", - "LogLineKind", "LogRequest", "LogResult", + "LoginProviderKind", "LspApi", "LspInitializeRequest", "MCPAllowedServer", + "MCPApplyInstallRequest", + "MCPApplyUninstallRequest", "MCPAppsCallToolRequest", "MCPAppsDiagnoseCapability", "MCPAppsDiagnoseRequest", @@ -50708,6 +58614,9 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "MCPConfigUpdateRequest", "MCPConfigureGitHubRequest", "MCPConfigureGitHubResult", + "MCPConfiguredServer", + "MCPConfiguredServerList", + "MCPConfiguredServerState", "MCPDiagnosticDetails", "MCPDiagnosticDirection", "MCPDiagnosticKind", @@ -50727,22 +58636,47 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "MCPHeadersHandlePendingHeadersRefreshRequestResult", "MCPHostState", "MCPInstallPlan", + "MCPInstallationFailureReason", + "MCPInstallationInput", + "MCPInstallationManagementOutcome", + "MCPInstallationManagementOutcomeKind", + "MCPInstallationManagementResult", + "MCPInstallationManagementResultKind", + "MCPInstallationOperationRequest", + "MCPInstallationOperationStatus", + "MCPInstallationOutcome", + "MCPInstallationOutcomeKind", + "MCPInstallationRemoteConfiguration", + "MCPInstallationResult", + "MCPInstallationReview", + "MCPInstallationReviewAction", + "MCPInstallationSecret", + "MCPInstallationSecretStorage", + "MCPInstallationState", + "MCPInstallationSummary", + "MCPInstallationsRequest", "MCPIsServerRunningRequest", "MCPIsServerRunningResult", "MCPListToolsRequest", "MCPListToolsResult", "MCPOauthAuthenticationStateChangedRequest", + "MCPOauthCancelLoginRequest", + "MCPOauthCancelLoginResult", + "MCPOauthCompleteRequest", "MCPOauthHandlePendingRequest", "MCPOauthHandlePendingResult", "MCPOauthLoginRequest", "MCPOauthLoginResult", "MCPOauthPendingRequestResponse", + "MCPOauthPrepareLoginRequest", + "MCPOauthPrepareLoginResult", "MCPOauthProbeNeedsAuthReason", "MCPOauthProbeRequest", "MCPOauthProbeResult", "MCPOauthProbeResultStatus", "MCPOauthRespondRequest", "MCPOauthRespondResult", + "MCPOwnedOauthLoginStatus", "MCPPlan", "MCPPlanConfigurationChange", "MCPPlanConfigurationOperation", @@ -50778,10 +58712,23 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "MCPPlanScope", "MCPPlanSecretPlaceholder", "MCPPlanTarget", + "MCPPlanTargetClass", "MCPPlanTransportChoice", "MCPPlanTransportChoicePackage", "MCPPlanTransportChoiceRemote", + "MCPPlanUninstallRequest", "MCPPlanValueCategory", + "MCPPrepareInstallRequest", + "MCPPreparedInstall", + "MCPPrompt", + "MCPPromptArgument", + "MCPPromptIcon", + "MCPPromptMessage", + "MCPPromptRole", + "MCPPromptsGetRequest", + "MCPPromptsGetResult", + "MCPPromptsListRequest", + "MCPPromptsListResult", "MCPRegisterExternalClientRequest", "MCPReloadConfig", "MCPReloadWithConfigRequest", @@ -50823,6 +58770,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "MCPServerFailureInfo", "MCPServerList", "MCPServerNeedsAuthInfo", + "MCPServerOwnership", "MCPSetEnvValueModeDetails", "MCPSetEnvValueModeParams", "MCPSetEnvValueModeResult", @@ -50836,11 +58784,27 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "MCPToolUI", "MCPToolUIVisibility", "MCPTools", + "MCPUninstallPlan", "MCPUnregisterExternalClientRequest", "ManagedMCPServerConfig", + "ManagedSettingMeta", "ManagedSettingsApi", + "ManagedSettingsChannel", + "ManagedSettingsComposeLayer", + "ManagedSettingsComposeRequest", + "ManagedSettingsComposeResult", + "ManagedSettingsDiagnostic", + "ManagedSettingsDiagnosticSeverity", + "ManagedSettingsLayer", + "ManagedSettingsMeta", "ManagedSettingsReadResult", + "ManagedSettingsResolveRequest", + "ManagedSettingsResolveResult", "ManagedSettingsResolvedData", + "ManagedSettingsSchemaResult", + "ManagedSettingsValidateRequest", + "ManagedSettingsValidateResult", + "ManagedSettingsValues", "MarketplaceAddResult", "MarketplaceBrowseResult", "MarketplaceInfo", @@ -50871,6 +58835,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "McpPlanScalarValueType", "McpPlanSecretReference", "McpPlanTransportChoice", + "McpPromptsApi", "McpResourcesApi", "McpSafeForTelemetry", "McpServerAuthConfig", @@ -50901,6 +58866,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "Model", "ModelApi", "ModelApplyStartupOverlayRequest", + "ModelArtifactDetails", "ModelBilling", "ModelBillingPromo", "ModelBillingTokenPrices", @@ -50922,6 +58888,31 @@ async def handle_git_hub_token_get_token(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", @@ -50935,6 +58926,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ModelWarningText", "ModelsListRequest", "MoveMCPLoadingToBackgroundResult", + "NScope", "NameApi", "NameGetResult", "NameSetAutoRequest", @@ -50942,6 +58934,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "NameSetRequest", "NamedProviderConfig", "OpenCanvasInstance", + "OperationClass", "OptionsApi", "OptionsUpdateAdditionalContentExclusionPolicy", "OptionsUpdateAdditionalContentExclusionPolicyRule", @@ -50951,7 +58944,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "OptionsUpdateEnvValueMode", "OptionsUpdateReasoningSummary", "OptionsUpdateToolFilterPrecedence", - "PauseCheckpointAction", "PauseInfoClass", "PauseInfoType", "PendingPermissionRequest", @@ -50969,14 +58961,14 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "PermissionDecisionApproveForLocationApprovalExtensionManagementKind", "PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess", "PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind", - "PermissionDecisionApproveForLocationApprovalFactory", - "PermissionDecisionApproveForLocationApprovalFactoryKind", "PermissionDecisionApproveForLocationApprovalMCP", "PermissionDecisionApproveForLocationApprovalMCPSampling", "PermissionDecisionApproveForLocationApprovalMCPSamplingKind", "PermissionDecisionApproveForLocationApprovalMemory", "PermissionDecisionApproveForLocationApprovalRead", "PermissionDecisionApproveForLocationApprovalReadKind", + "PermissionDecisionApproveForLocationApprovalWorkflow", + "PermissionDecisionApproveForLocationApprovalWorkflowKind", "PermissionDecisionApproveForLocationApprovalWrite", "PermissionDecisionApproveForLocationApprovalWriteKind", "PermissionDecisionApproveForLocationKind", @@ -50987,17 +58979,19 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "PermissionDecisionApproveForSessionApprovalExtensionEnvAccess", "PermissionDecisionApproveForSessionApprovalExtensionManagement", "PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess", - "PermissionDecisionApproveForSessionApprovalFactory", "PermissionDecisionApproveForSessionApprovalMCP", "PermissionDecisionApproveForSessionApprovalMCPSampling", "PermissionDecisionApproveForSessionApprovalMemory", "PermissionDecisionApproveForSessionApprovalRead", + "PermissionDecisionApproveForSessionApprovalWorkflow", "PermissionDecisionApproveForSessionApprovalWrite", "PermissionDecisionApproveForSessionKind", "PermissionDecisionApproveOnce", "PermissionDecisionApproveOnceKind", "PermissionDecisionApprovePermanently", "PermissionDecisionApprovePermanentlyKind", + "PermissionDecisionApproveReadOnlyForSession", + "PermissionDecisionApproveReadOnlyForSessionKind", "PermissionDecisionApproved", "PermissionDecisionApprovedForLocation", "PermissionDecisionApprovedForLocationKind", @@ -51062,11 +59056,11 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess", "PermissionsLocationsAddToolApprovalDetailsExtensionManagement", "PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess", - "PermissionsLocationsAddToolApprovalDetailsFactory", "PermissionsLocationsAddToolApprovalDetailsMCP", "PermissionsLocationsAddToolApprovalDetailsMCPSampling", "PermissionsLocationsAddToolApprovalDetailsMemory", "PermissionsLocationsAddToolApprovalDetailsRead", + "PermissionsLocationsAddToolApprovalDetailsWorkflow", "PermissionsLocationsAddToolApprovalDetailsWrite", "PermissionsLocationsAddToolApprovalResult", "PermissionsLocationsApi", @@ -51090,10 +59084,11 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "PermissionsSetRequiredResult", "PermissionsUrlsApi", "PermissionsUrlsSetUnrestrictedModeResult", - "PhaseStatus", + "Phase", "PingRequest", "PingResult", "PlanApi", + "PlanClass", "PlanReadResult", "PlanReadSQLTodosResult", "PlanReadSQLTodosWithDependenciesResult", @@ -51141,9 +59136,11 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ProviderApi", "ProviderConfig", "ProviderConfigAzure", + "ProviderConfigModelProvider", "ProviderConfigTransport", "ProviderConfigType", "ProviderConfigWireApi", + "ProviderDescriptor", "ProviderEndpoint", "ProviderEndpointTransport", "ProviderEndpointType", @@ -51159,6 +59156,10 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ProviderTransport", "ProviderType", "ProviderWireAPI", + "ProviderWithdrawRequest", + "ProviderWithdrawResult", + "ProvidersApi", + "ProvidersModelsApi", "PurpleSource", "PushAttachment", "PushAttachmentBlob", @@ -51262,14 +59263,14 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "RemoteSessionMetadataValue", "RemoteSessionMode", "RemoteSessionRepository", + "Resource", "ResponseFormat", "ResponseFormatType", - "RunFailureKind", - "RunOptions", - "RunStatus", + "ReviewClass", "SandboxApi", "SandboxConfig", "SandboxConfigAuth", + "SandboxConfigSource", "SandboxConfigUserPolicy", "SandboxConfigUserPolicyExperimental", "SandboxConfigUserPolicyExperimentalSeatbelt", @@ -51277,12 +59278,20 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SandboxConfigUserPolicyNetwork", "SandboxConfigUserPolicyNetworkProxy", "SandboxConfigUserPolicySeatbelt", + "SandboxCredentialsConfig", "SandboxDisableForSessionRequest", "SandboxDisableForSessionResult", "SandboxEnforcementStatus", + "SandboxGrantPathForRequestRequest", + "SandboxGrantPathForRequestResult", "SandboxHostCapability", "SandboxHostCapabilityName", "SandboxHostSupport", + "SandboxMaskedEnvVar", + "SandboxProxyCACreateResult", + "SandboxProxyCARequest", + "SandboxProxyCAState", + "SandboxProxyCAStatus", "SandboxSessionChange", "Saved", "ScheduleAddAtRequest", @@ -51314,14 +59323,18 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ServerAgentsApi", "ServerCatalogApi", "ServerCommandsApi", + "ServerConnectorsApi", + "ServerEnvironmentsApi", "ServerExtensionsApi", "ServerHooksApi", + "ServerHostApi", "ServerInstructionSourceList", "ServerInstructionsApi", "ServerLlmInferenceApi", "ServerManagedSettingsApi", "ServerMcpApi", "ServerMcpConfigApi", + "ServerMcpInstallationsApi", "ServerModelsApi", "ServerPluginsApi", "ServerPluginsBuiltinApi", @@ -51329,6 +59342,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ServerRpc", "ServerRuntimeApi", "ServerSandboxApi", + "ServerSandboxProxyCaApi", "ServerSecretsApi", "ServerSessionFsApi", "ServerSessionsApi", @@ -51336,6 +59350,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ServerSkillList", "ServerSkillsApi", "ServerSkillsConfigApi", + "ServerSkillsInstallationsApi", "ServerToolsApi", "ServerUserApi", "ServerUserSettingsApi", @@ -51351,6 +59366,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionCapability", "SessionCommandsListRequest", "SessionCompletionItem", + "SessionConnectedIDEInfo", "SessionContext", "SessionContextAttribution", "SessionContextHostType", @@ -51362,6 +59378,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionFSExistsRequest", "SessionFSExistsResult", "SessionFSMkdirRequest", + "SessionFSReadFileBytesRequest", + "SessionFSReadFileBytesResult", "SessionFSReadFileRequest", "SessionFSReadFileResult", "SessionFSReaddirRequest", @@ -51387,8 +59405,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionFSSqliteTransactionStatement", "SessionFSStatRequest", "SessionFSStatResult", + "SessionFSWriteFileBytesRequest", "SessionFSWriteFileRequest", - "SessionFactoryPauseAtCheckpointResult", "SessionFsHandler", "SessionFsReaddirWithTypesEntryType", "SessionGitHubAuthGetAllAuthAvailableResult", @@ -51416,6 +59434,11 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionListFilter", "SessionLoadDeferredRepoHooksResult", "SessionLogLevel", + "SessionMCPOauthCancelLoginRequest", + "SessionMCPOauthCancelLoginResult", + "SessionMCPOauthPrepareLoginRequest", + "SessionMCPOauthPrepareLoginResult", + "SessionMCPSetConnectedIDEInfoParams", "SessionManagedPermissions", "SessionManagedSettings", "SessionMcpAppsCallToolResult", @@ -51462,6 +59485,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionWorkflowPauseAtCheckpointResult", "SessionWorkingDirectoryContext", "SessionWorkingDirectoryContextHostType", + "SessionWorkingDirectoryContextWithClient", "SessionsBulkDeleteRequest", "SessionsCheckInUseRequest", "SessionsCheckInUseResult", @@ -51469,6 +59493,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionsClientMetadataEntryStatus", "SessionsCloseRequest", "SessionsCloseResult", + "SessionsCreateWorkspaceRequest", + "SessionsCreateWorkspaceResult", "SessionsDeleteRequest", "SessionsEnrichMetadataRequest", "SessionsFindByPrefixRequest", @@ -51493,6 +59519,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionsListNonEmptySessionIDSResult", "SessionsListRequest", "SessionsLoadDeferredRepoHooksRequest", + "SessionsLoadWorkspaceRequest", + "SessionsLoadWorkspaceResult", "SessionsOpenAttach", "SessionsOpenAttachKind", "SessionsOpenCloud", @@ -51525,6 +59553,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionsStartRemoteControlRequest", "SessionsStopRemoteControlRequest", "SessionsTransferRemoteControlRequest", + "SessionsUpdateWorkspaceFieldsRequest", + "SessionsUpdateWorkspaceFieldsResult", "SettableAuthInfo", "SettableAuthInfoType", "SettableTokenAuthInfo", @@ -51544,12 +59574,36 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ShellOptions", "ShutdownRequest", "Skill", + "SkillApplyInstallRequest", + "SkillApplyUninstallRequest", "SkillDiscoveryPath", "SkillDiscoveryPathList", "SkillDiscoveryScope", + "SkillInstallPlan", + "SkillInstallationFailureReason", + "SkillInstallationFileReview", + "SkillInstallationLocation", + "SkillInstallationManagementOutcome", + "SkillInstallationManagementOutcomeKind", + "SkillInstallationManagementResult", + "SkillInstallationOperationRequest", + "SkillInstallationOperationStatus", + "SkillInstallationOutcome", + "SkillInstallationOwnershipState", + "SkillInstallationResult", + "SkillInstallationReview", + "SkillInstallationScope", + "SkillInstallationSessionState", + "SkillInstallationSource", + "SkillInstallationSummary", + "SkillInstallationsRequest", "SkillList", + "SkillPlanInstallRequest", + "SkillPlanUninstallRequest", "SkillProviderDescriptor", "SkillProviderListRequest", + "SkillSetEnabledRequest", + "SkillUninstallPlan", "SkillsApi", "SkillsConfigSetDisabledSkillsRequest", "SkillsConfigSetSkillDisabledRequest", @@ -51727,7 +59781,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "UserSettingMetadata", "UserSettingsGetResult", "UserSettingsSetRequest", - "UserSettingsSetResult", "VisibilityApi", "VisibilityGetResult", "VisibilitySetRequest", @@ -51780,7 +59833,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "WorkflowRunStatus", "WorkflowRunSummary", "WorkflowRunTerminal", - "WorkflowRunTerminalPauseInfo", "Workspace", "WorkspaceDiffFileChange", "WorkspaceDiffFileChangeType", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 6fef3e2ac2..15c09bbc49 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,14 +276,15 @@ 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. - FACTORY_RUN_UPDATED = "factory.run_updated" + WORKFLOW_RUN_UPDATED = "workflow.run_updated" # Experimental: this event is part of an experimental API and may change or be removed. - FACTORY_RUN_STARTED = "factory.run_started" + WORKFLOW_RUN_STARTED = "workflow.run_started" # Experimental: this event is part of an experimental API and may change or be removed. - FACTORY_RUN_SETTLED = "factory.run_settled" + WORKFLOW_RUN_SETTLED = "workflow.run_settled" SESSION_SKILLS_LOADED = "session.skills_loaded" SESSION_CUSTOM_AGENTS_UPDATED = "session.custom_agents_updated" SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded" @@ -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: @@ -501,6 +528,8 @@ class AssistantFusionPhaseCompletedData: _projection_message: Any = None # Internal: this field is an internal SDK API and is not part of the public surface. _projection_mode: _FusionProjectionMode | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + reasoning_effort: str | None = None # Internal: this field is an internal SDK API and is not part of the public surface. _staged_terminal: _FusionStagedTerminal | None = None @@ -520,6 +549,7 @@ def from_dict(obj: Any) -> "AssistantFusionPhaseCompletedData": verdict = from_union([from_none, from_str], obj.get("verdict")) _projection_message = obj.get("projectionMessage") _projection_mode = from_union([from_none, lambda x: parse_enum(_FusionProjectionMode, x)], obj.get("projectionMode")) + reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) _staged_terminal = from_union([from_none, _FusionStagedTerminal.from_dict], obj.get("stagedTerminal")) return AssistantFusionPhaseCompletedData( content=content, @@ -535,6 +565,7 @@ def from_dict(obj: Any) -> "AssistantFusionPhaseCompletedData": verdict=verdict, _projection_message=_projection_message, _projection_mode=_projection_mode, + reasoning_effort=reasoning_effort, _staged_terminal=_staged_terminal, ) @@ -555,6 +586,8 @@ def to_dict(self) -> dict: result["projectionMessage"] = self._projection_message if self._projection_mode is not None: result["projectionMode"] = from_union([from_none, lambda x: to_enum(_FusionProjectionMode, x)], self._projection_mode) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) if self._staged_terminal is not None: result["stagedTerminal"] = from_union([from_none, lambda x: to_class(_FusionStagedTerminal, x)], self._staged_terminal) return result @@ -576,6 +609,8 @@ class AssistantFusionPhaseFailedData: usage: FusionPhaseUsage degraded_to_phase_id: str | None = None error_message: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + reasoning_effort: str | None = None @staticmethod def from_dict(obj: Any) -> "AssistantFusionPhaseFailedData": @@ -592,6 +627,7 @@ def from_dict(obj: Any) -> "AssistantFusionPhaseFailedData": usage = FusionPhaseUsage.from_dict(obj.get("usage")) degraded_to_phase_id = from_union([from_none, from_str], obj.get("degradedToPhaseId")) error_message = from_union([from_none, from_str], obj.get("errorMessage")) + reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) return AssistantFusionPhaseFailedData( conversation_scope=conversation_scope, duration_ms=duration_ms, @@ -605,6 +641,7 @@ def from_dict(obj: Any) -> "AssistantFusionPhaseFailedData": usage=usage, degraded_to_phase_id=degraded_to_phase_id, error_message=error_message, + reasoning_effort=reasoning_effort, ) def to_dict(self) -> dict: @@ -623,6 +660,8 @@ def to_dict(self) -> dict: result["degradedToPhaseId"] = from_union([from_none, from_str], self.degraded_to_phase_id) if self.error_message is not None: result["errorMessage"] = from_union([from_none, from_str], self.error_message) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) return result @@ -637,6 +676,8 @@ class AssistantFusionPhaseStartedData: phase_id: str phase_kind: FusionPhaseKind role: str + # Experimental: this field is part of an experimental API and may change or be removed. + reasoning_effort: str | None = None @staticmethod def from_dict(obj: Any) -> "AssistantFusionPhaseStartedData": @@ -648,6 +689,7 @@ def from_dict(obj: Any) -> "AssistantFusionPhaseStartedData": phase_id = from_str(obj.get("phaseId")) phase_kind = parse_enum(FusionPhaseKind, obj.get("phaseKind")) role = from_str(obj.get("role")) + reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) return AssistantFusionPhaseStartedData( conversation_scope=conversation_scope, fusion_id=fusion_id, @@ -656,6 +698,7 @@ def from_dict(obj: Any) -> "AssistantFusionPhaseStartedData": phase_id=phase_id, phase_kind=phase_kind, role=role, + reasoning_effort=reasoning_effort, ) def to_dict(self) -> dict: @@ -667,6 +710,8 @@ def to_dict(self) -> dict: result["phaseId"] = from_str(self.phase_id) result["phaseKind"] = to_enum(FusionPhaseKind, self.phase_kind) result["role"] = from_str(self.role) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) return result @@ -676,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: @@ -692,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 @@ -1102,99 +1152,6 @@ def to_dict(self) -> dict: return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryRunSettledData: - "Ephemeral signal that a factory run reached a terminal status." - consumed_nano_aiu: int - consumed_subagents: int - elapsed_ms: int - run_id: str - status: FactoryRunSettledStatus - failure_type: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "FactoryRunSettledData": - assert isinstance(obj, dict) - consumed_nano_aiu = from_int(obj.get("consumedNanoAiu")) - consumed_subagents = from_int(obj.get("consumedSubagents")) - elapsed_ms = from_int(obj.get("elapsedMs")) - run_id = from_str(obj.get("runId")) - status = parse_enum(FactoryRunSettledStatus, obj.get("status")) - failure_type = from_union([from_none, from_str], obj.get("failureType")) - return FactoryRunSettledData( - consumed_nano_aiu=consumed_nano_aiu, - consumed_subagents=consumed_subagents, - elapsed_ms=elapsed_ms, - run_id=run_id, - status=status, - failure_type=failure_type, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["consumedNanoAiu"] = to_int(self.consumed_nano_aiu) - result["consumedSubagents"] = to_int(self.consumed_subagents) - result["elapsedMs"] = to_int(self.elapsed_ms) - result["runId"] = from_str(self.run_id) - result["status"] = to_enum(FactoryRunSettledStatus, self.status) - if self.failure_type is not None: - result["failureType"] = from_union([from_none, from_str], self.failure_type) - return result - - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryRunStartedData: - "Ephemeral signal that a factory run attempt began executing." - attempt: int - factory_name: str - run_id: str - - @staticmethod - def from_dict(obj: Any) -> "FactoryRunStartedData": - assert isinstance(obj, dict) - attempt = from_int(obj.get("attempt")) - factory_name = from_str(obj.get("factoryName")) - run_id = from_str(obj.get("runId")) - return FactoryRunStartedData( - attempt=attempt, - factory_name=factory_name, - run_id=run_id, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["attempt"] = to_int(self.attempt) - result["factoryName"] = from_str(self.factory_name) - result["runId"] = from_str(self.run_id) - return result - - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class FactoryRunUpdatedData: - "Ephemeral invalidation signal for a changed factory run." - revision: int - run_id: str - - @staticmethod - def from_dict(obj: Any) -> "FactoryRunUpdatedData": - assert isinstance(obj, dict) - revision = from_int(obj.get("revision")) - run_id = from_str(obj.get("runId")) - return FactoryRunUpdatedData( - revision=revision, - run_id=run_id, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["revision"] = to_int(self.revision) - result["runId"] = from_str(self.run_id) - return result - - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class FusionAttribution: @@ -1267,6 +1224,37 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class FusionCritic: + # Experimental: this field is part of an experimental API and may change or be removed. + model: str + # Experimental: this field is part of an experimental API and may change or be removed. + phase_id: str + # Experimental: this field is part of an experimental API and may change or be removed. + reasoning_effort: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "FusionCritic": + assert isinstance(obj, dict) + model = from_str(obj.get("model")) + phase_id = from_str(obj.get("phaseId")) + reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + return FusionCritic( + model=model, + phase_id=phase_id, + reasoning_effort=reasoning_effort, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["model"] = from_str(self.model) + result["phaseId"] = from_str(self.phase_id) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class FusionFollowUpRecommendation: @@ -1477,7 +1465,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionAssentDetectedData: - "Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation." + "Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow." # Experimental: this field is part of an experimental API and may change or be removed. request_id: str # Experimental: this field is part of an experimental API and may change or be removed. @@ -1543,7 +1531,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionCarriedForwardData: - "Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left." + "Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions." # Experimental: this field is part of an experimental API and may change or be removed. decision_source: PermissionDecisionSource # Experimental: this field is part of an experimental API and may change or be removed. @@ -1579,7 +1567,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionContextualAuthorizationData: - "Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify." + "Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it." # Experimental: this field is part of an experimental API and may change or be removed. polarity: PermissionMessageAuthorizationPolarity # Experimental: this field is part of an experimental API and may change or be removed. @@ -1625,7 +1613,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionMessageAuthorizationData: - "Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume." + "Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it." # Experimental: this field is part of an experimental API and may change or be removed. action_class: str # Experimental: this field is part of an experimental API and may change or be removed. @@ -1689,7 +1677,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionMessageAuthorizationDegradedData: - "Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt." + "Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions." # Experimental: this field is part of an experimental API and may change or be removed. turn_index: int @@ -1710,7 +1698,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionMessageAuthorizationReadData: - "Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction." + "Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it." # Experimental: this field is part of an experimental API and may change or be removed. turn_index: int # Experimental: this field is part of an experimental API and may change or be removed. @@ -1734,6 +1722,40 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionSandboxPathGrant: + "A sandbox filesystem policy edit that would let a blocked operation run inside the sandbox instead of outside it. Offered only on a sandbox escalation request whose denial adding this path lifts, and only when managed policy permits the grant. A host accepts it with session.sandbox.grantPathForRequest, which adds the path to the session's sandbox policy and re-runs the operation sandboxed; a host that persists sandbox settings may also save the path there." + access: PermissionSandboxPathGrantAccess + path: str + denied_path: str | None = None + removed_readonly_paths: list[str] | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionSandboxPathGrant": + assert isinstance(obj, dict) + access = parse_enum(PermissionSandboxPathGrantAccess, obj.get("access")) + path = from_str(obj.get("path")) + denied_path = from_union([from_none, from_str], obj.get("deniedPath")) + removed_readonly_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("removedReadonlyPaths")) + return PermissionSandboxPathGrant( + access=access, + path=path, + denied_path=denied_path, + removed_readonly_paths=removed_readonly_paths, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["access"] = to_enum(PermissionSandboxPathGrantAccess, self.access) + result["path"] = from_str(self.path) + if self.denied_path is not None: + result["deniedPath"] = from_union([from_none, from_str], self.denied_path) + if self.removed_readonly_paths is not None: + result["removedReadonlyPaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.removed_readonly_paths) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAutoModeResolvedData: @@ -1752,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 @@ -1771,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, @@ -1787,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, ) @@ -1819,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 @@ -2196,14 +2223,20 @@ class SessionFusionResolvedData: secondary_model: str | None synthetic_model: str turn_id: str + # Experimental: this field is part of an experimental API and may change or be removed. + critics: list[FusionCritic] | None = None follow_up: FusionFollowUpRecommendation | None = None # Experimental: this field is part of an experimental API and may change or be removed. hint: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + judge_model: str | None = None model_universe_version: str | None = None # Experimental: this field is part of an experimental API and may change or be removed. phase_plan: list[FusionPhasePlanStep] | None = None plan_version: str | None = None policy_version: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + repair_model: str | None = None route_source: str | None = None routing_latency_ms: float | None = None rule_id: str | None = None @@ -2224,12 +2257,15 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": secondary_model = from_union([from_none, from_str], obj.get("secondaryModel")) synthetic_model = from_str(obj.get("syntheticModel")) turn_id = from_str(obj.get("turnId")) + critics = from_union([from_none, lambda x: from_list(FusionCritic.from_dict, x)], obj.get("critics")) follow_up = from_union([from_none, FusionFollowUpRecommendation.from_dict], obj.get("followUp")) hint = from_union([from_none, from_str], obj.get("hint")) + judge_model = from_union([from_none, from_str], obj.get("judgeModel")) model_universe_version = from_union([from_none, from_str], obj.get("modelUniverseVersion")) phase_plan = from_union([from_none, lambda x: from_list(FusionPhasePlanStep.from_dict, x)], obj.get("phasePlan")) plan_version = from_union([from_none, from_str], obj.get("planVersion")) policy_version = from_union([from_none, from_str], obj.get("policyVersion")) + repair_model = from_union([from_none, from_str], obj.get("repairModel")) route_source = from_union([from_none, from_str], obj.get("routeSource")) routing_latency_ms = from_union([from_none, from_float], obj.get("routingLatencyMs")) rule_id = from_union([from_none, from_str], obj.get("ruleId")) @@ -2247,12 +2283,15 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": secondary_model=secondary_model, synthetic_model=synthetic_model, turn_id=turn_id, + critics=critics, follow_up=follow_up, hint=hint, + judge_model=judge_model, model_universe_version=model_universe_version, phase_plan=phase_plan, plan_version=plan_version, policy_version=policy_version, + repair_model=repair_model, route_source=route_source, routing_latency_ms=routing_latency_ms, rule_id=rule_id, @@ -2273,10 +2312,14 @@ def to_dict(self) -> dict: result["secondaryModel"] = from_union([from_none, from_str], self.secondary_model) result["syntheticModel"] = from_str(self.synthetic_model) result["turnId"] = from_str(self.turn_id) + if self.critics is not None: + result["critics"] = from_union([from_none, lambda x: from_list(lambda x: to_class(FusionCritic, x), x)], self.critics) if self.follow_up is not None: result["followUp"] = from_union([from_none, lambda x: to_class(FusionFollowUpRecommendation, x)], self.follow_up) if self.hint is not None: result["hint"] = from_union([from_none, from_str], self.hint) + if self.judge_model is not None: + result["judgeModel"] = from_union([from_none, from_str], self.judge_model) if self.model_universe_version is not None: result["modelUniverseVersion"] = from_union([from_none, from_str], self.model_universe_version) if self.phase_plan is not None: @@ -2285,6 +2328,8 @@ def to_dict(self) -> dict: result["planVersion"] = from_union([from_none, from_str], self.plan_version) if self.policy_version is not None: result["policyVersion"] = from_union([from_none, from_str], self.policy_version) + if self.repair_model is not None: + result["repairModel"] = from_union([from_none, from_str], self.repair_model) if self.route_source is not None: result["routeSource"] = from_union([from_none, from_str], self.route_source) if self.routing_latency_ms is not None: @@ -2420,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 @@ -2548,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: @@ -2607,71 +2676,164 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AbortData: - "Turn abort information including the reason for termination" - reason: AbortReason +class WorkflowRunSettledData: + "Ephemeral signal that a workflow run reached a terminal status." + consumed_nano_aiu: int + consumed_subagents: int + elapsed_ms: int + run_id: str + status: WorkflowRunSettledStatus + failure_type: str | None = None @staticmethod - def from_dict(obj: Any) -> "AbortData": + def from_dict(obj: Any) -> "WorkflowRunSettledData": assert isinstance(obj, dict) - reason = parse_enum(AbortReason, obj.get("reason")) - return AbortData( - reason=reason, + consumed_nano_aiu = from_int(obj.get("consumedNanoAiu")) + consumed_subagents = from_int(obj.get("consumedSubagents")) + elapsed_ms = from_int(obj.get("elapsedMs")) + run_id = from_str(obj.get("runId")) + status = parse_enum(WorkflowRunSettledStatus, obj.get("status")) + failure_type = from_union([from_none, from_str], obj.get("failureType")) + return WorkflowRunSettledData( + consumed_nano_aiu=consumed_nano_aiu, + consumed_subagents=consumed_subagents, + elapsed_ms=elapsed_ms, + run_id=run_id, + status=status, + failure_type=failure_type, ) def to_dict(self) -> dict: result: dict = {} - result["reason"] = to_enum(AbortReason, self.reason) + result["consumedNanoAiu"] = to_int(self.consumed_nano_aiu) + result["consumedSubagents"] = to_int(self.consumed_subagents) + result["elapsedMs"] = to_int(self.elapsed_ms) + result["runId"] = from_str(self.run_id) + result["status"] = to_enum(WorkflowRunSettledStatus, self.status) + if self.failure_type is not None: + result["failureType"] = from_union([from_none, from_str], self.failure_type) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AgentInterruptedData: - "Metadata for work the user interrupted while the agent was running" - activity: AgentInterruptedActivity - elapsed: timedelta - turn: int - api_endpoint: str | None = None - cancel_phase: AgentInterruptedCancelPhase | None = None - interrupted_agent_count: int | None = None - model: str | None = None - output_ttft: timedelta | None = None - reasoning_effort: str | None = None - safe_tool_names: list[str] | None = None - tool_call_ids: list[str] | None = None - tool_names: list[str] | None = None - transport: ModelCallFailureTransport | None = None +class WorkflowRunStartedData: + "Ephemeral signal that a workflow run attempt began executing." + attempt: int + run_id: str + workflow_name: str @staticmethod - def from_dict(obj: Any) -> "AgentInterruptedData": + def from_dict(obj: Any) -> "WorkflowRunStartedData": assert isinstance(obj, dict) - activity = parse_enum(AgentInterruptedActivity, obj.get("activity")) - elapsed = from_timedelta(obj.get("elapsedMs")) - turn = from_int(obj.get("turn")) - api_endpoint = from_union([from_none, from_str], obj.get("apiEndpoint")) - cancel_phase = from_union([from_none, lambda x: parse_enum(AgentInterruptedCancelPhase, x)], obj.get("cancelPhase")) - interrupted_agent_count = from_union([from_none, from_int], obj.get("interruptedAgentCount")) - model = from_union([from_none, from_str], obj.get("model")) - output_ttft = from_union([from_none, from_timedelta], obj.get("outputTtftMs")) - reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) - safe_tool_names = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("safeToolNames")) - tool_call_ids = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolCallIds")) - tool_names = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolNames")) - transport = from_union([from_none, lambda x: parse_enum(ModelCallFailureTransport, x)], obj.get("transport")) - return AgentInterruptedData( - activity=activity, - elapsed=elapsed, - turn=turn, - api_endpoint=api_endpoint, - cancel_phase=cancel_phase, - interrupted_agent_count=interrupted_agent_count, - model=model, - output_ttft=output_ttft, - reasoning_effort=reasoning_effort, - safe_tool_names=safe_tool_names, - tool_call_ids=tool_call_ids, - tool_names=tool_names, + attempt = from_int(obj.get("attempt")) + run_id = from_str(obj.get("runId")) + workflow_name = from_str(obj.get("workflowName")) + return WorkflowRunStartedData( + attempt=attempt, + run_id=run_id, + workflow_name=workflow_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["attempt"] = to_int(self.attempt) + result["runId"] = from_str(self.run_id) + result["workflowName"] = from_str(self.workflow_name) + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class WorkflowRunUpdatedData: + "Ephemeral invalidation signal for a changed workflow run." + revision: int + run_id: str + + @staticmethod + def from_dict(obj: Any) -> "WorkflowRunUpdatedData": + assert isinstance(obj, dict) + revision = from_int(obj.get("revision")) + run_id = from_str(obj.get("runId")) + return WorkflowRunUpdatedData( + revision=revision, + run_id=run_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["revision"] = to_int(self.revision) + result["runId"] = from_str(self.run_id) + return result + + +@dataclass +class AbortData: + "Turn abort information including the reason for termination" + reason: AbortReason + + @staticmethod + def from_dict(obj: Any) -> "AbortData": + assert isinstance(obj, dict) + reason = parse_enum(AbortReason, obj.get("reason")) + return AbortData( + reason=reason, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["reason"] = to_enum(AbortReason, self.reason) + return result + + +@dataclass +class AgentInterruptedData: + "Metadata for work the user interrupted while the agent was running" + activity: AgentInterruptedActivity + elapsed: timedelta + turn: int + api_endpoint: str | None = None + cancel_phase: AgentInterruptedCancelPhase | None = None + interrupted_agent_count: int | None = None + model: str | None = None + output_ttft: timedelta | None = None + reasoning_effort: str | None = None + safe_tool_names: list[str] | None = None + tool_call_ids: list[str] | None = None + tool_names: list[str] | None = None + transport: ModelCallFailureTransport | None = None + + @staticmethod + def from_dict(obj: Any) -> "AgentInterruptedData": + assert isinstance(obj, dict) + activity = parse_enum(AgentInterruptedActivity, obj.get("activity")) + elapsed = from_timedelta(obj.get("elapsedMs")) + turn = from_int(obj.get("turn")) + api_endpoint = from_union([from_none, from_str], obj.get("apiEndpoint")) + cancel_phase = from_union([from_none, lambda x: parse_enum(AgentInterruptedCancelPhase, x)], obj.get("cancelPhase")) + interrupted_agent_count = from_union([from_none, from_int], obj.get("interruptedAgentCount")) + model = from_union([from_none, from_str], obj.get("model")) + output_ttft = from_union([from_none, from_timedelta], obj.get("outputTtftMs")) + reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + safe_tool_names = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("safeToolNames")) + tool_call_ids = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolCallIds")) + tool_names = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolNames")) + transport = from_union([from_none, lambda x: parse_enum(ModelCallFailureTransport, x)], obj.get("transport")) + return AgentInterruptedData( + activity=activity, + elapsed=elapsed, + turn=turn, + api_endpoint=api_endpoint, + cancel_phase=cancel_phase, + interrupted_agent_count=interrupted_agent_count, + model=model, + output_ttft=output_ttft, + reasoning_effort=reasoning_effort, + safe_tool_names=safe_tool_names, + tool_call_ids=tool_call_ids, + tool_names=tool_names, transport=transport, ) @@ -3152,15 +3314,18 @@ class AssistantTurnEndData: "Turn completion metadata including the turn identifier" turn_id: str model: str | None = None + parent_tool_call_id: str | None = None @staticmethod def from_dict(obj: Any) -> "AssistantTurnEndData": assert isinstance(obj, dict) turn_id = from_str(obj.get("turnId")) model = from_union([from_none, from_str], obj.get("model")) + parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) return AssistantTurnEndData( turn_id=turn_id, model=model, + parent_tool_call_id=parent_tool_call_id, ) def to_dict(self) -> dict: @@ -3168,6 +3333,8 @@ def to_dict(self) -> dict: result["turnId"] = from_str(self.turn_id) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) return result @@ -3206,6 +3373,7 @@ class AssistantTurnStartData: turn_id: str interaction_id: str | None = None model: str | None = None + parent_tool_call_id: str | None = None @staticmethod def from_dict(obj: Any) -> "AssistantTurnStartData": @@ -3213,10 +3381,12 @@ def from_dict(obj: Any) -> "AssistantTurnStartData": turn_id = from_str(obj.get("turnId")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) model = from_union([from_none, from_str], obj.get("model")) + parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) return AssistantTurnStartData( turn_id=turn_id, interaction_id=interaction_id, model=model, + parent_tool_call_id=parent_tool_call_id, ) def to_dict(self) -> dict: @@ -3226,6 +3396,8 @@ def to_dict(self) -> dict: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) return result @@ -3304,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 @@ -3329,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 @@ -3363,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")) @@ -3383,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")) @@ -3407,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, @@ -3427,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, @@ -3458,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: @@ -3498,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: @@ -5093,30 +5275,6 @@ def to_dict(self) -> dict: return result -@dataclass -class FactoryPermissionPhase: - "A declared phase shown in a factory permission prompt." - title: str - detail: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "FactoryPermissionPhase": - assert isinstance(obj, dict) - title = from_str(obj.get("title")) - detail = from_union([from_none, from_str], obj.get("detail")) - return FactoryPermissionPhase( - title=title, - detail=detail, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["title"] = from_str(self.title) - if self.detail is not None: - result["detail"] = from_union([from_none, from_str], self.detail) - return result - - @dataclass class GitHubMcpToolConfig: "Per-session configuration for the built-in GitHub MCP server" @@ -5359,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)" @@ -5840,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 @@ -5854,6 +6144,8 @@ 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. _quota_snapshots: dict[str, _AssistantUsageQuotaSnapshot] | None = None @@ -5871,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")) @@ -5884,6 +6177,8 @@ 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")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) @@ -5897,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, @@ -5910,6 +6206,8 @@ 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, reasoning_effort=reasoning_effort, @@ -5929,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: @@ -5955,6 +6255,10 @@ 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: result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) if self._quota_snapshots is not None: @@ -6018,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." @@ -6066,6 +6398,7 @@ class ModelCallStartData: # Experimental: this field is part of an experimental API and may change or be removed. fusion: FusionAttribution | None = None model: str | None = None + parent_tool_call_id: str | None = None # Internal: this field is an internal SDK API and is not part of the public surface. _previous_response_id: str | None = None @@ -6075,11 +6408,13 @@ def from_dict(obj: Any) -> "ModelCallStartData": turn_id = from_str(obj.get("turnId")) fusion = from_union([from_none, FusionAttribution.from_dict], obj.get("fusion")) model = from_union([from_none, from_str], obj.get("model")) + parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) _previous_response_id = from_union([from_none, from_str], obj.get("previousResponseId")) return ModelCallStartData( turn_id=turn_id, fusion=fusion, model=model, + parent_tool_call_id=parent_tool_call_id, _previous_response_id=_previous_response_id, ) @@ -6090,6 +6425,8 @@ def to_dict(self) -> dict: result["fusion"] = from_union([from_none, lambda x: to_class(FusionAttribution, x)], self.fusion) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) if self._previous_response_id is not None: result["previousResponseId"] = from_union([from_none, from_str], self._previous_response_id) return result @@ -6217,6 +6554,27 @@ def to_dict(self) -> dict: return result +@dataclass +class PermissionApprovedReadOnlyForSession: + "Permission response variant that approves a request and records file-tool read authority for specific directories in this logical session." + directories: list[str] + kind: ClassVar[str] = "approved-read-only-for-session" + + @staticmethod + def from_dict(obj: Any) -> "PermissionApprovedReadOnlyForSession": + assert isinstance(obj, dict) + directories = from_list(from_str, obj.get("directories")) + return PermissionApprovedReadOnlyForSession( + directories=directories, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["directories"] = from_list(from_str, self.directories) + result["kind"] = self.kind + return result + + @dataclass class PermissionCancelled: "Permission response variant indicating the request was cancelled before use, with an optional reason." @@ -6416,6 +6774,8 @@ class PermissionPromptRequestCommands: request_sandbox_permissive: bool | None = None tool_call_id: str | None = None warning: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + sandbox_path_grant: PermissionSandboxPathGrant | None = None @staticmethod def from_dict(obj: Any) -> "PermissionPromptRequestCommands": @@ -6431,6 +6791,7 @@ def from_dict(obj: Any) -> "PermissionPromptRequestCommands": request_sandbox_permissive = from_union([from_none, from_bool], obj.get("requestSandboxPermissive")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) warning = from_union([from_none, from_str], obj.get("warning")) + sandbox_path_grant = from_union([from_none, PermissionSandboxPathGrant.from_dict], obj.get("sandboxPathGrant")) return PermissionPromptRequestCommands( can_offer_session_approval=can_offer_session_approval, command_identifiers=command_identifiers, @@ -6443,6 +6804,7 @@ def from_dict(obj: Any) -> "PermissionPromptRequestCommands": request_sandbox_permissive=request_sandbox_permissive, tool_call_id=tool_call_id, warning=warning, + sandbox_path_grant=sandbox_path_grant, ) def to_dict(self) -> dict: @@ -6466,6 +6828,8 @@ def to_dict(self) -> dict: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) if self.warning is not None: result["warning"] = from_union([from_none, from_str], self.warning) + if self.sandbox_path_grant is not None: + result["sandboxPathGrant"] = from_union([from_none, lambda x: to_class(PermissionSandboxPathGrant, x)], self.sandbox_path_grant) return result @@ -6619,103 +6983,6 @@ def to_dict(self) -> dict: return result -@dataclass -class PermissionPromptRequestFactory: - "Factory run or authoring permission prompt" - approval_key: str - can_persist_approval: bool - description: str - kind: ClassVar[str] = "factory" - name: str - operation: FactoryPermissionOperation - phases: list[FactoryPermissionPhase] - # Experimental: this field is part of an experimental API and may change or be removed. - assisted_approval: PermissionAssistedApproval | None = None - declared_max_ai_credits: float | None = None - declared_max_concurrent_subagents: int | None = None - declared_max_total_subagents: int | None = None - declared_timeout_seconds: float | None = None - managed_approval_required: bool | None = None - max_ai_credits: float | None = None - max_concurrent_subagents: int | None = None - max_total_subagents: int | None = None - timeout_seconds: float | None = None - tool_call_id: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "PermissionPromptRequestFactory": - assert isinstance(obj, dict) - approval_key = from_str(obj.get("approvalKey")) - can_persist_approval = from_bool(obj.get("canPersistApproval")) - description = from_str(obj.get("description")) - name = from_str(obj.get("name")) - operation = parse_enum(FactoryPermissionOperation, obj.get("operation")) - phases = from_list(FactoryPermissionPhase.from_dict, obj.get("phases")) - assisted_approval = from_union([from_none, PermissionAssistedApproval.from_dict], obj.get("assistedApproval")) - declared_max_ai_credits = from_union([from_none, from_float], obj.get("declaredMaxAiCredits")) - declared_max_concurrent_subagents = from_union([from_none, from_int], obj.get("declaredMaxConcurrentSubagents")) - declared_max_total_subagents = from_union([from_none, from_int], obj.get("declaredMaxTotalSubagents")) - declared_timeout_seconds = from_union([from_none, from_float], obj.get("declaredTimeoutSeconds")) - managed_approval_required = from_union([from_none, from_bool], obj.get("managedApprovalRequired")) - max_ai_credits = from_union([from_none, from_float], obj.get("maxAiCredits")) - max_concurrent_subagents = from_union([from_none, from_int], obj.get("maxConcurrentSubagents")) - max_total_subagents = from_union([from_none, from_int], obj.get("maxTotalSubagents")) - timeout_seconds = from_union([from_none, from_float], obj.get("timeoutSeconds")) - tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) - return PermissionPromptRequestFactory( - approval_key=approval_key, - can_persist_approval=can_persist_approval, - description=description, - name=name, - operation=operation, - phases=phases, - assisted_approval=assisted_approval, - declared_max_ai_credits=declared_max_ai_credits, - declared_max_concurrent_subagents=declared_max_concurrent_subagents, - declared_max_total_subagents=declared_max_total_subagents, - declared_timeout_seconds=declared_timeout_seconds, - managed_approval_required=managed_approval_required, - max_ai_credits=max_ai_credits, - max_concurrent_subagents=max_concurrent_subagents, - max_total_subagents=max_total_subagents, - timeout_seconds=timeout_seconds, - tool_call_id=tool_call_id, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["approvalKey"] = from_str(self.approval_key) - result["canPersistApproval"] = from_bool(self.can_persist_approval) - result["description"] = from_str(self.description) - result["kind"] = self.kind - result["name"] = from_str(self.name) - result["operation"] = to_enum(FactoryPermissionOperation, self.operation) - result["phases"] = from_list(lambda x: to_class(FactoryPermissionPhase, x), self.phases) - if self.assisted_approval is not None: - result["assistedApproval"] = from_union([from_none, lambda x: to_class(PermissionAssistedApproval, x)], self.assisted_approval) - if self.declared_max_ai_credits is not None: - result["declaredMaxAiCredits"] = from_union([from_none, to_float], self.declared_max_ai_credits) - if self.declared_max_concurrent_subagents is not None: - result["declaredMaxConcurrentSubagents"] = from_union([from_none, to_int], self.declared_max_concurrent_subagents) - if self.declared_max_total_subagents is not None: - result["declaredMaxTotalSubagents"] = from_union([from_none, to_int], self.declared_max_total_subagents) - if self.declared_timeout_seconds is not None: - result["declaredTimeoutSeconds"] = from_union([from_none, to_float], self.declared_timeout_seconds) - if self.managed_approval_required is not None: - result["managedApprovalRequired"] = from_union([from_none, from_bool], self.managed_approval_required) - if self.max_ai_credits is not None: - result["maxAiCredits"] = from_union([from_none, to_float], self.max_ai_credits) - if self.max_concurrent_subagents is not None: - result["maxConcurrentSubagents"] = from_union([from_none, to_int], self.max_concurrent_subagents) - if self.max_total_subagents is not None: - result["maxTotalSubagents"] = from_union([from_none, to_int], self.max_total_subagents) - if self.timeout_seconds is not None: - result["timeoutSeconds"] = from_union([from_none, to_float], self.timeout_seconds) - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) - return result - - @dataclass class PermissionPromptRequestHook: "Hook confirmation permission prompt" @@ -6879,6 +7146,8 @@ class PermissionPromptRequestPath: paths: list[str] # Experimental: this field is part of an experimental API and may change or be removed. assisted_approval: PermissionAssistedApproval | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + read_only_directories: list[str] | None = None tool_call_id: str | None = None @staticmethod @@ -6887,11 +7156,13 @@ def from_dict(obj: Any) -> "PermissionPromptRequestPath": access_kind = parse_enum(PermissionPromptRequestPathAccessKind, obj.get("accessKind")) paths = from_list(from_str, obj.get("paths")) assisted_approval = from_union([from_none, PermissionAssistedApproval.from_dict], obj.get("assistedApproval")) + read_only_directories = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("readOnlyDirectories")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) return PermissionPromptRequestPath( access_kind=access_kind, paths=paths, assisted_approval=assisted_approval, + read_only_directories=read_only_directories, tool_call_id=tool_call_id, ) @@ -6902,6 +7173,8 @@ def to_dict(self) -> dict: result["paths"] = from_list(from_str, self.paths) if self.assisted_approval is not None: result["assistedApproval"] = from_union([from_none, lambda x: to_class(PermissionAssistedApproval, x)], self.assisted_approval) + if self.read_only_directories is not None: + result["readOnlyDirectories"] = from_union([from_none, lambda x: from_list(from_str, x)], self.read_only_directories) if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @@ -7010,6 +7283,103 @@ def to_dict(self) -> dict: return result +@dataclass +class PermissionPromptRequestWorkflow: + "Workflow run or authoring permission prompt" + approval_key: str + can_persist_approval: bool + description: str + kind: ClassVar[str] = "workflow" + name: str + operation: WorkflowPermissionOperation + phases: list[WorkflowPermissionPhase] + # Experimental: this field is part of an experimental API and may change or be removed. + assisted_approval: PermissionAssistedApproval | None = None + declared_max_ai_credits: float | None = None + declared_max_concurrent_subagents: int | None = None + declared_max_total_subagents: int | None = None + declared_timeout_seconds: float | None = None + managed_approval_required: bool | None = None + max_ai_credits: float | None = None + max_concurrent_subagents: int | None = None + max_total_subagents: int | None = None + timeout_seconds: float | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestWorkflow": + assert isinstance(obj, dict) + approval_key = from_str(obj.get("approvalKey")) + can_persist_approval = from_bool(obj.get("canPersistApproval")) + description = from_str(obj.get("description")) + name = from_str(obj.get("name")) + operation = parse_enum(WorkflowPermissionOperation, obj.get("operation")) + phases = from_list(WorkflowPermissionPhase.from_dict, obj.get("phases")) + assisted_approval = from_union([from_none, PermissionAssistedApproval.from_dict], obj.get("assistedApproval")) + declared_max_ai_credits = from_union([from_none, from_float], obj.get("declaredMaxAiCredits")) + declared_max_concurrent_subagents = from_union([from_none, from_int], obj.get("declaredMaxConcurrentSubagents")) + declared_max_total_subagents = from_union([from_none, from_int], obj.get("declaredMaxTotalSubagents")) + declared_timeout_seconds = from_union([from_none, from_float], obj.get("declaredTimeoutSeconds")) + managed_approval_required = from_union([from_none, from_bool], obj.get("managedApprovalRequired")) + max_ai_credits = from_union([from_none, from_float], obj.get("maxAiCredits")) + max_concurrent_subagents = from_union([from_none, from_int], obj.get("maxConcurrentSubagents")) + max_total_subagents = from_union([from_none, from_int], obj.get("maxTotalSubagents")) + timeout_seconds = from_union([from_none, from_float], obj.get("timeoutSeconds")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestWorkflow( + approval_key=approval_key, + can_persist_approval=can_persist_approval, + description=description, + name=name, + operation=operation, + phases=phases, + assisted_approval=assisted_approval, + declared_max_ai_credits=declared_max_ai_credits, + declared_max_concurrent_subagents=declared_max_concurrent_subagents, + declared_max_total_subagents=declared_max_total_subagents, + declared_timeout_seconds=declared_timeout_seconds, + managed_approval_required=managed_approval_required, + max_ai_credits=max_ai_credits, + max_concurrent_subagents=max_concurrent_subagents, + max_total_subagents=max_total_subagents, + timeout_seconds=timeout_seconds, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["approvalKey"] = from_str(self.approval_key) + result["canPersistApproval"] = from_bool(self.can_persist_approval) + result["description"] = from_str(self.description) + result["kind"] = self.kind + result["name"] = from_str(self.name) + result["operation"] = to_enum(WorkflowPermissionOperation, self.operation) + result["phases"] = from_list(lambda x: to_class(WorkflowPermissionPhase, x), self.phases) + if self.assisted_approval is not None: + result["assistedApproval"] = from_union([from_none, lambda x: to_class(PermissionAssistedApproval, x)], self.assisted_approval) + if self.declared_max_ai_credits is not None: + result["declaredMaxAiCredits"] = from_union([from_none, to_float], self.declared_max_ai_credits) + if self.declared_max_concurrent_subagents is not None: + result["declaredMaxConcurrentSubagents"] = from_union([from_none, to_int], self.declared_max_concurrent_subagents) + if self.declared_max_total_subagents is not None: + result["declaredMaxTotalSubagents"] = from_union([from_none, to_int], self.declared_max_total_subagents) + if self.declared_timeout_seconds is not None: + result["declaredTimeoutSeconds"] = from_union([from_none, to_float], self.declared_timeout_seconds) + if self.managed_approval_required is not None: + result["managedApprovalRequired"] = from_union([from_none, from_bool], self.managed_approval_required) + if self.max_ai_credits is not None: + result["maxAiCredits"] = from_union([from_none, to_float], self.max_ai_credits) + if self.max_concurrent_subagents is not None: + result["maxConcurrentSubagents"] = from_union([from_none, to_int], self.max_concurrent_subagents) + if self.max_total_subagents is not None: + result["maxTotalSubagents"] = from_union([from_none, to_int], self.max_total_subagents) + if self.timeout_seconds is not None: + result["timeoutSeconds"] = from_union([from_none, to_float], self.timeout_seconds) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + @dataclass class PermissionPromptRequestWrite: "File write permission prompt" @@ -7307,97 +7677,6 @@ def to_dict(self) -> dict: return result -@dataclass -class PermissionRequestFactory: - "Factory run or authoring permission request" - approval_key: str - can_persist_approval: bool - description: str - kind: ClassVar[str] = "factory" - name: str - operation: FactoryPermissionOperation - phases: list[FactoryPermissionPhase] - declared_max_ai_credits: float | None = None - declared_max_concurrent_subagents: int | None = None - declared_max_total_subagents: int | None = None - declared_timeout_seconds: float | None = None - max_ai_credits: float | None = None - max_concurrent_subagents: int | None = None - max_total_subagents: int | None = None - timeout_seconds: float | None = None - tool_call_id: str | None = None - managed_approval_required: bool | None = None - - @staticmethod - def from_dict(obj: Any) -> "PermissionRequestFactory": - assert isinstance(obj, dict) - approval_key = from_str(obj.get("approvalKey")) - can_persist_approval = from_bool(obj.get("canPersistApproval")) - description = from_str(obj.get("description")) - name = from_str(obj.get("name")) - operation = parse_enum(FactoryPermissionOperation, obj.get("operation")) - phases = from_list(FactoryPermissionPhase.from_dict, obj.get("phases")) - declared_max_ai_credits = from_union([from_none, from_float], obj.get("declaredMaxAiCredits")) - declared_max_concurrent_subagents = from_union([from_none, from_int], obj.get("declaredMaxConcurrentSubagents")) - declared_max_total_subagents = from_union([from_none, from_int], obj.get("declaredMaxTotalSubagents")) - declared_timeout_seconds = from_union([from_none, from_float], obj.get("declaredTimeoutSeconds")) - max_ai_credits = from_union([from_none, from_float], obj.get("maxAiCredits")) - max_concurrent_subagents = from_union([from_none, from_int], obj.get("maxConcurrentSubagents")) - max_total_subagents = from_union([from_none, from_int], obj.get("maxTotalSubagents")) - timeout_seconds = from_union([from_none, from_float], obj.get("timeoutSeconds")) - tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) - managed_approval_required = from_union([from_none, from_bool], obj.get("managedApprovalRequired")) - return PermissionRequestFactory( - approval_key=approval_key, - can_persist_approval=can_persist_approval, - description=description, - name=name, - operation=operation, - phases=phases, - declared_max_ai_credits=declared_max_ai_credits, - declared_max_concurrent_subagents=declared_max_concurrent_subagents, - declared_max_total_subagents=declared_max_total_subagents, - declared_timeout_seconds=declared_timeout_seconds, - max_ai_credits=max_ai_credits, - max_concurrent_subagents=max_concurrent_subagents, - max_total_subagents=max_total_subagents, - timeout_seconds=timeout_seconds, - tool_call_id=tool_call_id, - managed_approval_required=managed_approval_required, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["approvalKey"] = from_str(self.approval_key) - result["canPersistApproval"] = from_bool(self.can_persist_approval) - result["description"] = from_str(self.description) - result["kind"] = self.kind - result["name"] = from_str(self.name) - result["operation"] = to_enum(FactoryPermissionOperation, self.operation) - result["phases"] = from_list(lambda x: to_class(FactoryPermissionPhase, x), self.phases) - if self.declared_max_ai_credits is not None: - result["declaredMaxAiCredits"] = from_union([from_none, to_float], self.declared_max_ai_credits) - if self.declared_max_concurrent_subagents is not None: - result["declaredMaxConcurrentSubagents"] = from_union([from_none, to_int], self.declared_max_concurrent_subagents) - if self.declared_max_total_subagents is not None: - result["declaredMaxTotalSubagents"] = from_union([from_none, to_int], self.declared_max_total_subagents) - if self.declared_timeout_seconds is not None: - result["declaredTimeoutSeconds"] = from_union([from_none, to_float], self.declared_timeout_seconds) - if self.max_ai_credits is not None: - result["maxAiCredits"] = from_union([from_none, to_float], self.max_ai_credits) - if self.max_concurrent_subagents is not None: - result["maxConcurrentSubagents"] = from_union([from_none, to_int], self.max_concurrent_subagents) - if self.max_total_subagents is not None: - result["maxTotalSubagents"] = from_union([from_none, to_int], self.max_total_subagents) - if self.timeout_seconds is not None: - result["timeoutSeconds"] = from_union([from_none, to_float], self.timeout_seconds) - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) - if self.managed_approval_required is not None: - result["managedApprovalRequired"] = from_union([from_none, from_bool], self.managed_approval_required) - return result - - @dataclass class PermissionRequestHook: "Hook confirmation permission request" @@ -7577,6 +7856,8 @@ class PermissionRequestRead: tool_call_id: str | None = None # Experimental: this field is part of an experimental API and may change or be removed. resolved_path: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + sandbox_path_grant: PermissionSandboxPathGrant | None = None @staticmethod def from_dict(obj: Any) -> "PermissionRequestRead": @@ -7588,6 +7869,7 @@ def from_dict(obj: Any) -> "PermissionRequestRead": request_sandbox_bypass_reason = from_union([from_none, from_str], obj.get("requestSandboxBypassReason")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) resolved_path = from_union([from_none, from_str], obj.get("resolvedPath")) + sandbox_path_grant = from_union([from_none, PermissionSandboxPathGrant.from_dict], obj.get("sandboxPathGrant")) return PermissionRequestRead( intention=intention, path=path, @@ -7596,6 +7878,7 @@ def from_dict(obj: Any) -> "PermissionRequestRead": request_sandbox_bypass_reason=request_sandbox_bypass_reason, tool_call_id=tool_call_id, resolved_path=resolved_path, + sandbox_path_grant=sandbox_path_grant, ) def to_dict(self) -> dict: @@ -7613,6 +7896,8 @@ def to_dict(self) -> dict: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) if self.resolved_path is not None: result["resolvedPath"] = from_union([from_none, from_str], self.resolved_path) + if self.sandbox_path_grant is not None: + result["sandboxPathGrant"] = from_union([from_none, lambda x: to_class(PermissionSandboxPathGrant, x)], self.sandbox_path_grant) return result @@ -7638,6 +7923,8 @@ class PermissionRequestShell: resolved_working_directory: str | None = None tool_call_id: str | None = None warning: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + sandbox_path_grant: PermissionSandboxPathGrant | None = None @staticmethod def from_dict(obj: Any) -> "PermissionRequestShell": @@ -7658,6 +7945,7 @@ def from_dict(obj: Any) -> "PermissionRequestShell": resolved_working_directory = from_union([from_none, from_str], obj.get("resolvedWorkingDirectory")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) warning = from_union([from_none, from_str], obj.get("warning")) + sandbox_path_grant = from_union([from_none, PermissionSandboxPathGrant.from_dict], obj.get("sandboxPathGrant")) return PermissionRequestShell( can_offer_session_approval=can_offer_session_approval, commands=commands, @@ -7675,6 +7963,7 @@ def from_dict(obj: Any) -> "PermissionRequestShell": resolved_working_directory=resolved_working_directory, tool_call_id=tool_call_id, warning=warning, + sandbox_path_grant=sandbox_path_grant, ) def to_dict(self) -> dict: @@ -7705,6 +7994,8 @@ def to_dict(self) -> dict: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) if self.warning is not None: result["warning"] = from_union([from_none, from_str], self.warning) + if self.sandbox_path_grant is not None: + result["sandboxPathGrant"] = from_union([from_none, lambda x: to_class(PermissionSandboxPathGrant, x)], self.sandbox_path_grant) return result @@ -7823,6 +8114,97 @@ def to_dict(self) -> dict: return result +@dataclass +class PermissionRequestWorkflow: + "Workflow run or authoring permission request" + approval_key: str + can_persist_approval: bool + description: str + kind: ClassVar[str] = "workflow" + name: str + operation: WorkflowPermissionOperation + phases: list[WorkflowPermissionPhase] + declared_max_ai_credits: float | None = None + declared_max_concurrent_subagents: int | None = None + declared_max_total_subagents: int | None = None + declared_timeout_seconds: float | None = None + managed_approval_required: bool | None = None + max_ai_credits: float | None = None + max_concurrent_subagents: int | None = None + max_total_subagents: int | None = None + timeout_seconds: float | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestWorkflow": + assert isinstance(obj, dict) + approval_key = from_str(obj.get("approvalKey")) + can_persist_approval = from_bool(obj.get("canPersistApproval")) + description = from_str(obj.get("description")) + name = from_str(obj.get("name")) + operation = parse_enum(WorkflowPermissionOperation, obj.get("operation")) + phases = from_list(WorkflowPermissionPhase.from_dict, obj.get("phases")) + declared_max_ai_credits = from_union([from_none, from_float], obj.get("declaredMaxAiCredits")) + declared_max_concurrent_subagents = from_union([from_none, from_int], obj.get("declaredMaxConcurrentSubagents")) + declared_max_total_subagents = from_union([from_none, from_int], obj.get("declaredMaxTotalSubagents")) + declared_timeout_seconds = from_union([from_none, from_float], obj.get("declaredTimeoutSeconds")) + managed_approval_required = from_union([from_none, from_bool], obj.get("managedApprovalRequired")) + max_ai_credits = from_union([from_none, from_float], obj.get("maxAiCredits")) + max_concurrent_subagents = from_union([from_none, from_int], obj.get("maxConcurrentSubagents")) + max_total_subagents = from_union([from_none, from_int], obj.get("maxTotalSubagents")) + timeout_seconds = from_union([from_none, from_float], obj.get("timeoutSeconds")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestWorkflow( + approval_key=approval_key, + can_persist_approval=can_persist_approval, + description=description, + name=name, + operation=operation, + phases=phases, + declared_max_ai_credits=declared_max_ai_credits, + declared_max_concurrent_subagents=declared_max_concurrent_subagents, + declared_max_total_subagents=declared_max_total_subagents, + declared_timeout_seconds=declared_timeout_seconds, + managed_approval_required=managed_approval_required, + max_ai_credits=max_ai_credits, + max_concurrent_subagents=max_concurrent_subagents, + max_total_subagents=max_total_subagents, + timeout_seconds=timeout_seconds, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["approvalKey"] = from_str(self.approval_key) + result["canPersistApproval"] = from_bool(self.can_persist_approval) + result["description"] = from_str(self.description) + result["kind"] = self.kind + result["name"] = from_str(self.name) + result["operation"] = to_enum(WorkflowPermissionOperation, self.operation) + result["phases"] = from_list(lambda x: to_class(WorkflowPermissionPhase, x), self.phases) + if self.declared_max_ai_credits is not None: + result["declaredMaxAiCredits"] = from_union([from_none, to_float], self.declared_max_ai_credits) + if self.declared_max_concurrent_subagents is not None: + result["declaredMaxConcurrentSubagents"] = from_union([from_none, to_int], self.declared_max_concurrent_subagents) + if self.declared_max_total_subagents is not None: + result["declaredMaxTotalSubagents"] = from_union([from_none, to_int], self.declared_max_total_subagents) + if self.declared_timeout_seconds is not None: + result["declaredTimeoutSeconds"] = from_union([from_none, to_float], self.declared_timeout_seconds) + if self.managed_approval_required is not None: + result["managedApprovalRequired"] = from_union([from_none, from_bool], self.managed_approval_required) + if self.max_ai_credits is not None: + result["maxAiCredits"] = from_union([from_none, to_float], self.max_ai_credits) + if self.max_concurrent_subagents is not None: + result["maxConcurrentSubagents"] = from_union([from_none, to_int], self.max_concurrent_subagents) + if self.max_total_subagents is not None: + result["maxTotalSubagents"] = from_union([from_none, to_int], self.max_total_subagents) + if self.timeout_seconds is not None: + result["timeoutSeconds"] = from_union([from_none, to_float], self.timeout_seconds) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + @dataclass class PermissionRequestWrite: "File write permission request" @@ -7838,6 +8220,8 @@ class PermissionRequestWrite: # Experimental: this field is part of an experimental API and may change or be removed. resolved_path: str | None = None tool_call_id: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + sandbox_path_grant: PermissionSandboxPathGrant | None = None @staticmethod def from_dict(obj: Any) -> "PermissionRequestWrite": @@ -7852,6 +8236,7 @@ def from_dict(obj: Any) -> "PermissionRequestWrite": request_sandbox_bypass_reason = from_union([from_none, from_str], obj.get("requestSandboxBypassReason")) resolved_path = from_union([from_none, from_str], obj.get("resolvedPath")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + sandbox_path_grant = from_union([from_none, PermissionSandboxPathGrant.from_dict], obj.get("sandboxPathGrant")) return PermissionRequestWrite( can_offer_session_approval=can_offer_session_approval, diff=diff, @@ -7863,6 +8248,7 @@ def from_dict(obj: Any) -> "PermissionRequestWrite": request_sandbox_bypass_reason=request_sandbox_bypass_reason, resolved_path=resolved_path, tool_call_id=tool_call_id, + sandbox_path_grant=sandbox_path_grant, ) def to_dict(self) -> dict: @@ -7884,6 +8270,8 @@ def to_dict(self) -> dict: result["resolvedPath"] = from_union([from_none, from_str], self.resolved_path) if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + if self.sandbox_path_grant is not None: + result["sandboxPathGrant"] = from_union([from_none, lambda x: to_class(PermissionSandboxPathGrant, x)], self.sandbox_path_grant) return result @@ -8036,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 @@ -8066,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")) @@ -8090,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, @@ -8130,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: @@ -8239,6 +8633,7 @@ class SandboxDecisionData: gh_auth: bool | None = None git_auth: bool | None = None keychain_access: bool | None = None + permissive_source: SandboxPermissiveSource | None = None policy_source: SandboxPolicySource | None = None process_name: str | None = None proxy_mode: SandboxProxyMode | None = None @@ -8272,6 +8667,7 @@ def from_dict(obj: Any) -> "SandboxDecisionData": gh_auth = from_union([from_none, from_bool], obj.get("ghAuth")) git_auth = from_union([from_none, from_bool], obj.get("gitAuth")) keychain_access = from_union([from_none, from_bool], obj.get("keychainAccess")) + permissive_source = from_union([from_none, lambda x: parse_enum(SandboxPermissiveSource, x)], obj.get("permissiveSource")) policy_source = from_union([from_none, lambda x: parse_enum(SandboxPolicySource, x)], obj.get("policySource")) process_name = from_union([from_none, from_str], obj.get("processName")) proxy_mode = from_union([from_none, lambda x: parse_enum(SandboxProxyMode, x)], obj.get("proxyMode")) @@ -8302,6 +8698,7 @@ def from_dict(obj: Any) -> "SandboxDecisionData": gh_auth=gh_auth, git_auth=git_auth, keychain_access=keychain_access, + permissive_source=permissive_source, policy_source=policy_source, process_name=process_name, proxy_mode=proxy_mode, @@ -8352,6 +8749,8 @@ def to_dict(self) -> dict: result["gitAuth"] = from_union([from_none, from_bool], self.git_auth) if self.keychain_access is not None: result["keychainAccess"] = from_union([from_none, from_bool], self.keychain_access) + if self.permissive_source is not None: + result["permissiveSource"] = from_union([from_none, lambda x: to_enum(SandboxPermissiveSource, x)], self.permissive_source) if self.policy_source is not None: result["policySource"] = from_union([from_none, lambda x: to_enum(SandboxPolicySource, x)], self.policy_source) if self.process_name is not None: @@ -8516,6 +8915,8 @@ class SessionCompactionCompleteData: success: bool # Internal: this field is an internal SDK API and is not part of the public surface. _active_factory_summary: str | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _active_workflow_summary: str | None = None behavior_model_id: str | None = None checkpoint_number: int | None = None checkpoint_path: str | None = None @@ -8543,6 +8944,7 @@ def from_dict(obj: Any) -> "SessionCompactionCompleteData": assert isinstance(obj, dict) success = from_bool(obj.get("success")) _active_factory_summary = from_union([from_none, from_str], obj.get("activeFactorySummary")) + _active_workflow_summary = from_union([from_none, from_str], obj.get("activeWorkflowSummary")) behavior_model_id = from_union([from_none, from_str], obj.get("behaviorModelId")) checkpoint_number = from_union([from_none, from_int], obj.get("checkpointNumber")) checkpoint_path = from_union([from_none, from_str], obj.get("checkpointPath")) @@ -8567,6 +8969,7 @@ def from_dict(obj: Any) -> "SessionCompactionCompleteData": return SessionCompactionCompleteData( success=success, _active_factory_summary=_active_factory_summary, + _active_workflow_summary=_active_workflow_summary, behavior_model_id=behavior_model_id, checkpoint_number=checkpoint_number, checkpoint_path=checkpoint_path, @@ -8595,6 +8998,8 @@ def to_dict(self) -> dict: result["success"] = from_bool(self.success) if self._active_factory_summary is not None: result["activeFactorySummary"] = from_union([from_none, from_str], self._active_factory_summary) + if self._active_workflow_summary is not None: + result["activeWorkflowSummary"] = from_union([from_none, from_str], self._active_workflow_summary) if self.behavior_model_id is not None: result["behaviorModelId"] = from_union([from_none, from_str], self.behavior_model_id) if self.checkpoint_number is not None: @@ -9300,26 +9705,36 @@ class SessionMcpServerStatusChangedData: "Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error." server_name: str status: McpServerStatus + config_source: str | None = None error: str | None = None + error_classification: str | None = None @staticmethod def from_dict(obj: Any) -> "SessionMcpServerStatusChangedData": assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) status = parse_enum(McpServerStatus, obj.get("status")) + config_source = from_union([from_none, from_str], obj.get("configSource")) error = from_union([from_none, from_str], obj.get("error")) + error_classification = from_union([from_none, from_str], obj.get("errorClassification")) return SessionMcpServerStatusChangedData( server_name=server_name, status=status, + config_source=config_source, error=error, + error_classification=error_classification, ) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) result["status"] = to_enum(McpServerStatus, self.status) + if self.config_source is not None: + result["configSource"] = from_union([from_none, from_str], self.config_source) if self.error is not None: result["error"] = from_union([from_none, from_str], self.error) + if self.error_classification is not None: + result["errorClassification"] = from_union([from_none, from_str], self.error_classification) return result @@ -9402,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 @@ -9419,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")) @@ -9433,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, @@ -9459,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: @@ -9580,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 @@ -9599,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")) @@ -9615,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, @@ -9641,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: @@ -9934,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 @@ -9955,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")) @@ -9973,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, @@ -10001,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: @@ -11019,6 +11449,7 @@ class SubagentStartedData: parent_id: str | None = None resumable: bool | None = None task_model_source: SubagentTaskModelSource | None = None + workflow_run_id: str | None = None @staticmethod def from_dict(obj: Any) -> "SubagentStartedData": @@ -11035,6 +11466,7 @@ def from_dict(obj: Any) -> "SubagentStartedData": parent_id = from_union([from_none, from_str], obj.get("parentId")) resumable = from_union([from_none, from_bool], obj.get("resumable")) task_model_source = from_union([from_none, lambda x: parse_enum(SubagentTaskModelSource, x)], obj.get("taskModelSource")) + workflow_run_id = from_union([from_none, from_str], obj.get("workflowRunId")) return SubagentStartedData( agent_description=agent_description, agent_display_name=agent_display_name, @@ -11048,6 +11480,7 @@ def from_dict(obj: Any) -> "SubagentStartedData": parent_id=parent_id, resumable=resumable, task_model_source=task_model_source, + workflow_run_id=workflow_run_id, ) def to_dict(self) -> dict: @@ -11072,6 +11505,8 @@ def to_dict(self) -> dict: result["resumable"] = from_union([from_none, from_bool], self.resumable) if self.task_model_source is not None: result["taskModelSource"] = from_union([from_none, lambda x: to_enum(SubagentTaskModelSource, x)], self.task_model_source) + if self.workflow_run_id is not None: + result["workflowRunId"] = from_union([from_none, from_str], self.workflow_run_id) return result @@ -11243,128 +11678,39 @@ def to_dict(self) -> dict: result: dict = {} result["agentId"] = from_str(self.agent_id) result["agentType"] = from_str(self.agent_type) - result["type"] = self.type - if self.description is not None: - result["description"] = from_union([from_none, from_str], self.description) - if self.display_name is not None: - result["displayName"] = from_union([from_none, from_str], self.display_name) - return result - - -@dataclass -class SystemNotificationData: - "System-generated notification for runtime events like background task completion" - content: str - kind: SystemNotification - responses_reasoning: ResponsesReasoning | None = None - - @staticmethod - def from_dict(obj: Any) -> "SystemNotificationData": - assert isinstance(obj, dict) - content = from_str(obj.get("content")) - kind = _load_SystemNotification(obj.get("kind")) - responses_reasoning = from_union([from_none, ResponsesReasoning.from_dict], obj.get("responsesReasoning")) - return SystemNotificationData( - content=content, - kind=kind, - responses_reasoning=responses_reasoning, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["content"] = from_str(self.content) - result["kind"] = self.kind.to_dict() - if self.responses_reasoning is not None: - result["responsesReasoning"] = from_union([from_none, lambda x: to_class(ResponsesReasoning, x)], self.responses_reasoning) - return result - - -@dataclass -class SystemNotificationFactoryCompleted: - "System notification metadata for a factory execution attempt that reached a terminal state." - attempt: int - consumed_nano_aiu: int - consumed_subagents: int - elapsed_ms: int - factory_name: str - run_id: str - status: SystemNotificationFactoryCompletedStatus - type: ClassVar[str] = "factory_completed" - failure: Any = None - pause_info: SystemNotificationFactoryPauseInfo | None = None - result_preview: str | None = None - retry_guidance: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "SystemNotificationFactoryCompleted": - assert isinstance(obj, dict) - attempt = from_int(obj.get("attempt")) - consumed_nano_aiu = from_int(obj.get("consumedNanoAiu")) - consumed_subagents = from_int(obj.get("consumedSubagents")) - elapsed_ms = from_int(obj.get("elapsedMs")) - factory_name = from_str(obj.get("factoryName")) - run_id = from_str(obj.get("runId")) - status = parse_enum(SystemNotificationFactoryCompletedStatus, obj.get("status")) - failure = obj.get("failure") - pause_info = from_union([from_none, SystemNotificationFactoryPauseInfo.from_dict], obj.get("pauseInfo")) - result_preview = from_union([from_none, from_str], obj.get("resultPreview")) - retry_guidance = from_union([from_none, from_str], obj.get("retryGuidance")) - return SystemNotificationFactoryCompleted( - attempt=attempt, - consumed_nano_aiu=consumed_nano_aiu, - consumed_subagents=consumed_subagents, - elapsed_ms=elapsed_ms, - factory_name=factory_name, - run_id=run_id, - status=status, - failure=failure, - pause_info=pause_info, - result_preview=result_preview, - retry_guidance=retry_guidance, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["attempt"] = to_int(self.attempt) - result["consumedNanoAiu"] = to_int(self.consumed_nano_aiu) - result["consumedSubagents"] = to_int(self.consumed_subagents) - result["elapsedMs"] = to_int(self.elapsed_ms) - result["factoryName"] = from_str(self.factory_name) - result["runId"] = from_str(self.run_id) - result["status"] = to_enum(SystemNotificationFactoryCompletedStatus, self.status) - result["type"] = self.type - if self.failure is not None: - result["failure"] = self.failure - if self.pause_info is not None: - result["pauseInfo"] = from_union([from_none, lambda x: to_class(SystemNotificationFactoryPauseInfo, x)], self.pause_info) - if self.result_preview is not None: - result["resultPreview"] = from_union([from_none, from_str], self.result_preview) - if self.retry_guidance is not None: - result["retryGuidance"] = from_union([from_none, from_str], self.retry_guidance) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + if self.display_name is not None: + result["displayName"] = from_union([from_none, from_str], self.display_name) return result @dataclass -class SystemNotificationFactoryPauseInfo: - "Durable metadata describing who initiated a factory pause." - type: SystemNotificationFactoryPauseInfoType - key: str | None = None +class SystemNotificationData: + "System-generated notification for runtime events like background task completion" + content: str + kind: SystemNotification + responses_reasoning: ResponsesReasoning | None = None @staticmethod - def from_dict(obj: Any) -> "SystemNotificationFactoryPauseInfo": + def from_dict(obj: Any) -> "SystemNotificationData": assert isinstance(obj, dict) - type = parse_enum(SystemNotificationFactoryPauseInfoType, obj.get("type")) - key = from_union([from_none, from_str], obj.get("key")) - return SystemNotificationFactoryPauseInfo( - type=type, - key=key, + content = from_str(obj.get("content")) + kind = _load_SystemNotification(obj.get("kind")) + responses_reasoning = from_union([from_none, ResponsesReasoning.from_dict], obj.get("responsesReasoning")) + return SystemNotificationData( + content=content, + kind=kind, + responses_reasoning=responses_reasoning, ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(SystemNotificationFactoryPauseInfoType, self.type) - if self.key is not None: - result["key"] = from_union([from_none, from_str], self.key) + result["content"] = from_str(self.content) + result["kind"] = self.kind.to_dict() + if self.responses_reasoning is not None: + result["responsesReasoning"] = from_union([from_none, lambda x: to_class(ResponsesReasoning, x)], self.responses_reasoning) return result @@ -11514,6 +11860,95 @@ def to_dict(self) -> dict: return result +@dataclass +class SystemNotificationWorkflowCompleted: + "System notification metadata for a workflow execution attempt that reached a terminal state." + attempt: int + consumed_nano_aiu: int + consumed_subagents: int + elapsed_ms: int + run_id: str + status: SystemNotificationWorkflowCompletedStatus + type: ClassVar[str] = "workflow_completed" + workflow_name: str + failure: Any = None + pause_info: SystemNotificationWorkflowPauseInfo | None = None + result_preview: str | None = None + retry_guidance: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationWorkflowCompleted": + assert isinstance(obj, dict) + attempt = from_int(obj.get("attempt")) + consumed_nano_aiu = from_int(obj.get("consumedNanoAiu")) + consumed_subagents = from_int(obj.get("consumedSubagents")) + elapsed_ms = from_int(obj.get("elapsedMs")) + run_id = from_str(obj.get("runId")) + status = parse_enum(SystemNotificationWorkflowCompletedStatus, obj.get("status")) + workflow_name = from_str(obj.get("workflowName")) + failure = obj.get("failure") + pause_info = from_union([from_none, SystemNotificationWorkflowPauseInfo.from_dict], obj.get("pauseInfo")) + result_preview = from_union([from_none, from_str], obj.get("resultPreview")) + retry_guidance = from_union([from_none, from_str], obj.get("retryGuidance")) + return SystemNotificationWorkflowCompleted( + attempt=attempt, + consumed_nano_aiu=consumed_nano_aiu, + consumed_subagents=consumed_subagents, + elapsed_ms=elapsed_ms, + run_id=run_id, + status=status, + workflow_name=workflow_name, + failure=failure, + pause_info=pause_info, + result_preview=result_preview, + retry_guidance=retry_guidance, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["attempt"] = to_int(self.attempt) + result["consumedNanoAiu"] = to_int(self.consumed_nano_aiu) + result["consumedSubagents"] = to_int(self.consumed_subagents) + result["elapsedMs"] = to_int(self.elapsed_ms) + result["runId"] = from_str(self.run_id) + result["status"] = to_enum(SystemNotificationWorkflowCompletedStatus, self.status) + result["type"] = self.type + result["workflowName"] = from_str(self.workflow_name) + if self.failure is not None: + result["failure"] = self.failure + if self.pause_info is not None: + result["pauseInfo"] = from_union([from_none, lambda x: to_class(SystemNotificationWorkflowPauseInfo, x)], self.pause_info) + if self.result_preview is not None: + result["resultPreview"] = from_union([from_none, from_str], self.result_preview) + if self.retry_guidance is not None: + result["retryGuidance"] = from_union([from_none, from_str], self.retry_guidance) + return result + + +@dataclass +class SystemNotificationWorkflowPauseInfo: + "Durable metadata describing who initiated a workflow pause." + type: SystemNotificationWorkflowPauseInfoType + key: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationWorkflowPauseInfo": + assert isinstance(obj, dict) + type = parse_enum(SystemNotificationWorkflowPauseInfoType, obj.get("type")) + key = from_union([from_none, from_str], obj.get("key")) + return SystemNotificationWorkflowPauseInfo( + type=type, + key=key, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(SystemNotificationWorkflowPauseInfoType, self.type) + if self.key is not None: + result["key"] = from_union([from_none, from_str], self.key) + return result + + @dataclass class ToolExecutionCompleteContentAudio: "Audio content block with base64-encoded data" @@ -11742,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 @@ -11765,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")) @@ -11782,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, @@ -11803,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: @@ -12203,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 @@ -12475,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" @@ -12792,28 +13248,6 @@ def to_dict(self) -> dict: return result -@dataclass -class UserToolSessionApprovalFactory: - "Session-scoped factory approval, optionally narrowed by approval key." - kind: ClassVar[str] = "factory" - approval_key: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "UserToolSessionApprovalFactory": - assert isinstance(obj, dict) - approval_key = from_union([from_none, from_str], obj.get("approvalKey")) - return UserToolSessionApprovalFactory( - approval_key=approval_key, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = self.kind - if self.approval_key is not None: - result["approvalKey"] = from_union([from_none, from_str], self.approval_key) - return result - - @dataclass class UserToolSessionApprovalMcp: "Session-scoped tool-approval rule for an MCP server tool, or all tools on the server when `toolName` is null." @@ -12873,6 +13307,28 @@ def to_dict(self) -> dict: return result +@dataclass +class UserToolSessionApprovalWorkflow: + "Session-scoped workflow approval, optionally narrowed by approval key." + kind: ClassVar[str] = "workflow" + approval_key: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalWorkflow": + assert isinstance(obj, dict) + approval_key = from_union([from_none, from_str], obj.get("approvalKey")) + return UserToolSessionApprovalWorkflow( + approval_key=approval_key, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.approval_key is not None: + result["approvalKey"] = from_union([from_none, from_str], self.approval_key) + return result + + @dataclass class UserToolSessionApprovalWrite: "Session-scoped tool-approval rule for filesystem write operations." @@ -12890,6 +13346,30 @@ def to_dict(self) -> dict: return result +@dataclass +class WorkflowPermissionPhase: + "A declared phase shown in a workflow permission prompt." + title: str + detail: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "WorkflowPermissionPhase": + assert isinstance(obj, dict) + title = from_str(obj.get("title")) + detail = from_union([from_none, from_str], obj.get("detail")) + return WorkflowPermissionPhase( + title=title, + detail=detail, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["title"] = from_str(self.title) + if self.detail is not None: + result["detail"] = from_union([from_none, from_str], self.detail) + return result + + @dataclass class WorkingDirectoryContext: "Working directory and git context at session start" @@ -12995,7 +13475,7 @@ def _load_PermissionPromptRequest(obj: Any) -> "PermissionPromptRequest": case "path": return PermissionPromptRequestPath.from_dict(obj) case "hook": return PermissionPromptRequestHook.from_dict(obj) case "extension-management": return PermissionPromptRequestExtensionManagement.from_dict(obj) - case "factory": return PermissionPromptRequestFactory.from_dict(obj) + case "workflow": return PermissionPromptRequestWorkflow.from_dict(obj) case "extension-permission-access": return PermissionPromptRequestExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionPromptRequestExtensionEnvAccess.from_dict(obj) case _: raise ValueError(f"Unknown PermissionPromptRequest kind: {kind!r}") @@ -13014,7 +13494,7 @@ def _load_PermissionRequest(obj: Any) -> "PermissionRequest": case "custom-tool": return PermissionRequestCustomTool.from_dict(obj) case "hook": return PermissionRequestHook.from_dict(obj) case "extension-management": return PermissionRequestExtensionManagement.from_dict(obj) - case "factory": return PermissionRequestFactory.from_dict(obj) + case "workflow": return PermissionRequestWorkflow.from_dict(obj) case "extension-permission-access": return PermissionRequestExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return PermissionRequestExtensionEnvAccess.from_dict(obj) case _: raise ValueError(f"Unknown PermissionRequest kind: {kind!r}") @@ -13025,6 +13505,7 @@ def _load_PermissionResult(obj: Any) -> "PermissionResult": kind = obj.get("kind") match kind: case "approved": return PermissionApproved.from_dict(obj) + case "approved-read-only-for-session": return PermissionApprovedReadOnlyForSession.from_dict(obj) case "approved-for-session": return PermissionApprovedForSession.from_dict(obj) case "approved-for-location": return PermissionApprovedForLocation.from_dict(obj) case "cancelled": return PermissionCancelled.from_dict(obj) @@ -13046,7 +13527,7 @@ def _load_SystemNotification(obj: Any) -> "SystemNotification": case "shell_completed": return SystemNotificationShellCompleted.from_dict(obj) case "shell_detached_completed": return SystemNotificationShellDetachedCompleted.from_dict(obj) case "instruction_discovered": return SystemNotificationInstructionDiscovered.from_dict(obj) - case "factory_completed": return SystemNotificationFactoryCompleted.from_dict(obj) + case "workflow_completed": return SystemNotificationWorkflowCompleted.from_dict(obj) case "unclassified": return SystemNotificationUnclassified.from_dict(obj) case _: raise ValueError(f"Unknown SystemNotification type: {kind!r}") @@ -13076,7 +13557,7 @@ def _load_UserToolSessionApproval(obj: Any) -> "UserToolSessionApproval": case "memory": return UserToolSessionApprovalMemory.from_dict(obj) case "custom-tool": return UserToolSessionApprovalCustomTool.from_dict(obj) case "extension-management": return UserToolSessionApprovalExtensionManagement.from_dict(obj) - case "factory": return UserToolSessionApprovalFactory.from_dict(obj) + case "workflow": return UserToolSessionApprovalWorkflow.from_dict(obj) case "extension-permission-access": return UserToolSessionApprovalExtensionPermissionAccess.from_dict(obj) case "extension-env-access": return UserToolSessionApprovalExtensionEnvAccess.from_dict(obj) case _: raise ValueError(f"Unknown UserToolSessionApproval kind: {kind!r}") @@ -13095,11 +13576,11 @@ def _load_UserToolSessionApproval(obj: Any) -> "UserToolSessionApproval": # Derived user-facing permission prompt details for UI consumers -PermissionPromptRequest = PermissionPromptRequestCommands | PermissionPromptRequestWrite | PermissionPromptRequestRead | PermissionPromptRequestMcp | PermissionPromptRequestUrl | PermissionPromptRequestMemory | PermissionPromptRequestCustomTool | PermissionPromptRequestPath | PermissionPromptRequestHook | PermissionPromptRequestExtensionManagement | PermissionPromptRequestFactory | PermissionPromptRequestExtensionPermissionAccess | PermissionPromptRequestExtensionEnvAccess +PermissionPromptRequest = PermissionPromptRequestCommands | PermissionPromptRequestWrite | PermissionPromptRequestRead | PermissionPromptRequestMcp | PermissionPromptRequestUrl | PermissionPromptRequestMemory | PermissionPromptRequestCustomTool | PermissionPromptRequestPath | PermissionPromptRequestHook | PermissionPromptRequestExtensionManagement | PermissionPromptRequestWorkflow | PermissionPromptRequestExtensionPermissionAccess | PermissionPromptRequestExtensionEnvAccess # Details of the permission being requested -PermissionRequest = PermissionRequestShell | PermissionRequestWrite | PermissionRequestRead | PermissionRequestMcp | PermissionRequestUrl | PermissionRequestMemory | PermissionRequestCustomTool | PermissionRequestHook | PermissionRequestExtensionManagement | PermissionRequestFactory | PermissionRequestExtensionPermissionAccess | PermissionRequestExtensionEnvAccess +PermissionRequest = PermissionRequestShell | PermissionRequestWrite | PermissionRequestRead | PermissionRequestMcp | PermissionRequestUrl | PermissionRequestMemory | PermissionRequestCustomTool | PermissionRequestHook | PermissionRequestExtensionManagement | PermissionRequestWorkflow | PermissionRequestExtensionPermissionAccess | PermissionRequestExtensionEnvAccess # Location within a cited source (character, page, or content-block range) that supports a span. @@ -13107,11 +13588,11 @@ def _load_UserToolSessionApproval(obj: Any) -> "UserToolSessionApproval": # Structured metadata identifying what triggered this notification -SystemNotification = SystemNotificationAgentCompleted | SystemNotificationAgentIdle | SystemNotificationNewInboxMessage | SystemNotificationShellCompleted | SystemNotificationShellDetachedCompleted | SystemNotificationInstructionDiscovered | SystemNotificationFactoryCompleted | SystemNotificationUnclassified +SystemNotification = SystemNotificationAgentCompleted | SystemNotificationAgentIdle | SystemNotificationNewInboxMessage | SystemNotificationShellCompleted | SystemNotificationShellDetachedCompleted | SystemNotificationInstructionDiscovered | SystemNotificationWorkflowCompleted | SystemNotificationUnclassified # The approval to add as a session-scoped rule -UserToolSessionApproval = UserToolSessionApprovalCommands | UserToolSessionApprovalRead | UserToolSessionApprovalWrite | UserToolSessionApprovalMcp | UserToolSessionApprovalMemory | UserToolSessionApprovalCustomTool | UserToolSessionApprovalExtensionManagement | UserToolSessionApprovalFactory | UserToolSessionApprovalExtensionPermissionAccess | UserToolSessionApprovalExtensionEnvAccess +UserToolSessionApproval = UserToolSessionApprovalCommands | UserToolSessionApprovalRead | UserToolSessionApprovalWrite | UserToolSessionApprovalMcp | UserToolSessionApprovalMemory | UserToolSessionApprovalCustomTool | UserToolSessionApprovalExtensionManagement | UserToolSessionApprovalWorkflow | UserToolSessionApprovalExtensionPermissionAccess | UserToolSessionApprovalExtensionEnvAccess # The embedded resource contents, either text or base64-encoded binary @@ -13119,7 +13600,7 @@ def _load_UserToolSessionApproval(obj: Any) -> "UserToolSessionApproval": # The result of the permission request -PermissionResult = PermissionApproved | PermissionApprovedForSession | PermissionApprovedForLocation | PermissionCancelled | PermissionDeniedByRules | PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDeniedInteractivelyByUser | PermissionDeniedByContentExclusionPolicy | PermissionDeniedByPermissionRequestHook +PermissionResult = PermissionApproved | PermissionApprovedReadOnlyForSession | PermissionApprovedForSession | PermissionApprovedForLocation | PermissionCancelled | PermissionDeniedByRules | PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDeniedInteractivelyByUser | PermissionDeniedByContentExclusionPolicy | PermissionDeniedByPermissionRequestHook # Experimental: this enum is part of an experimental API and may change or be removed. @@ -13253,10 +13734,10 @@ class FusionTurnKind(Enum): # Experimental: this enum is part of an experimental API and may change or be removed. class PermissionMessageAuthorizationPolarity(Enum): - "Which direction a message-backed authorization claim moves authority in." - # The human's words authorized an effect. + "Direction stored in a historical extractor claim. Current runtimes do not apply it." + # Historical claim recorded as a grant. GRANT = "grant" - # The human's words refused an effect. + # Historical claim recorded as a denial. DENIAL = "denial" @@ -13278,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." @@ -13558,28 +14059,6 @@ class ExtensionsLoadedExtensionStatus(Enum): STARTING = "starting" -class FactoryPermissionOperation(Enum): - "Operation gated by a factory permission request." - # Running a registered factory, which spends subagents, active time, and AI credits under the approved limits. - RUN = "run" - # Authoring a factory, which writes JavaScript into a session-scoped extension and loads it. - AUTHOR = "author" - - -class FactoryRunSettledStatus(Enum): - "Terminal status a factory run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent." - # The factory body resolved and its result was committed. - COMPLETED = "completed" - # The run was stopped by a limit, an approval refusal or another policy decision. - HALTED = "halted" - # The attempt paused intentionally while preserving resumable run state. - PAUSED = "paused" - # The run was cancelled by its caller or by session disposal. - CANCELLED = "cancelled" - # The run failed, with `failureType` carrying the class when it has one. - ERROR = "error" - - class HandoffSourceType(Enum): "Origin type of the session being handed off" # The handoff originated from a remote session. @@ -13588,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. @@ -13739,7 +14235,7 @@ class McpOauthRequestReason(Enum): class McpServerSource(Enum): - "Configuration source: user, workspace, plugin, builtin, or managed" + "Configuration source: user, workspace, plugin, builtin, managed, or account" # Server configured in the user's global MCP configuration. USER = "user" # Server configured by the current workspace. @@ -13750,6 +14246,8 @@ class McpServerSource(Enum): BUILTIN = "builtin" # Server supplied by a trusted host-managed catalog. MANAGED = "managed" + # Server contributed by a signed-in account; enablement and organization policy still apply. + ACCOUNT = "account" class McpServerStatus(Enum): @@ -13764,7 +14262,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" @@ -13816,6 +14314,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. @@ -13854,6 +14372,8 @@ class ModelChangeSource(Enum): CHANGEBOARDING_SHORTCUT = "changeboarding_shortcut" # An SDK or RPC caller selected the model. SDK = "sdk" + # The user accepted a CAPI-issued Auto tier recommendation. + AUTO_TIER_RECOMMENDATION = "auto_tier_recommendation" class ModelDeselectedReason(Enum): @@ -13926,6 +14446,8 @@ class PermissionApprovalEvaluationReasonCode(Enum): ACTION_TOO_LONG = "action-too-long" # The script path was not authorized for inspection. PATH_NOT_AUTHORIZED = "path-not-authorized" + # A code source was excluded from review by content exclusion policy. + CONTENT_EXCLUDED = "content-excluded" # The script working directory was invalid. INVALID_WORKING_DIRECTORY = "invalid-working-directory" # The script snapshot could not be read. @@ -13950,6 +14472,18 @@ class PermissionApprovalEvaluationReasonCode(Enum): UNREVIEWABLE_SCRIPT_INVOCATION = "unreviewable-script-invocation" # The script argument binding could not be reviewed. ARGUMENT_BINDING_UNREVIEWABLE = "argument-binding-unreviewable" + # The shell command could not be analyzed for execution evidence. + UNSUPPORTED_COMMAND_SHAPE = "unsupported-command-shape" + # The shell command used a code source that cannot be bound for review. + UNSUPPORTED_SOURCE = "unsupported-source" + # The shell command used a code source computed at run time. + DYNAMIC_SOURCE = "dynamic-source" + # The shell command referenced more code sources than can be reviewed. + TOO_MANY_SOURCES = "too-many-sources" + # A code-bearing executable could not be inspected. + EXECUTABLE_UNAVAILABLE = "executable-unavailable" + # A code-bearing executable exceeded the binding size limit. + EXECUTABLE_TOO_LARGE = "executable-too-large" # The script review metadata was malformed. MALFORMED_SCRIPT_ACTION_REVIEW = "malformed-script-action-review" # The script snapshot manifest was malformed. @@ -13963,6 +14497,10 @@ class PermissionApprovalEvaluationReasonCode(Enum): # The request inherited an outcome from another decision. INHERITED = "inherited" + @classmethod + def _missing_(cls, value: object) -> "PermissionApprovalEvaluationReasonCode | None": + return cls.UNKNOWN if isinstance(value, str) else None + class PermissionDecisionSource(Enum): "Controlled reason or actor responsible for a permission response." @@ -13974,7 +14512,7 @@ class PermissionDecisionSource(Enum): HOST_POLICY = "host_policy" # The host denied the request because no interactive user response was available. UNATTENDED_FALLBACK = "unattended_fallback" - # A live authorization record from an earlier human decision in this session contained the proposal, so it ran without another prompt. This is not a new human decision and never mints authority of its own. + # Historical compatibility value for sessions created while authorization carry-forward was executable. Current runtimes do not produce this source. AUTHORIZATION_CARRY_FORWARD = "authorization_carry_forward" @@ -14094,6 +14632,14 @@ class PermissionRequestMemoryScope(Enum): USER = "user" +class PermissionSandboxPathGrantAccess(Enum): + "Access a sandbox path grant confers" + # Read access: the path is added to readonlyPaths. + READ = "read" + # Read and write access: the path is added to readwritePaths. + READ_WRITE = "readWrite" + + class PersistedBinaryImageType(Enum): "Binary result type discriminator. Use \"image\" for images and \"resource\" for other binary data." # Binary image data. @@ -14202,6 +14748,7 @@ class SandboxDecisionDataKind(Enum): SPAWN_COMPLETED = "spawn_completed" ENFORCEMENT_STATE = "enforcement_state" ACCESS_DENIED = "access_denied" + ACCESS_RECORDED = "access_recorded" BYPASS_DECIDED = "bypass_decided" PERMISSIVE_RETRY_DECIDED = "permissive_retry_decided" PERMISSIVE_RETRY_COMPLETED = "permissive_retry_completed" @@ -14274,7 +14821,7 @@ class SandboxEnforcementPoint(Enum): class SandboxOutcome(Enum): - "Finite result of a sandbox decision. Each `SandboxDecisionData` variant uses a disjoint subset: `policy_resolved` is `resolved | degraded`, `spawn_completed` and `permissive_retry_completed` are `succeeded | failed`, `enforcement_state` is `engaged | inactive | failed`, `access_denied` is `denied`, and escalation decisions are `approved | declined`." + "Finite result of a sandbox decision. Each `SandboxDecisionData` variant uses a disjoint subset: `policy_resolved` is `resolved | degraded`, `spawn_completed` and `permissive_retry_completed` are `succeeded | failed`, `enforcement_state` is `engaged | inactive | failed`, `access_denied` is `denied`, `access_recorded` is `allowed`, and escalation decisions are `approved | declined`." # The sandbox policy resolved successfully. Describes configuration only and makes no claim that a backend engaged. RESOLVED = "resolved" # No sandbox governed the workload. @@ -14293,6 +14840,16 @@ class SandboxOutcome(Enum): APPROVED = "approved" # A request to run outside the process sandbox was not granted. DECLINED = "declined" + # Permissive learning mode recorded an access the enforced policy would have refused, and allowed it. + ALLOWED = "allowed" + + +class SandboxPermissiveSource(Enum): + "Why a sandboxed run recorded and allowed its process-container access checks instead of enforcing them." + # A person approved the permissive retry for this command after a sandboxed attempt was blocked. + APPROVED_RETRY = "approved_retry" + # Device-managed policy (`sandbox.learningMode: "allow"`) starts sandboxed shell commands in permissive learning mode. + POLICY = "policy" class SandboxPlatform(Enum): @@ -14447,22 +15004,22 @@ class SystemNotificationAgentCompletedStatus(Enum): FAILED = "failed" -class SystemNotificationFactoryCompletedStatus(Enum): - "Terminal status reached by a factory execution attempt." - # The factory completed successfully. +class SystemNotificationWorkflowCompletedStatus(Enum): + "Terminal status reached by a workflow execution attempt." + # The workflow completed successfully. COMPLETED = "completed" - # The factory was halted. + # The workflow was halted. HALTED = "halted" - # The factory attempt paused intentionally. + # The workflow attempt paused intentionally. PAUSED = "paused" - # The factory was cancelled. + # The workflow was cancelled. CANCELLED = "cancelled" - # The factory failed. + # The workflow failed. ERROR = "error" -class SystemNotificationFactoryPauseInfoType(Enum): - "Durable metadata describing who initiated a factory pause. discriminator" +class SystemNotificationWorkflowPauseInfoType(Enum): + "Durable metadata describing who initiated a workflow pause. discriminator" USER = "user" CHECKPOINT = "checkpoint" @@ -14507,6 +15064,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. @@ -14539,6 +15106,28 @@ class Verbosity(Enum): HIGH = "high" +class WorkflowPermissionOperation(Enum): + "Operation gated by a workflow permission request." + # Running a registered workflow, which spends subagents, active time, and AI credits under the approved limits. + RUN = "run" + # Authoring a workflow, which writes JavaScript into a session-scoped extension and loads it. + AUTHOR = "author" + + +class WorkflowRunSettledStatus(Enum): + "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." + # The workflow body resolved and its result was committed. + COMPLETED = "completed" + # The run was stopped by a limit, an approval refusal or another policy decision. + HALTED = "halted" + # The attempt paused intentionally while preserving resumable run state. + PAUSED = "paused" + # The run was cancelled by its caller or by session disposal. + CANCELLED = "cancelled" + # The run failed, with `failureType` carrying the class when it has one. + ERROR = "error" + + class WorkingDirectoryContextHostType(Enum): "Hosting platform type of the repository (github or ado)" # Repository is hosted on GitHub. @@ -14555,7 +15144,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 | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | 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 @@ -14645,12 +15234,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) @@ -14707,11 +15298,12 @@ 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.FACTORY_RUN_UPDATED: data = FactoryRunUpdatedData.from_dict(data_obj) - case SessionEventType.FACTORY_RUN_STARTED: data = FactoryRunStartedData.from_dict(data_obj) - case SessionEventType.FACTORY_RUN_SETTLED: data = FactoryRunSettledData.from_dict(data_obj) + case SessionEventType.WORKFLOW_RUN_UPDATED: data = WorkflowRunUpdatedData.from_dict(data_obj) + case SessionEventType.WORKFLOW_RUN_STARTED: data = WorkflowRunStartedData.from_dict(data_obj) + case SessionEventType.WORKFLOW_RUN_SETTLED: data = WorkflowRunSettledData.from_dict(data_obj) case SessionEventType.SESSION_SKILLS_LOADED: data = SessionSkillsLoadedData.from_dict(data_obj) case SessionEventType.SESSION_CUSTOM_AGENTS_UPDATED: data = SessionCustomAgentsUpdatedData.from_dict(data_obj) case SessionEventType.SESSION_MCP_SERVERS_LOADED: data = SessionMcpServersLoadedData.from_dict(data_obj) @@ -14878,14 +15470,9 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ExtensionsLoadedExtensionStatus", "ExternalToolCompletedData", "ExternalToolRequestedData", - "FactoryPermissionOperation", - "FactoryPermissionPhase", - "FactoryRunSettledData", - "FactoryRunSettledStatus", - "FactoryRunStartedData", - "FactoryRunUpdatedData", "FusionAttribution", "FusionConversationScope", + "FusionCritic", "FusionFollowUpAction", "FusionFollowUpRecommendation", "FusionPattern", @@ -14905,6 +15492,10 @@ def session_event_to_dict(x: SessionEvent) -> Any: "HookEndError", "HookProgressData", "HookStartData", + "HumanResponseActor", + "HumanResponseRecordedData", + "HumanResponseRecordedResponse", + "HumanResponseRecordedResponseResponseKind", "IndexedSearchDisabledReason", "IndexedSearchErrorType", "IndexedSearchIncrementalPhase", @@ -14942,6 +15533,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ModelCallFailureRequestFingerprint", "ModelCallFailureSource", "ModelCallFailureTransport", + "ModelCallFinalResult", + "ModelCallFinalResultData", "ModelCallFinishedData", "ModelCallFinishedOutcome", "ModelCallStartData", @@ -14958,6 +15551,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PermissionApproved", "PermissionApprovedForLocation", "PermissionApprovedForSession", + "PermissionApprovedReadOnlyForSession", "PermissionAssentDetectedData", "PermissionAssistedApproval", "PermissionCancelled", @@ -14981,7 +15575,6 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PermissionPromptRequestExtensionEnvAccess", "PermissionPromptRequestExtensionManagement", "PermissionPromptRequestExtensionPermissionAccess", - "PermissionPromptRequestFactory", "PermissionPromptRequestHook", "PermissionPromptRequestMcp", "PermissionPromptRequestMemory", @@ -14989,6 +15582,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PermissionPromptRequestPathAccessKind", "PermissionPromptRequestRead", "PermissionPromptRequestUrl", + "PermissionPromptRequestWorkflow", "PermissionPromptRequestWrite", "PermissionRecommendation", "PermissionRecoveryAttempt", @@ -15004,7 +15598,6 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PermissionRequestExtensionEnvAccess", "PermissionRequestExtensionManagement", "PermissionRequestExtensionPermissionAccess", - "PermissionRequestFactory", "PermissionRequestHook", "PermissionRequestMcp", "PermissionRequestMemory", @@ -15017,10 +15610,13 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PermissionRequestShellCommandSegment", "PermissionRequestShellPossibleUrl", "PermissionRequestUrl", + "PermissionRequestWorkflow", "PermissionRequestWrite", "PermissionRequestedData", "PermissionResult", "PermissionRule", + "PermissionSandboxPathGrant", + "PermissionSandboxPathGrantAccess", "PersistedBinaryImage", "PersistedBinaryImageType", "PersistedBinaryResult", @@ -15045,6 +15641,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SandboxEnforcementPoint", "SandboxFilesystemPolicyDetails", "SandboxOutcome", + "SandboxPermissiveSource", "SandboxPlatform", "SandboxPolicySource", "SandboxProxyMode", @@ -15153,15 +15750,15 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SystemNotificationAgentCompletedStatus", "SystemNotificationAgentIdle", "SystemNotificationData", - "SystemNotificationFactoryCompleted", - "SystemNotificationFactoryCompletedStatus", - "SystemNotificationFactoryPauseInfo", - "SystemNotificationFactoryPauseInfoType", "SystemNotificationInstructionDiscovered", "SystemNotificationNewInboxMessage", "SystemNotificationShellCompleted", "SystemNotificationShellDetachedCompleted", "SystemNotificationUnclassified", + "SystemNotificationWorkflowCompleted", + "SystemNotificationWorkflowCompletedStatus", + "SystemNotificationWorkflowPauseInfo", + "SystemNotificationWorkflowPauseInfoType", "TaskBlocker", "TaskBlockerKind", "TaskCompletionOutcome", @@ -15178,6 +15775,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ToolExecutionCompleteContentText", "ToolExecutionCompleteData", "ToolExecutionCompleteError", + "ToolExecutionCompleteFileEdit", + "ToolExecutionCompleteFileEditKind", "ToolExecutionCompleteResult", "ToolExecutionCompleteShellExecution", "ToolExecutionCompleteToolDescription", @@ -15202,6 +15801,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ToolExecutionStartToolDescriptionMetaUI", "ToolExecutionStartToolDescriptionMetaUIVisibility", "ToolSearchActivatedData", + "ToolShellOutputData", + "ToolShellOutputStream", "ToolUserRequestedData", "UIEphemeralQueryPhase", "UiEphemeralQueryData", @@ -15216,12 +15817,18 @@ def session_event_to_dict(x: SessionEvent) -> Any: "UserToolSessionApprovalExtensionEnvAccess", "UserToolSessionApprovalExtensionManagement", "UserToolSessionApprovalExtensionPermissionAccess", - "UserToolSessionApprovalFactory", "UserToolSessionApprovalMcp", "UserToolSessionApprovalMemory", "UserToolSessionApprovalRead", + "UserToolSessionApprovalWorkflow", "UserToolSessionApprovalWrite", "Verbosity", + "WorkflowPermissionOperation", + "WorkflowPermissionPhase", + "WorkflowRunSettledData", + "WorkflowRunSettledStatus", + "WorkflowRunStartedData", + "WorkflowRunUpdatedData", "WorkingDirectoryContext", "WorkingDirectoryContextHostType", "WorkspaceFileChangedOperation", diff --git a/python/copilot/host.py b/python/copilot/host.py new file mode 100644 index 0000000000..1a64122c02 --- /dev/null +++ b/python/copilot/host.py @@ -0,0 +1,414 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +"""Experimental, connection-owned in-process AHP hosting.""" + +from __future__ import annotations + +import asyncio +import copy +import inspect +import logging +import types +from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Union, get_args, get_origin, get_type_hints, is_typeddict +from uuid import uuid4 +from weakref import WeakKeyDictionary + +from .generated.rpc import ( + HostExitedNotification, + HostGitHubEnvironmentOptions, + HostLocalServerOptions, + HostPublishSessionResult, + HostStartResult, +) + +if TYPE_CHECKING: + from ._jsonrpc import JsonRpcClient + from .session import CopilotSession + +logger = logging.getLogger(__name__) +AhpHostExit = HostExitedNotification + + +class _HandoffEnded(RuntimeError): + pass + + +@dataclass(frozen=True) +class AhpSessionCreateRequest: + """Preserve ``config`` when calling ``client.create_session(**config)``. + + Add application prompts, tools and callbacks. ``cancellation_event`` is set + when participation ends; the SDK still releases any session returned late. + """ + + config: dict[str, Any] + cancellation_event: asyncio.Event + + +@dataclass(frozen=True) +class AhpSessionResumeRequest: + """Resume ``session_id`` with ``**config``, or return its retained original.""" + + session_id: str + config: dict[str, Any] + cancellation_event: asyncio.Event + + +@dataclass(frozen=True) +class AhpHost: + """An experimental AHP listener owned by the client's current connection. + + Disposal joins runtime cleanup without destroying application sessions. + Reconnecting the client does not transfer this handle to its new connection. + """ + + host_id: 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: + """Stop and join the listener, including on repeated/concurrent calls.""" + await self._rpc.request("host.dispose", {"hostId": self.host_id}) + + async def publish_session(self, session_id: str) -> HostPublishSessionResult: + """Publish a resident session attached to the owning client.""" + result = await self._rpc.request( + "host.publishSession", {"hostId": self.host_id, "sessionId": session_id} + ) + return HostPublishSessionResult.from_dict(result) + + async def __aenter__(self) -> AhpHost: + return self + + async def __aexit__(self, *_: object) -> None: + await self.dispose() + + +@dataclass(frozen=True) +class AhpHostOptions: + """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. + """ + + 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 + on_exit: Callable[[AhpHostExit], Awaitable[None] | None] | None = None + + +@dataclass +class _Handoff: + host_id: str + session_id: str + options: AhpHostOptions + cancellation: asyncio.Event = field(default_factory=asyncio.Event) + session: CopilotSession | None = None + released: bool = False + configs: WeakKeyDictionary[CopilotSession, dict[str, Any]] = field( + default_factory=WeakKeyDictionary + ) + + +def _config_key(name: str, hints: dict[str, Any]) -> str: + # SDK names normalize acronyms differently from wire names (OAuth, GitHub). + return next((field for field in hints if field.replace("_", "").lower() == name.lower()), name) + + +def _config_value(value: Any, annotation: Any) -> Any: + """Translate typed config fields, preserving arbitrary dictionary keys.""" + origin = get_origin(annotation) + if origin in (Union, types.UnionType): + variants = get_args(annotation) + if isinstance(value, dict): + for variant in variants: + if is_typeddict(variant) and all( + _config_key(key, get_type_hints(variant)) in get_type_hints(variant) + for key in value + ): + return _config_value(value, variant) + for variant in variants: + if get_origin(variant) is dict: + return _config_value(value, variant) + for variant in variants: + if value is not None and variant is not type(None): + return _config_value(value, variant) + if isinstance(value, dict): + if is_typeddict(annotation): + hints = get_type_hints(annotation) + result = {} + for key, item in value.items(): + name = _config_key(key, hints) + if name not in hints: + raise ValueError(f"Unsupported AHP session setting: {key}") + result[name] = _config_value(item, hints[name]) + return result + if origin is dict: + return { + key: _config_value(item, get_args(annotation)[1]) for key, item in value.items() + } + decoder = getattr(annotation, "from_dict", None) + if callable(decoder): + return decoder(value) + if isinstance(value, list) and origin is list: + return [_config_value(item, get_args(annotation)[0]) for item in value] + return copy.deepcopy(value) + + +def _session_config(config: dict[str, Any], method: Callable[..., Any]) -> dict[str, Any]: + hints = get_type_hints(method) + result = {} + for key, value in config.items(): + name = "config_directory" if key == "configDir" else _config_key(key, hints) + if name not in hints: + raise ValueError(f"Unsupported AHP session setting: {key}") + result[name] = _config_value(value, hints[name]) + return result + + +def _contains(actual: Any, expected: Any) -> bool: + if isinstance(expected, dict): + return isinstance(actual, dict) and all( + key in actual and _contains(actual[key], value) for key, value in expected.items() + ) + return type(actual) is type(expected) and actual == expected + + +class _AhpHostManager: + def __init__( + self, + get_session: Callable[[str], CopilotSession | None], + create_session: Callable[..., Any], + resume_session: Callable[..., Any], + ) -> None: + self._get_session = get_session + self._create_session = create_session + self._resume_session = resume_session + self._hosts: dict[str, AhpHostOptions] = {} + self._handoffs: dict[str, _Handoff] = {} + self._tasks: set[asyncio.Task[Any]] = set() + + def capture(self, session: CopilotSession, config: dict[str, Any]) -> None: + for entry in self._handoffs.values(): + if entry.session_id == session.session_id: + 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, + "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, + } + startup = asyncio.create_task( + rpc.request( + "host.start", {key: value for key, value in params.items() if value is not None} + ) + ) + try: + info = HostStartResult.from_dict(await asyncio.shield(startup)) + 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. + async def cleanup() -> None: + try: + try: + await startup + except Exception: + return + await rpc.request("host.dispose", {"hostId": host_id}) + except Exception: + logger.exception("AHP canceled startup cleanup failed") + finally: + self._release_host(host_id) + + task = asyncio.create_task(cleanup()) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + raise + except BaseException: + self._release_host(host_id) + raise + + def notification(self, method: str, params: dict[str, Any]) -> bool: + if method == "host.sessionReleased": + entry = self._handoffs.get(params["handoffId"]) + if entry is not None and entry.host_id == params["hostId"]: + self._release(params["handoffId"]) + return True + if method == "host.exited": + self._exit(HostExitedNotification.from_dict(params)) + return True + return False + + def disconnect(self) -> None: + for host_id in list(self._hosts): + self._exit( + HostExitedNotification.from_dict( + { + "hostId": host_id, + "reason": "ownerDisconnected", + "error": ( + "Owner connection closed; runtime cleanup cannot be acknowledged " + "on this connection." + ), + } + ) + ) + + def _exit(self, event: AhpHostExit) -> None: + options = self._release_host(event.host_id) + if options is not None and options.on_exit is not None: + self._notify(options.on_exit, event, "AHP host exit callback failed") + + def _release_host(self, host_id: str) -> AhpHostOptions | None: + options = self._hosts.pop(host_id, None) + for handoff_id, entry in list(self._handoffs.items()): + if entry.host_id == host_id: + self._release(handoff_id) + return options + + def _release(self, handoff_id: str) -> None: + entry = self._handoffs.pop(handoff_id, None) + if entry is None: + return + entry.released = True + entry.cancellation.set() + entry.configs.clear() + self._notify_released(entry) + + def _notify_released(self, entry: _Handoff) -> None: + session, entry.session = entry.session, None + if session is not None: + if entry.options.on_session_released is not None: + self._notify( + entry.options.on_session_released, + session, + "AHP session release callback failed", + ) + + def _notify(self, callback: Callable[..., Any], value: Any, message: str) -> None: + async def invoke() -> None: + try: + result = callback(value) + if inspect.isawaitable(result): + await result + except Exception: + logger.exception(message) + + task = asyncio.create_task(invoke()) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + + async def materialize(self, params: dict[str, Any]) -> dict[str, str]: + host_id, handoff_id = params["hostId"], params["handoffId"] + options = self._hosts.get(host_id) + resume = params.get("resume", False) + if options is None or handoff_id in self._handoffs: + raise ValueError("AHP session factory is unavailable or handoff already exists") + if not (options.resume_session if resume else options.create_session): + raise ValueError("AHP session factory is unavailable") + expected = copy.deepcopy(params["config"]) + for public, wire in { + "suppressResumeEvent": "disableResume", + "enableExperimentalMode": "isExperimentalMode", + "enableMcpApps": "requestMcpApps", + }.items(): + if public in expected: + expected[wire] = expected.pop(public) + session_id = expected.get("sessionId") + if not isinstance(session_id, str) or not session_id: + raise ValueError("AHP session handoff requires sessionId") + entry = _Handoff(host_id, session_id, options) + self._handoffs[handoff_id] = entry + + async def run() -> dict[str, str]: + try: + method = self._resume_session if resume else self._create_session + config = _session_config(params["config"], method) + if resume: + config.pop("session_id") + assert options.resume_session is not None + session = await options.resume_session( + AhpSessionResumeRequest(session_id, config, entry.cancellation) + ) + else: + assert options.create_session is not None + session = await options.create_session( + AhpSessionCreateRequest(config, entry.cancellation) + ) + entry.session = session + if entry.released: + self._notify_released(entry) + raise _HandoffEnded("AHP session handoff ended") + if session.session_id != session_id or self._get_session(session_id) is not session: + raise ValueError( + "AHP callback must return the requested session from this client" + ) + actual = entry.configs.pop(session, None) + if (not resume or actual is not None) and not _contains(actual, expected): + raise ValueError( + "AHP callback must preserve the supplied session configuration" + ) + return {"sessionId": session_id} + except asyncio.CancelledError as exc: + self._release(handoff_id) + raise RuntimeError("AHP session factory was cancelled") from exc + except BaseException: + self._release(handoff_id) + raise + + materialized = asyncio.create_task(run()) + self._tasks.add(materialized) + + def completed(task: asyncio.Task[Any]) -> None: + self._tasks.discard(task) + if not task.cancelled(): + failure = task.exception() + if ( + entry.released + and failure is not None + and not isinstance(failure, _HandoffEnded) + ): + logger.error( + "AHP session factory failed", + exc_info=(type(failure), failure, failure.__traceback__), + ) + + materialized.add_done_callback(completed) + cancelled = asyncio.create_task(entry.cancellation.wait()) + try: + await asyncio.wait([materialized, cancelled], return_when=asyncio.FIRST_COMPLETED) + if materialized.done(): + return materialized.result() + if entry.released: + raise _HandoffEnded("AHP session handoff ended") + return await materialized + except BaseException: + self._release(handoff_id) + raise + finally: + cancelled.cancel() diff --git a/python/copilot/installation_confirmation.py b/python/copilot/installation_confirmation.py new file mode 100644 index 0000000000..da2ad1ca51 --- /dev/null +++ b/python/copilot/installation_confirmation.py @@ -0,0 +1,210 @@ +"""Installation confirmation receiver support.""" + +from __future__ import annotations + +import asyncio +import inspect +import logging +from collections.abc import Awaitable +from dataclasses import dataclass +from typing import Literal, Protocol + +from ._jsonrpc import JsonRpcClient +from .generated.rpc import ( + InstallationConfirmationRequest, + InstallationConfirmationResponse, + InstallationDecision, +) + +logger = logging.getLogger(__name__) + +InstallationConfirmationDecision = InstallationDecision | Literal["confirm", "decline", "cancel"] +"""Decision returned by an installation confirmation handler.""" + + +@dataclass(frozen=True) +class InstallationConfirmationContext: + """Cancellation signal for one installation review on its original connection.""" + + cancelled: asyncio.Event + """Set when the runtime retires this request or the original connection closes.""" + + +class InstallationConfirmationHandler(Protocol): + """Collect a fresh human decision for one complete installation review. + + Match the request's operation and optional policy session metadata to the + exact action previously registered on this connection before presenting it. + The SDK echoes the original challenge and fingerprint, so the handler + returns only an explicit decision. + """ + + def __call__( + self, + request: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ) -> InstallationConfirmationDecision | Awaitable[InstallationConfirmationDecision]: + """Return ``"confirm"``, ``"decline"`` or ``"cancel"``.""" + + +class _InstallationConfirmationAdapter: + def __init__( + self, + client: JsonRpcClient, + handler: InstallationConfirmationHandler | None, + ) -> None: + self._client = client + self._handler = handler + self._loop = asyncio.get_running_loop() + self._connection_closed = asyncio.Event() + + def register(self) -> None: + self._client.set_raw_request_handler("installations.confirm", self.handle_request) + + def close_connection(self) -> None: + if self._connection_closed.is_set(): + return + if self._loop.is_closed(): + return + self._loop.call_soon_threadsafe(self._connection_closed.set) + + async def handle_request(self, message: dict) -> None: + request_id = message["id"] + request_cancelled = self._client.incoming_request_cancelled_event(request_id) + cancelled = asyncio.Event() + if request_cancelled.is_set() or self._connection_closed.is_set(): + cancelled.set() + context = InstallationConfirmationContext(cancelled=cancelled) + + try: + request = InstallationConfirmationRequest.from_dict(message.get("params")) + except Exception: + await self._client._send_error_response( # noqa: SLF001 + request_id, + -32602, + "Invalid installation confirmation review", + None, + ) + return + + if self._handler is None: + await self._client._send_error_response( # noqa: SLF001 + request_id, + -32603, + "No installations client-global handler registered", + None, + ) + return + + confirmation_id = request.confirmation_id + review_fingerprint = request.review_fingerprint + handler_task = asyncio.create_task(self._call_handler(request, context)) + request_cancelled_task = asyncio.create_task(request_cancelled.wait()) + connection_closed_task = asyncio.create_task(self._connection_closed.wait()) + wait_tasks = {handler_task, request_cancelled_task, connection_closed_task} + + try: + done, pending = await asyncio.wait(wait_tasks, return_when=asyncio.FIRST_COMPLETED) + for task in pending - {handler_task}: + task.cancel() + + if connection_closed_task in done or self._connection_closed.is_set(): + cancelled.set() + _drop_late_handler_result(handler_task) + return + + if request_cancelled_task in done or request_cancelled.is_set(): + cancelled.set() + _drop_late_handler_result(handler_task) + await self._client._send_error_response( # noqa: SLF001 + request_id, + -32800, + "Installation confirmation request cancelled", + None, + ) + return + + decision = await handler_task + if self._connection_closed.is_set(): + cancelled.set() + return + if request_cancelled.is_set(): + cancelled.set() + await self._client._send_error_response( # noqa: SLF001 + request_id, + -32800, + "Installation confirmation request cancelled", + None, + ) + return + + try: + normalised = _normalise_decision(decision) + except ValueError: + await self._client._send_error_response( # noqa: SLF001 + request_id, + -32603, + "Invalid installation confirmation decision", + None, + ) + return + + response = InstallationConfirmationResponse( + confirmation_id=confirmation_id, + decision=normalised, + review_fingerprint=review_fingerprint, + ) + await self._client._send_response(request_id, response.to_dict()) # noqa: SLF001 + except Exception as exc: # pylint: disable=broad-except + if self._connection_closed.is_set(): + cancelled.set() + return + await self._client._send_error_response( # noqa: SLF001 + request_id, + -32603, + str(exc) or "Installation confirmation handler failed", + None, + ) + finally: + for task in (request_cancelled_task, connection_closed_task): + if not task.done(): + task.cancel() + + async def _call_handler( + self, + request: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ) -> InstallationConfirmationDecision: + assert self._handler is not None + result = self._handler(request, context) + if inspect.isawaitable(result): + result = await result + return result + + +def _normalise_decision(decision: InstallationConfirmationDecision) -> InstallationDecision: + if isinstance(decision, InstallationDecision): + return decision + if isinstance(decision, str): + try: + return InstallationDecision(decision) + except ValueError as exc: + raise ValueError("Invalid installation confirmation decision") from exc + raise ValueError("Invalid installation confirmation decision") + + +def _drop_late_handler_result(task: asyncio.Task) -> None: + if task.done(): + try: + task.result() + except Exception: + logger.debug("Installation confirmation handler failed after retirement", exc_info=True) + return + + def consume(completed: asyncio.Task) -> None: + try: + completed.result() + except Exception: + logger.debug("Installation confirmation handler failed after retirement", exc_info=True) + + task.add_done_callback(consume) diff --git a/python/copilot/session.py b/python/copilot/session.py index 9610c8ea21..31def8e813 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -21,7 +21,18 @@ from datetime import UTC, datetime from enum import Enum from types import TracebackType -from typing import TYPE_CHECKING, Any, Literal, NotRequired, Required, TypedDict, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + Literal, + NotRequired, + Protocol, + Required, + TypedDict, + TypeVar, + cast, + runtime_checkable, +) from pydantic import BaseModel @@ -45,6 +56,7 @@ LogRequest, MCPOauthHandlePendingRequest, MCPOauthPendingRequestResponse, + MCPServerConfigDeferTools, ModelSwitchAutoTierResult, ModelSwitchToRequest, PermissionDecision, @@ -52,10 +64,13 @@ PermissionDecisionContext, PermissionDecisionRequest, PermissionDecisionUserNotAvailable, + ProtocolExternalToolDefinition, ProviderTokenAcquireRequest, ProviderTokenAcquireResult, SessionLogLevel, SessionRpc, + SkillProviderDescriptor, + ToolsSetRequest, UIElicitationRequest, UIElicitationResponse, UIElicitationResponseAction, @@ -63,6 +78,8 @@ UIElicitationSchemaProperty, UIElicitationSchemaPropertyType, UIHandlePendingElicitationRequest, + _SkillProviderListResult, + _SkillProviderReadResult, ) from .generated.rpc import ( CanvasHandler as RpcCanvasHandler, @@ -215,8 +232,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 +431,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 +1242,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 +1316,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 +1521,46 @@ class ProviderTokenArgs(TypedDict): BearerTokenProvider = Callable[[ProviderTokenArgs], str | Awaitable[str]] +@runtime_checkable +class SkillProvider(Protocol): + """**Experimental.** Provides session-scoped skills from SDK-owned storage. + + Providers are ephemeral and must be supplied again when resuming a session. + The runtime may call providers concurrently, so implementations must be + concurrency-safe. Synchronous providers run on the event loop like other + Python SDK callbacks; keep them fast or make them async. When the runtime + cancels a call, for example after its 30-second limit or when the session + disconnects, an async provider's task is cancelled with + :class:`asyncio.CancelledError`. + """ + + def list_skills( + self, + ) -> list[SkillProviderDescriptor] | None | Awaitable[list[SkillProviderDescriptor] | None]: + """Return skill catalog descriptors, or ``None`` for an empty catalog.""" + ... + + def read_skill(self, name: str) -> str | None | Awaitable[str | None]: + """Return SKILL.md markdown for ``name``, or ``None`` when not found.""" + ... + + +def _skill_provider_failure_error(method_name: Literal["listSkills", "readSkill"]) -> JsonRpcError: + return JsonRpcError(-32603, f"Skill provider {method_name} failed") + + +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 +1571,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 +1627,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 +1747,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 +1808,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 +1816,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() @@ -1666,6 +1834,8 @@ def __init__( self._transform_callbacks_lock = threading.Lock() self._command_handlers: dict[str, CommandHandler] = {} self._command_handlers_lock = threading.Lock() + self._skill_provider: SkillProvider | None = None + self._skill_provider_lock = threading.Lock() self._bearer_token_providers: dict[str, BearerTokenProvider] = {} self._bearer_token_providers_lock = threading.Lock() self._elicitation_handler: ElicitationHandler | None = None @@ -1704,6 +1874,7 @@ def _mark_disconnected(self) -> None: self._destroyed = True self._fail_structured_waits() self._cancel_pending_external_tools() + self._clear_skill_provider() self._run_disconnect_callback() def _fail_structured_waits(self) -> None: @@ -1847,6 +2018,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 +3107,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: """ @@ -2913,6 +3126,77 @@ def _get_tool_handler(self, name: str) -> ToolHandler | None: with self._tool_handlers_lock: return self._tool_handlers.get(name) + def _register_skill_provider(self, provider: SkillProvider | None) -> None: + """Register or clear the session-scoped skill provider.""" + with self._skill_provider_lock: + self._skill_provider = provider + + def _clear_skill_provider(self) -> None: + self._register_skill_provider(None) + + async def _handle_skill_provider_list(self) -> dict: + provider = self._require_skill_provider() + skills = await self._call_skill_provider("listSkills", provider.list_skills) + resolved_skills = cast(list[SkillProviderDescriptor] | None, skills) + try: + return _SkillProviderListResult( + skills=[] if resolved_skills is None else list(resolved_skills) + ).to_dict() + except Exception: + self._log_skill_provider_failure("listSkills") + raise _skill_provider_failure_error("listSkills") from None + + async def _handle_skill_provider_read(self, name: str) -> dict: + provider = self._require_skill_provider() + markdown = await self._call_skill_provider( + "readSkill", functools.partial(provider.read_skill, name) + ) + return _SkillProviderReadResult(markdown=cast(str | None, markdown)).to_dict() + + def _require_skill_provider(self) -> SkillProvider: + with self._skill_provider_lock: + provider = self._skill_provider + if provider is None: + raise JsonRpcError(-32603, f"No skill provider for session: {self.session_id}") + return provider + + async def _call_skill_provider( + self, operation: Literal["listSkills", "readSkill"], call: Callable[[], Any] + ) -> Any: + """Run a provider callback, reporting any failure to the runtime generically.""" + provider_start = time.perf_counter() + try: + result = call() + if inspect.isawaitable(result): + result = await result + except asyncio.CancelledError: + # Only a cancellation of this dispatch task propagates; one raised by + # the provider itself is an ordinary provider failure. + task = asyncio.current_task() + if task is not None and task.cancelling(): + raise + self._log_skill_provider_failure(operation) + raise _skill_provider_failure_error(operation) from None + except Exception: + self._log_skill_provider_failure(operation) + raise _skill_provider_failure_error(operation) from None + log_timing( + logger, + logging.DEBUG, + f"CopilotSession skill provider {operation} dispatch", + provider_start, + session_id=self.session_id, + ) + return result + + def _log_skill_provider_failure(self, operation: Literal["listSkills", "readSkill"]) -> None: + logger.warning( + "Skill provider %s failed", + operation, + extra={"session_id": self.session_id}, + exc_info=True, + ) + def _register_permission_handler(self, handler: _PermissionHandlerFn | None) -> None: """ Register a handler for permission requests. @@ -3184,6 +3468,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) @@ -3297,6 +3583,7 @@ async def disconnect(self) -> None: self._event_handlers.clear() with self._tool_handlers_lock: self._tool_handlers.clear() + self._clear_skill_provider() with self._permission_handler_lock: self._permission_handler = None with self._command_handlers_lock: 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_abort_e2e.py b/python/e2e/test_abort_e2e.py index ce3a497f49..9dff78adde 100644 --- a/python/e2e/test_abort_e2e.py +++ b/python/e2e/test_abort_e2e.py @@ -115,7 +115,6 @@ async def slow_tool_handler(invocation: ToolInvocation) -> ToolResult: session.send("Use slow_analysis with value 'test_abort'. Wait for the result.") ) - # Wait for the tool to start executing tool_value = await asyncio.wait_for(tool_started, timeout=60.0) assert tool_value == "test_abort" 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_auth_host_e2e.py b/python/e2e/test_auth_host_e2e.py new file mode 100644 index 0000000000..796e6b4382 --- /dev/null +++ b/python/e2e/test_auth_host_e2e.py @@ -0,0 +1,42 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# --------------------------------------------------------------------------------------------- + +"""Authentication host E2E coverage.""" + +import os + +import pytest +import pytest_asyncio + +from .testharness import E2ETestContext + + +@pytest_asyncio.fixture(loop_scope="module") +async def ctx(request): + context = E2ETestContext() + await context.setup() + yield context + await context.teardown( + test_failed=request.session.stash.get("any_test_failed", False) + or bool(os.environ.get("GITHUB_ACTIONS")) + ) + + +@pytest.mark.asyncio(loop_scope="module") +@pytest.mark.parametrize( + ("copilot_host", "gh_host", "expected_host"), + [ + ("tenant.ghe.example", "fallback.ghe.example", "https://tenant.ghe.example"), + ("", "fallback.ghe.example", "https://fallback.ghe.example"), + ("", "", "https://github.com"), + ], +) +async def test_should_report_selected_github_auth_host(ctx, copilot_host, gh_host, expected_host): + ctx.add_runtime_env("COPILOT_GH_HOST", copilot_host) + ctx.add_runtime_env("GH_HOST", gh_host) + + await ctx.client.start() + status = await ctx.client.get_auth_status() + assert status.isAuthenticated, status.statusMessage + assert status.host == expected_host 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_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index f6b2cd37cc..1267bf6d57 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -103,6 +103,7 @@ def _get_available_port() -> int: env: { COPILOT_HOME: process.env.COPILOT_HOME, COPILOT_SDK_AUTH_TOKEN: process.env.COPILOT_SDK_AUTH_TOKEN, + COPILOT_RUNTIME_PROCESS_FILE_LOGGING: process.env.COPILOT_RUNTIME_PROCESS_FILE_LOGGING, COPILOT_OTEL_ENABLED: process.env.COPILOT_OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL: process.env.OTEL_EXPORTER_OTLP_PROTOCOL, @@ -276,7 +277,11 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes cli_path=cli_path, base_directory=copilot_home_from_option, cli_args=["--capture-file", capture_path], - env={**ctx.get_env(), "COPILOT_HOME": copilot_home_from_env}, + env={ + **ctx.get_env(), + "COPILOT_HOME": copilot_home_from_env, + "COPILOT_RUNTIME_PROCESS_FILE_LOGGING": "0", + }, github_token="process-option-token", log_level="debug", session_idle_timeout_seconds=17, @@ -309,6 +314,7 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes assert env["COPILOT_HOME"] == copilot_home_from_option assert env["COPILOT_SDK_AUTH_TOKEN"] == "process-option-token" + assert env["COPILOT_RUNTIME_PROCESS_FILE_LOGGING"] == "1" assert env["COPILOT_OTEL_ENABLED"] == "true" assert env["OTEL_EXPORTER_OTLP_ENDPOINT"] == "http://127.0.0.1:4318" assert env["OTEL_EXPORTER_OTLP_PROTOCOL"] == "http/protobuf" @@ -434,6 +440,7 @@ async def test_should_send_empty_mode_custom_agent_locality_defaults(self, ctx: cli_args=["--capture-file", capture_path], mode="empty", base_directory=ctx.work_dir, + env={**ctx.get_env(), "COPILOT_RUNTIME_PROCESS_FILE_LOGGING": "0"}, use_logged_in_user=False, ), ) @@ -453,6 +460,7 @@ async def test_should_send_empty_mode_custom_agent_locality_defaults(self, ctx: try: with open(capture_path) as f: capture = json.load(f) + assert capture["env"]["COPILOT_RUNTIME_PROCESS_FILE_LOGGING"] == "0" create_request = next( r for r in capture["requests"] if r["method"] == "session.create" ) 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_and_agents_e2e.py b/python/e2e/test_mcp_and_agents_e2e.py index 915928f1e3..bdb67a2c8e 100644 --- a/python/e2e/test_mcp_and_agents_e2e.py +++ b/python/e2e/test_mcp_and_agents_e2e.py @@ -94,7 +94,6 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( self, ctx: E2ETestContext ): """Test that MCP server configuration is accepted on session resume""" - # Create a session first session1 = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all ) @@ -177,7 +176,6 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( self, ctx: E2ETestContext ): """Test that custom agent configuration is accepted on session resume""" - # Create a session first session1 = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all ) 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_permissions_e2e.py b/python/e2e/test_permissions_e2e.py index 62537583b5..a9f3019eaf 100644 --- a/python/e2e/test_permissions_e2e.py +++ b/python/e2e/test_permissions_e2e.py @@ -210,7 +210,6 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC """Test resuming session with permission handler""" permission_requests = [] - # Create initial session session1 = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all ) 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 c9f08742cd..0000000000 --- a/python/e2e/test_rpc_e2e.py +++ /dev/null @@ -1,237 +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" - ) - - # Get initial model - 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 - - # Delete plan - 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 file - read_result = await session.rpc.workspaces.read_file( - WorkspacesReadFileRequest(path="test.txt") - ) - assert read_result.content == file_content - - # Create nested file - 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 bc5d0ae52c..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.factory.run": rpc.FactoryApi.run, - "session.factory.resume": rpc.FactoryApi.resume, - "session.factory.getRun": rpc.FactoryApi.get_run, - "session.factory.listRuns": rpc.FactoryApi.list_runs, - "session.factory.getRunDetail": rpc.FactoryApi.get_run_detail, - "session.factory.getRunProgress": rpc.FactoryApi.get_run_progress, - "session.factory.cancel": rpc.FactoryApi.cancel, - "session.factory.pause": rpc.FactoryApi.pause, - "session.factory.log": rpc.FactoryApi.log, - "session.factory.agent": rpc.FactoryApi.agent, - "session.factory.journal.get": rpc.FactoryJournalApi.get, - "session.factory.journal.put": rpc.FactoryJournalApi.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.factory.run"] = await session.rpc.factory.run( - _request_for(rpc.FactoryApi.run) - ) - results["session.factory.resume"] = await session.rpc.factory.resume( - _request_for(rpc.FactoryApi.resume) - ) - results["session.factory.getRun"] = await session.rpc.factory.get_run( - _request_for(rpc.FactoryApi.get_run) - ) - results["session.factory.listRuns"] = await session.rpc.factory.list_runs( - _request_for(rpc.FactoryApi.list_runs) - ) - results["session.factory.getRunDetail"] = await session.rpc.factory.get_run_detail( - _request_for(rpc.FactoryApi.get_run_detail) - ) - results["session.factory.getRunProgress"] = await session.rpc.factory.get_run_progress( - _request_for(rpc.FactoryApi.get_run_progress) - ) - results["session.factory.cancel"] = await session.rpc.factory.cancel( - _request_for(rpc.FactoryApi.cancel) - ) - results["session.factory.pause"] = await session.rpc.factory.pause( - _request_for(rpc.FactoryApi.pause) - ) - results["session.factory.log"] = await session.rpc.factory.log( - _request_for(rpc.FactoryApi.log) - ) - results["session.factory.agent"] = await session.rpc.factory.agent( - _request_for(rpc.FactoryApi.agent) - ) - results["session.factory.journal.get"] = await session.rpc.factory.journal.get( - _request_for(rpc.FactoryJournalApi.get) - ) - results["session.factory.journal.put"] = await session.rpc.factory.journal.put( - _request_for(rpc.FactoryJournalApi.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" - - factory_detail = results["session.factory.getRunDetail"] - assert factory_detail.consumed.active_ms == 7 - assert factory_detail.agents[0].agent_id == "sample-value" - assert factory_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 new file mode 100644 index 0000000000..5300f8aacb --- /dev/null +++ b/python/e2e/test_runtime_host_e2e.py @@ -0,0 +1,310 @@ +"""All SDKs replay the same application-owned AHP hosting conversations.""" + +import asyncio +import json +import os +from contextlib import asynccontextmanager + +import pytest +import pytest_asyncio +from pydantic import BaseModel, Field + +from copilot import ( + AhpHost, + AhpHostOptions, + AhpSessionCreateRequest, + AhpSessionResumeRequest, + CopilotClient, + CopilotSession, + define_tool, +) +from copilot.session import PermissionHandler +from copilot.tools import ToolInvocation + +from .testharness import E2ETestContext +from .testharness.ahp import AhpTestClient +from .testharness.context import DEFAULT_GITHUB_TOKEN + +pytestmark = [ + pytest.mark.asyncio(loop_scope="module"), + pytest.mark.skipif( + os.environ.get("COPILOT_RUNTIME_HOST_E2E") != "1", + reason="Requires an integrated runtime; set COPILOT_RUNTIME_HOST_E2E=1", + ), +] +TOOL_PROMPT = "Use the magic_number tool with seed 'hello' and tell me the result" +COMPOSED_PROMPT = ( + "Call magic_number with seed 'hello' and client_echo with text 'ping', then report both results" +) +MARKER = "APPLICATION_OWNED_AHP_PROMPT" + + +@pytest_asyncio.fixture(loop_scope="module") +async def ahp(): + async with AhpTestClient() as client: + yield client + + +class Seed(BaseModel): + seed: str = Field(description="A seed value") + + +class Application: + def __init__(self, client: CopilotClient, work_dir: str): + self.client = client + self.work_dir = work_dir + self.sessions: list[CopilotSession] = [] + self.releases: list[CopilotSession] = [] + self.release_event = asyncio.Event() + self.exit_event = asyncio.Event() + self.exits = [] + self.tool_calls: list[str] = [] + self.hook_calls: list[str] = [] + self.create_calls = 0 + self.resume_calls = 0 + + @define_tool("magic_number", description="Returns a magic number") + def magic_number(params: Seed, invocation: ToolInvocation) -> str: + assert params.seed == "hello" + self.tool_calls.append(invocation.session_id) + return f"MAGIC_{params.seed}_42" + + self.tool = magic_number + + async def pre_tool(self, _input, invocation): + self.hook_calls.append(invocation["session_id"]) + return None + + def config(self, requested: dict) -> dict: + return { + **requested, + "on_permission_request": PermissionHandler.approve_all, + "system_message": {"mode": "append", "content": MARKER}, + "tools": [self.tool], + "hooks": {"on_pre_tool_use": self.pre_tool}, + } + + async def create(self, request: AhpSessionCreateRequest) -> CopilotSession: + self.create_calls += 1 + assert not request.cancellation_event.is_set() + assert request.config["working_directory"] == self.work_dir + session = await self.client.create_session(**self.config(request.config)) + self.sessions.append(session) + return session + + async def resume(self, request: AhpSessionResumeRequest) -> CopilotSession: + self.resume_calls += 1 + assert not request.cancellation_event.is_set() + assert request.config["continue_pending_work"] is False + assert request.config["working_directory"] == self.work_dir + session = await self.client.resume_session( + request.session_id, **self.config(request.config) + ) + self.sessions.append(session) + return session + + def release(self, session: CopilotSession) -> None: + self.releases.append(session) + self.release_event.set() + + def exited(self, event) -> None: + self.exits.append(event) + 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, + on_exit=self.exited, + ) + + +@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, + "token": host.token, + "githubToken": DEFAULT_GITHUB_TOKEN, + } + if client_id is not None: + command["clientId"] = client_id + connection = await ahp.request(command) + try: + yield connection["clientId"] + finally: + await ahp.request({"op": "close", "clientId": connection["clientId"]}) + + +async def assert_tools(ctx: E2ETestContext, app: Application, session_id: str): + assert app.tool_calls == [session_id] + assert session_id in app.hook_calls + exchanges = await ctx.get_exchanges() + assert any(MARKER in json.dumps(exchange["request"]["messages"]) for exchange in exchanges) + assert any( + tool["function"]["name"] == "magic_number" + for exchange in exchanges + for tool in exchange["request"]["tools"] + ) + + +class TestRuntimeHost: + async def test_creates_application_session_and_preserves_callbacks( + self, ctx: E2ETestContext, ahp: AhpTestClient + ): + await ctx.configure_for_test( + "multi_client", "both_clients_see_tool_request_and_completion_events" + ) + app = Application(ctx.client, ctx.work_dir) + try: + async with await ctx.client.start_ahp_host(app.options()) as host: + assert host.pid is None + async with connect(ahp, host) as client_id: + created = await ahp.request( + {"op": "create", "clientId": client_id, "workDir": ctx.work_dir} + ) + session_id = created["sessionId"] + assert app.create_calls == 1 + assert app.sessions[0].session_id == session_id + response = await ahp.request( + { + "op": "turn", + "clientId": client_id, + "sessionId": session_id, + "prompt": TOOL_PROMPT, + } + ) + assert "MAGIC_hello_42" in response["text"] + await assert_tools(ctx, app, session_id) + await asyncio.gather(host.dispose(), host.dispose()) + await ahp.request({"op": "stopped", "clientId": client_id, "url": host.url}) + await asyncio.wait_for(app.release_event.wait(), 10) + await asyncio.wait_for(app.exit_event.wait(), 10) + assert len(app.releases) == 1 + assert app.releases[0] is app.sessions[0] + assert len(app.exits) == 1 + assert await app.sessions[0].get_events() + assert (await ctx.client.ping("still alive")).message == "pong: still alive" + finally: + await ctx.client.stop() + + async def test_publishes_exact_resident_session_without_factory( + self, ctx: E2ETestContext, ahp: AhpTestClient + ): + await ctx.configure_for_test( + "multi_client", "both_clients_see_tool_request_and_completion_events" + ) + app = Application(ctx.client, ctx.work_dir) + try: + original = await ctx.client.create_session( + **app.config({"working_directory": ctx.work_dir}) + ) + async with await ctx.client.start_ahp_host(app.options()) as host: + published = await host.publish_session(original.session_id) + assert published.session_id == original.session_id + assert published.session_uri == f"ahp-session:/{original.session_id}" + async with connect(ahp, host) as client_id: + await ahp.request( + {"op": "attach", "clientId": client_id, "sessionId": original.session_id} + ) + response = await ahp.request( + { + "op": "turn", + "clientId": client_id, + "sessionId": original.session_id, + "prompt": TOOL_PROMPT, + } + ) + assert "MAGIC_hello_42" in response["text"] + await assert_tools(ctx, app, original.session_id) + await host.dispose() + await ahp.request({"op": "stopped", "clientId": client_id, "url": host.url}) + assert app.create_calls == app.resume_calls == 0 + assert not app.releases + assert await original.get_events() + assert (await ctx.client.ping("still alive")).message == "pong: still alive" + finally: + await ctx.client.stop() + + async def test_resumes_after_runtime_restart_and_composes_tools( + self, ctx: E2ETestContext, ahp: AhpTestClient + ): + await ctx.configure_for_test( + "runtime_host", "app_resume_callback_composes_tools_after_history" + ) + app = Application(ctx.client, ctx.work_dir) + try: + async with await ctx.client.start_ahp_host(app.options()) as host: + async with connect(ahp, host) as client_id: + created = await ahp.request( + { + "op": "create", + "clientId": client_id, + "workDir": ctx.work_dir, + "clientTools": True, + } + ) + session_id = created["sessionId"] + first = await ahp.request( + { + "op": "turn", + "clientId": client_id, + "sessionId": session_id, + "prompt": "What is 2+2?", + } + ) + assert "4" in first["text"] + await host.dispose() + await ahp.request({"op": "stopped", "clientId": client_id, "url": host.url}) + await asyncio.wait_for(app.release_event.wait(), 10) + assert app.releases[0] is app.sessions[0] + await ctx.client.stop() + + resumed = Application(ctx.client, ctx.work_dir) + async with await ctx.client.start_ahp_host(resumed.options()) as replacement: + assert replacement.pid is None + async with connect(ahp, replacement, client_id) as resumed_client_id: + attached = await ahp.request( + { + "op": "attach", + "clientId": resumed_client_id, + "sessionId": session_id, + "clientTools": True, + } + ) + assert [turn["message"]["text"] for turn in attached["history"]] == [ + "What is 2+2?" + ] + assert resumed.create_calls == 0 + assert resumed.resume_calls == 1 + assert resumed.sessions[0].session_id == session_id + assert resumed.sessions[0] is not app.sessions[0] + response = await ahp.request( + { + "op": "turn", + "clientId": resumed_client_id, + "sessionId": session_id, + "prompt": COMPOSED_PROMPT, + "clientTools": True, + } + ) + assert "MAGIC_hello_42" in response["text"] + assert "CLIENT_ECHO_ping" in response["text"] + assert response["clientToolCalls"] == 1 + await assert_tools(ctx, resumed, session_id) + await replacement.dispose() + await ahp.request( + {"op": "stopped", "clientId": resumed_client_id, "url": replacement.url} + ) + await asyncio.wait_for(resumed.release_event.wait(), 10) + assert len(resumed.releases) == 1 + assert resumed.releases[0] is resumed.sessions[0] + assert await resumed.sessions[0].get_events() + finally: + await ctx.client.stop() diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index 5cd163c9f2..21bb8bd3a4 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -220,7 +220,6 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont await s.get_events() async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): - # Create initial session session1 = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all ) @@ -247,7 +246,6 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC assert "4" in answer3.data.content async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestContext): - # Create initial session session1 = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all ) @@ -437,7 +435,6 @@ async def test_should_delete_session(self, ctx: E2ETestContext): session_ids = [s.session_id for s in sessions] assert session_id in session_ids - # Delete the session await ctx.client.delete_session(session_id) # Verify session no longer exists in the list @@ -594,7 +591,6 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): "run the shell command 'sleep 100' (note this works on both bash and PowerShell)" ) - # Wait for the tool to start executing _ = await wait_for_tool_start # Abort the session while the tool is running @@ -1007,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_skill_provider_e2e.py b/python/e2e/test_skill_provider_e2e.py new file mode 100644 index 0000000000..d91f858a85 --- /dev/null +++ b/python/e2e/test_skill_provider_e2e.py @@ -0,0 +1,399 @@ +""" +E2E tests for SDK skill providers. +""" + +import asyncio +import json +import os +from collections.abc import Callable +from dataclasses import dataclass + +import pytest + +from copilot import CloudSessionOptions, PermissionHandler, SkillProvider, SkillProviderDescriptor +from copilot.session_events import ( + SessionEvent, + SessionEventType, + SkillSource, + ToolExecutionCompleteData, + session_event_to_dict, +) + +from .testharness import E2ETestContext + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +@dataclass(frozen=True) +class ProvidedSkill: + descriptor: SkillProviderDescriptor + read: Callable[[], str | None] + + +class InMemorySkillProvider(SkillProvider): + """An in-memory provider that records every callback the runtime makes.""" + + def __init__(self, skills: list[ProvidedSkill]): + self.calls: list[str] = [] + self._skills = skills + + @property + def reads(self) -> list[str]: + return [call.removeprefix("read:") for call in self.calls if call.startswith("read:")] + + def list_skills(self) -> list[SkillProviderDescriptor]: + self.calls.append("list") + return [skill.descriptor for skill in self._skills] + + def read_skill(self, name: str) -> str | None: + self.calls.append(f"read:{name}") + for skill in self._skills: + if skill.descriptor.name == name: + return skill.read() + return None + + +def skill(name: str, description: str, markdown: str) -> ProvidedSkill: + return ProvidedSkill( + descriptor=SkillProviderDescriptor(description=description, name=name), + read=lambda: markdown, + ) + + +def failed_tool_executions(events: list[SessionEvent]) -> list[SessionEvent]: + return [ + event + for event in events + if event.type == SessionEventType.TOOL_EXECUTION_COMPLETE + and isinstance(event.data, ToolExecutionCompleteData) + and not event.data.success + ] + + +class TestSkillProviders: + async def test_should_load_provider_skill_lazily_through_skill_tool(self, ctx: E2ETestContext): + # Body-only content: the catalog descriptor supplies all of the metadata. + provider = InMemorySkillProvider( + [ + skill( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\n" + "The verification word is TANGERINE_QUARTZ_19. Reply with it.\n", + ) + ] + ) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=provider, + ) + + try: + skills = await session.rpc.skills.list() + listed = next((s for s in skills.skills if s.name == "provider-lookup"), None) + assert listed is not None + assert listed.source == SkillSource.SDK + assert listed.enabled is True + assert (listed.path or "") == "" + assert provider.reads == [] + + message = await session.send_and_wait( + "Use the skill tool to load the provider-lookup skill, " + "then reply with its verification word." + ) + + assert provider.reads == ["provider-lookup"] + assert message is not None + # Validate the final assistant response arrived (guards against truncated captures) + assert "TANGERINE_QUARTZ_19" in message.data.content + finally: + await session.disconnect() + + async def test_should_load_provider_and_file_based_skills_together(self, ctx: E2ETestContext): + skills_dir = os.path.join(ctx.work_dir, "file-skills") + file_skill_dir = os.path.join(skills_dir, "file-notes") + os.makedirs(file_skill_dir, exist_ok=True) + with open(os.path.join(file_skill_dir, "SKILL.md"), "w", newline="\n") as f: + f.write( + "---\n" + "name: file-notes\n" + "description: Reports the file notes verification word.\n" + "---\n" + "\n" + "The file notes verification word is MAPLE_FALCON_27.\n" + ) + + # Frontmatter may restate catalog metadata and is the only source of allowed-tools. + provider = InMemorySkillProvider( + [ + skill( + "provider-audit", + "Reports the provider audit verification word.", + "---\n" + "name: provider-audit\n" + "allowed-tools: view\n" + "---\n" + "\n" + "The provider audit verification word is COBALT_HERON_58.\n", + ) + ] + ) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_directories=[skills_dir], + skill_provider=provider, + ) + + try: + skills = await session.rpc.skills.list() + file_skill = next((s for s in skills.skills if s.name == "file-notes"), None) + provider_skill = next((s for s in skills.skills if s.name == "provider-audit"), None) + assert file_skill is not None + assert file_skill.source != SkillSource.SDK + assert file_skill.path + assert provider_skill is not None + assert provider_skill.source == SkillSource.SDK + + message = await session.send_and_wait( + "Use the skill tool to load the file-notes skill and the " + "provider-audit skill, then reply with both verification words." + ) + + assert provider.reads == ["provider-audit"] + assert message is not None + assert "MAPLE_FALCON_27" in message.data.content + # Validate the final assistant response arrived (guards against truncated captures) + assert "COBALT_HERON_58" in message.data.content + finally: + await session.disconnect() + + async def test_should_rebind_skill_provider_on_resume(self, ctx: E2ETestContext): + original = InMemorySkillProvider( + [ + skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n", + ) + ] + ) + replacement = InMemorySkillProvider( + [ + skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n", + ) + ] + ) + first = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=original, + ) + session_id = first.session_id + try: + # A completed turn persists the session so that it can be resumed after disconnecting. + await first.send_and_wait( + "Without using any tools or skills, reply with exactly REBIND_READY." + ) + finally: + await first.disconnect() + assert original.reads == [] + original_calls_before_resume = len(original.calls) + + session = await ctx.client.resume_session( + session_id, + on_permission_request=PermissionHandler.approve_all, + skill_provider=replacement, + ) + + try: + message = await session.send_and_wait( + "Use the skill tool to load the rebind-check skill, " + "then reply with its verification word." + ) + + assert replacement.reads == ["rebind-check"] + assert len(original.calls) == original_calls_before_resume + assert message is not None + # Validate the final assistant response arrived (guards against truncated captures) + assert "BRONZE_BETA_22" in message.data.content + assert "AMBER_ALPHA_11" not in message.data.content + finally: + await session.disconnect() + + async def test_should_report_provider_read_failure_without_leaking_details( + self, ctx: E2ETestContext + ): + secret = "PROVIDER_SECRET_7F3A9C" + + def fail_with_secret() -> str | None: + raise RuntimeError(f"database unavailable: {secret}") + + provider = InMemorySkillProvider( + [ + ProvidedSkill( + descriptor=SkillProviderDescriptor( + description="Reports the broken lookup verification word.", + name="broken-lookup", + ), + read=fail_with_secret, + ) + ] + ) + events: list[SessionEvent] = [] + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=provider, + on_event=events.append, + ) + + try: + message = await session.send_and_wait( + "Use the skill tool to load the broken-lookup skill. " + "If loading fails, reply with exactly LOAD_FAILED." + ) + + assert "broken-lookup" in provider.reads + failures = failed_tool_executions(events) + assert len(failures) == 1 + serialized_events = json.dumps([session_event_to_dict(event) for event in events]) + assert secret not in serialized_events + assert message is not None + # Validate the final assistant response arrived (guards against truncated captures) + assert "LOAD_FAILED" in message.data.content + finally: + await session.disconnect() + + async def test_should_report_missing_provider_skill_as_not_found(self, ctx: E2ETestContext): + provider = InMemorySkillProvider( + [ + ProvidedSkill( + descriptor=SkillProviderDescriptor( + description="Reports the vanished lookup verification word.", + name="vanished-lookup", + ), + read=lambda: None, + ) + ] + ) + events: list[SessionEvent] = [] + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=provider, + on_event=events.append, + ) + + try: + message = await session.send_and_wait( + "Use the skill tool to load the vanished-lookup skill. " + "If loading fails, reply with exactly LOAD_FAILED." + ) + + assert "vanished-lookup" in provider.reads + failures = failed_tool_executions(events) + assert len(failures) == 1 + data = failures[0].data + assert isinstance(data, ToolExecutionCompleteData) + assert data.error is not None + assert "not found" in data.error.message.lower() + assert message is not None + # Validate the final assistant response arrived (guards against truncated captures) + assert "LOAD_FAILED" in message.data.content + finally: + await session.disconnect() + + async def test_should_keep_provider_dormant_when_skills_disabled(self, ctx: E2ETestContext): + provider = InMemorySkillProvider( + [skill("dormant-lookup", "Never listed.", "Never read.\n")] + ) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + enable_skills=False, + skill_provider=provider, + ) + + try: + await session.rpc.skills.ensure_loaded() + skills = await session.rpc.skills.list() + + assert [s for s in skills.skills if s.source == SkillSource.SDK] == [] + assert provider.calls == [] + finally: + await session.disconnect() + + async def test_should_unbind_provider_when_resumed_without_one(self, ctx: E2ETestContext): + provider = InMemorySkillProvider( + [skill("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n")] + ) + first = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=provider, + ) + before = await first.rpc.skills.list() + assert any(s.name == "unbound-lookup" for s in before.skills) + calls_before_resume = len(provider.calls) + + # Resume while the provider is still bound so the unbind is observable. + session = await ctx.client.resume_session( + first.session_id, + on_permission_request=PermissionHandler.approve_all, + ) + + try: + await session.rpc.skills.reload() + skills = await session.rpc.skills.list() + + assert [s for s in skills.skills if s.source == SkillSource.SDK] == [] + assert len(provider.calls) == calls_before_resume + finally: + await session.disconnect() + + async def test_should_cancel_a_blocked_provider_call_when_the_session_disconnects( + self, ctx: E2ETestContext + ): + entered = asyncio.Event() + cancelled = asyncio.Event() + + class BlockingSkillProvider: + async def list_skills(self) -> list[SkillProviderDescriptor]: + entered.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + cancelled.set() + raise + return [] + + def read_skill(self, name: str) -> str | None: + return None + + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=BlockingSkillProvider(), + ) + + # The list RPC fails once the binding is removed; only the provider's + # cancellation matters here. + listing = asyncio.create_task(session.rpc.skills.list()) + try: + await asyncio.wait_for(entered.wait(), timeout=30) + await session.disconnect() + await asyncio.wait_for(cancelled.wait(), timeout=10) + finally: + listing.cancel() + await asyncio.gather(listing, return_exceptions=True) + + async def test_should_reject_skill_provider_for_cloud_sessions(self, ctx: E2ETestContext): + provider = InMemorySkillProvider([skill("cloud-lookup", "Never listed.", "Never read.\n")]) + + with pytest.raises( + ValueError, match="Skill providers are not supported for cloud sessions." + ): + await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + cloud=CloudSessionOptions(), + skill_provider=provider, + ) + + assert provider.calls == [] 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_telemetry_e2e.py b/python/e2e/test_telemetry_e2e.py index 56031a14ec..d8fed23659 100644 --- a/python/e2e/test_telemetry_e2e.py +++ b/python/e2e/test_telemetry_e2e.py @@ -157,6 +157,107 @@ def echo(invocation: ToolInvocation) -> ToolResult: ) assert _string_attribute(tool_span, "gen_ai.tool.call.result") == marker + async def test_should_export_per_request_subagent_chat_spans(self, ctx: E2ETestContext): + telemetry_path = Path(ctx.work_dir) / f"telemetry-{uuid.uuid4().hex}.jsonl" + source_name = "python-sdk-subagent-telemetry-e2e" + prompt = ( + "Use the task tool in sync mode to ask a task agent to read subagent-otel.txt " + "with the view tool. Then reply with SUBAGENT_OTEL_DONE." + ) + (Path(ctx.work_dir) / "subagent-otel.txt").write_text( + "SUBAGENT_OTEL_FILE_CONTENT", encoding="utf-8" + ) + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token="fake-token-for-e2e-tests" + if os.environ.get("GITHUB_ACTIONS") == "true" + else None, + telemetry=TelemetryConfig( + file_path=str(telemetry_path), + exporter_type="file", + source_name=source_name, + capture_content=True, + ), + ) + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + session_id = session.session_id + try: + answer = await session.send_and_wait(prompt, timeout=60.0) + assert answer is not None + assert "SUBAGENT_OTEL_DONE" in (answer.data.content or "") + finally: + await session.disconnect() + finally: + await client.stop() + + spans = [ + entry + for entry in _read_telemetry_entries(telemetry_path) + if entry.get("type") == "span" + ] + assert all( + (span.get("instrumentationScope") or {}).get("name") == source_name for span in spans + ) + assert all((span.get("status") or {}).get("code", 0) != 2 for span in spans) + + invocations = [ + s for s in spans if _string_attribute(s, "gen_ai.operation.name") == "invoke_agent" + ] + assert len(invocations) == 2, invocations + roots = [s for s in invocations if _is_root_span(s)] + assert len(roots) == 1, roots + root = roots[0] + assert _string_attribute(root, "gen_ai.conversation.id") == session_id + assert root.get("spanId") and root.get("traceId") + tasks = [ + s + for s in spans + if _string_attribute(s, "gen_ai.operation.name") == "execute_tool" + and _string_attribute(s, "gen_ai.tool.name") == "task" + ] + assert len(tasks) == 1, tasks + task = tasks[0] + assert task.get("parentSpanId") == root["spanId"] + children = [s for s in invocations if s.get("parentSpanId") == task.get("spanId")] + assert len(children) == 1, children + child = children[0] + assert task.get("traceId") == child.get("traceId") == root["traceId"] + + chats = [s for s in spans if _string_attribute(s, "gen_ai.operation.name") == "chat"] + assert len(chats) == 4, chats + parent_chats = [s for s in chats if s.get("parentSpanId") == root["spanId"]] + assert len(parent_chats) == 2 + assert all(s.get("traceId") == root["traceId"] for s in parent_chats) + child_chats = [s for s in chats if s.get("parentSpanId") == child["spanId"]] + assert len(child_chats) == 2, child_chats + assert all(s.get("traceId") == root["traceId"] for s in child_chats) + assert all( + _string_attribute(s, "github.copilot.initiator") == "sub-agent" for s in child_chats + ) + requesting_chats = [ + s + for s in child_chats + if '"view"' in (_string_attribute(s, "gen_ai.output.messages") or "") + ] + assert len(requesting_chats) == 1 + assert "SUBAGENT_OTEL_FILE_CONTENT" not in ( + _string_attribute(requesting_chats[0], "gen_ai.input.messages") or "" + ) + finals = [ + s + for s in child_chats + if "SUBAGENT_OTEL_CHILD_DONE" in (_string_attribute(s, "gen_ai.output.messages") or "") + ] + assert len(finals) == 1 + assert "SUBAGENT_OTEL_FILE_CONTENT" in ( + _string_attribute(finals[0], "gen_ai.input.messages") or "" + ) + # --------------------------------------------------------------------------- # Unit-style tests mirroring dotnet/test/TelemetryTests.cs 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/e2e/testharness/ahp.py b/python/e2e/testharness/ahp.py new file mode 100644 index 0000000000..7475f56eb1 --- /dev/null +++ b/python/e2e/testharness/ahp.py @@ -0,0 +1,62 @@ +"""Test-side subprocess using the standard TypeScript AHP 0.9 client.""" + +import asyncio +import json +from pathlib import Path +from typing import Any + + +class AhpTestClient: + def __init__(self): + self._process: asyncio.subprocess.Process | None = None + self._lock = asyncio.Lock() + + async def __aenter__(self): + sdk = Path(__file__).parents[3] + self._process = await asyncio.create_subprocess_exec( + "node", + "--import", + "tsx", + str(sdk / "nodejs/test/e2e/harness/ahpTestDriver.ts"), + cwd=sdk / "nodejs", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + limit=4 * 1024 * 1024, + ) + try: + assert await self._read() == {"ready": True} + except BaseException: + if self._process.returncode is None: + self._process.kill() + await self._process.wait() + raise + return self + + async def _read(self) -> dict[str, Any]: + assert self._process and self._process.stdout + line = await asyncio.wait_for(self._process.stdout.readline(), 60) + if not line: + raise RuntimeError("AHP test client closed its output unexpectedly") + return json.loads(line) + + async def request(self, command: dict[str, Any]) -> dict[str, Any]: + async with self._lock: + assert self._process and self._process.stdin + self._process.stdin.write((json.dumps(command) + "\n").encode()) + await self._process.stdin.drain() + response = await self._read() + if "error" in response: + raise RuntimeError(response["error"]) + return response["result"] + + async def __aexit__(self, *_): + assert self._process and self._process.stdin + self._process.stdin.close() + try: + code = await asyncio.wait_for(self._process.wait(), 10) + except TimeoutError: + self._process.kill() + await self._process.wait() + raise + if code != 0: + raise RuntimeError(f"AHP test client exited with status {code}") diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 7028923273..cb1e34fbb4 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -102,6 +102,7 @@ async def setup(self, cli_args: list[str] | None = None): DEFAULT_GITHUB_TOKEN, { "login": "e2e-test-user", + "id": 12345, "copilot_plan": "individual_pro", "endpoints": { "api": self.proxy_url, 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 f90b1a1865..e8690212dc 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() @@ -138,7 +156,7 @@ class TestClientShutdown: async def test_stop_requests_runtime_shutdown_for_owned_process(self): calls: list[str] = [] process = Mock() - process.poll.return_value = None + process.poll.side_effect = [None, 0] process.wait.return_value = 0 class Runtime: @@ -154,12 +172,9 @@ async def shutdown(self, *, timeout=None): await client.stop() assert calls == ["runtime.shutdown"] - # The runtime never self-exits after runtime.shutdown (it keeps its - # JSON-RPC server alive to send the response and leaves termination to - # the caller), so stop() terminates the owned process. The mocked - # process exits on terminate() (wait returns immediately), so we never - # escalate to kill(). - process.terminate.assert_called_once() + process.stdin.close.assert_called_once() + process.wait.assert_called_once_with(timeout=10) + process.terminate.assert_not_called() process.kill.assert_not_called() @pytest.mark.asyncio @@ -788,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)) @@ -921,6 +1097,7 @@ async def mock_request(method, params, **kwargs): deny=["Shell(git push)"], ask=["Domain(publish.example)"], allow=["Read(**)"], + limit_to=["Domain(github.com)"], ) ), ) @@ -944,6 +1121,7 @@ async def mock_request(method, params, **kwargs): "deny": ["Shell(git push)"], "ask": ["Domain(publish.example)"], "allow": ["Read(**)"], + "limitTo": ["Domain(github.com)"], } } assert captured["session.resume"]["managedSettings"] == { @@ -1055,12 +1233,12 @@ async def mock_request(method, params, **kwargs): await client.create_session( on_permission_request=PermissionHandler.approve_all, managed_settings=ManagedSettings( - permissions=ManagedSettingsPermissions(deny=[], ask=[], allow=[]) + permissions=ManagedSettingsPermissions(deny=[], ask=[], allow=[], limit_to=[]) ), ) assert captured["session.create"]["managedSettings"] == { - "permissions": {"deny": [], "ask": [], "allow": []} + "permissions": {"deny": [], "ask": [], "allow": [], "limitTo": []} } finally: await client.force_stop() @@ -1697,6 +1875,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( @@ -2375,6 +2593,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", @@ -2386,6 +2605,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" @@ -2420,6 +2640,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, }, @@ -2430,11 +2651,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)) @@ -2624,6 +2887,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..ac3938df5d 100644 --- a/python/test_connectors.py +++ b/python/test_connectors.py @@ -1,18 +1,79 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + from unittest.mock import AsyncMock, Mock import pytest from copilot.generated.rpc import ( + AuthInfoType, ConnectorAccountRequest, ConnectorAvailability, ConnectorConnectRequest, ConnectorConnectResultKind, ConnectorContinueRequest, + ConnectorDiscoveryAccount, + ConnectorDiscoveryAccountList, + ConnectorDiscoveryAvailability, + ConnectorDiscoveryCapabilities, ConnectorReconcileRequest, + ServerRpc, ) from copilot.session import CopilotSession +@pytest.mark.parametrize("availability", ["disabled", "enabled", "unavailable"]) +@pytest.mark.parametrize( + ("result_type", "payload"), + [ + (ConnectorDiscoveryAccountList, {"availability": "enabled", "accounts": []}), + ( + ConnectorDiscoveryCapabilities, + { + "apiVersion": 1, + "availability": "enabled", + "conditionalCache": True, + "opaqueAccountSelection": True, + }, + ), + ], +) +def test_discovery_availability_is_distinct_from_session_availability( + result_type, payload, availability +): + assert ConnectorDiscoveryAvailability is not ConnectorAvailability + payload = {**payload, "availability": availability} + result = result_type.from_dict(payload) + assert result.availability is ConnectorDiscoveryAvailability(availability) + assert result.to_dict() == payload + + +@pytest.mark.asyncio +async def test_client_connector_accounts_are_typed(): + transport = Mock() + transport.request = AsyncMock( + return_value={ + "availability": "enabled", + "accounts": [ + { + "accountId": "account-1", + "authInfo": { + "type": "user", + "host": "https://github.com", + "login": "octocat", + }, + } + ], + } + ) + + result = await ServerRpc(transport).connectors.get_accounts() + + assert len(result.accounts) == 1 + assert isinstance(result.accounts[0], ConnectorDiscoveryAccount) + assert result.accounts[0].account_id == "account-1" + assert result.accounts[0].auth_info.login == "octocat" + + @pytest.mark.asyncio async def test_session_connectors_exposes_host_lifecycle_api(): transport = Mock() @@ -21,6 +82,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 +176,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 101b8ab797..5d0c656c44 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -21,7 +21,9 @@ ElicitationRequestedMode, ElicitationRequestedSchema, ManagedSettingsResolvedSource, + PermissionApprovalEvaluationReasonCode, PermissionPromptRequestMemory, + PermissionRequestedData, PermissionRequestMemory, PermissionRequestMemoryAction, SessionAutoTierSwitchFailedData, @@ -30,6 +32,7 @@ SessionResumeData, SessionStartData, SessionTaskCompleteData, + ToolExecutionCompleteData, UserMessageAgentMode, session_event_from_dict, session_event_to_dict, @@ -39,6 +42,91 @@ 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 + + @staticmethod + def _permission_requested_with_reason(reason_code): + return { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-09-18T22:00:00+00:00", + "parentId": None, + "type": "permission.requested", + "data": { + "requestId": "request-1", + "permissionRequest": { + "kind": "memory", + "fact": "fact", + "assistedApproval": { + "recommendation": "requireApproval", + "evaluation": { + "reasonCode": reason_code, + "judgeStatus": "not_called", + "evaluationStage": "pre_judge", + "judgeAttempted": False, + }, + }, + }, + }, + } + + @pytest.mark.parametrize("reason", list(PermissionApprovalEvaluationReasonCode)) + def test_permission_event_decodes_every_published_reason_code(self, reason): + wire = self._permission_requested_with_reason(reason.value) + + event = session_event_from_dict(wire) + assert isinstance(event.data, PermissionRequestedData) + evaluation = event.data.permission_request.assisted_approval.evaluation + assert evaluation.reason_code is reason + assert session_event_to_dict(event) == wire + + def test_shell_evidence_reason_codes_are_published(self): + assert { + "content-excluded", + "unsupported-command-shape", + "unsupported-source", + "dynamic-source", + "too-many-sources", + "executable-unavailable", + "executable-too-large", + } <= {reason.value for reason in PermissionApprovalEvaluationReasonCode} + + def test_future_reason_code_decodes_as_unknown(self): + event = session_event_from_dict( + self._permission_requested_with_reason("future-runtime-reason") + ) + + assert isinstance(event.data, PermissionRequestedData) + evaluation = event.data.permission_request.assisted_approval.evaluation + assert evaluation.reason_code is PermissionApprovalEvaluationReasonCode.UNKNOWN + assert evaluation.judge_status.value == "not_called" + assert evaluation.evaluation_stage.value == "pre_judge" + + def test_non_string_reason_code_still_raises(self): + with pytest.raises(ValueError): + PermissionApprovalEvaluationReasonCode(7) + @pytest.mark.parametrize( ("event_type", "data"), [("session.idle", {}), ("user.message", {"content": "hello"})] ) @@ -123,6 +211,15 @@ def test_object_event_preserves_complete_envelope(self, event_type, data): "command": "cat example", }, ), + ( + "sandbox.decision", + "access_recorded", + { + "denialClass": "filesystem_write", + "permissiveSource": "policy", + "deniedResource": "/example", + }, + ), ( "sandbox.decision", "bypass_decided", 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_github_token_provider.py b/python/test_github_token_provider.py index 9566a4f817..6da93a6633 100644 --- a/python/test_github_token_provider.py +++ b/python/test_github_token_provider.py @@ -19,6 +19,7 @@ def __init__(self, *, fail_method: str | None = None) -> None: self.fail_method = fail_method self.requests: list[tuple[str, dict[str, Any]]] = [] self.request_handlers: dict[str, Any] = {} + self.raw_request_handlers: dict[str, Any] = {} self.notification_method_handlers: dict[str, Any] = {} async def request(self, method: str, params: dict[str, Any], **kwargs: Any) -> dict[str, Any]: @@ -40,9 +41,12 @@ async def request(self, method: str, params: dict[str, Any], **kwargs: Any) -> d async def stop(self) -> None: pass - def set_request_handler(self, method: str, handler: Any) -> None: + def set_request_handler(self, method: str, handler: Any, **_options: Any) -> None: self.request_handlers[method] = handler + def set_raw_request_handler(self, method: str, handler: Any) -> None: + self.raw_request_handlers[method] = handler + def set_notification_method_handler(self, method: str, handler: Any) -> None: self.notification_method_handlers[method] = handler diff --git a/python/test_host.py b/python/test_host.py new file mode 100644 index 0000000000..546d797ccb --- /dev/null +++ b/python/test_host.py @@ -0,0 +1,439 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +import asyncio +from unittest.mock import AsyncMock, Mock + +import pytest + +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(): + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + + async def request(method, params, **_): + if method == "host.start": + return {"hostId": params["hostId"], "url": "ws://127.0.0.1:12345", "token": "secret"} + if method == "host.publishSession": + return { + "sessionId": params["sessionId"], + "sessionUri": f"copilot:/{params['sessionId']}", + } + if method in ("session.create", "session.resume"): + return {"sessionId": params["sessionId"]} + return {} + + rpc = Mock(request=AsyncMock(side_effect=request)) + client._client = rpc + client._rpc = ServerRpc(rpc) + client._state = "connected" + return client, rpc + + +def handoff(host, *, resume=False, **config): + return { + "hostId": host.host_id, + "handoffId": "participation", + "resume": resume, + "config": {"sessionId": "session", "workingDirectory": "/workspace", **config}, + } + + +def release(client, host): + client._ahp_hosts.notification( + "host.sessionReleased", {"hostId": host.host_id, "handoffId": "participation"} + ) + + +@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( + 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, + "localServer": {"port": 0, "requireConnectionToken": False}, + } + replacement = Mock(request=AsyncMock()) + client._client = replacement + published = await host.publish_session("resident") + assert published.session_id == "resident" + assert await asyncio.gather(host.dispose(), host.dispose()) == [None, None] + assert await host.dispose() is None + assert sum(call.args[0] == "host.dispose" for call in rpc.request.call_args_list) == 3 + replacement.request.assert_not_called() + for _ in range(2): + client._ahp_hosts.notification( + "host.exited", {"hostId": host.host_id, "reason": "disposed"} + ) + client._ahp_hosts.disconnect() + await asyncio.sleep(0) + exited.assert_called_once() + assert exited.call_args.args[0].reason.value == "disposed" + + +@pytest.mark.parametrize("resume", [False, True]) +async def test_factories_preserve_configuration_and_release_exact_original_once(resume): + client, _ = client_fixture() + released = Mock() + originals = [] + cancellations = [] + + async def create(request): + cancellations.append(request.cancellation_event) + original = await client.create_session(**request.config) + originals.append(original) + return original + + async def restore(request): + cancellations.append(request.cancellation_event) + original = await client.resume_session(request.session_id, **request.config) + originals.append(original) + return original + + host = await client.start_ahp_host( + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + resume_session=restore, + on_session_released=released, + ) + ) + config = ( + {"continuePendingWork": False, "suppressResumeEvent": True} + if resume + else {"streaming": True} + ) + result = await client._ahp_hosts.materialize( + handoff( + host, + resume=resume, + model="test-model", + configDir="/config", + mcpOAuthTokenStorage="in-memory", + gitHubToken="test-auth", + additionalDirectories=[], + enableMcpApps=True, + enableExperimentalMode=False, + infiniteSessions={"enabled": False, "backgroundCompactionThreshold": 0.7}, + featureFlags={"Arbitrary.MixedCase": True}, + **config, + ) + ) + assert result == {"sessionId": "session"} + client._ahp_hosts.notification( + "host.sessionReleased", {"hostId": "wrong-host", "handoffId": "participation"} + ) + assert not cancellations[0].is_set() + release(client, host) + release(client, host) + client._ahp_hosts.disconnect() + await asyncio.sleep(0) + assert cancellations[0].is_set() + released.assert_called_once_with(originals[0]) + assert client._get_session("session") is originals[0] + + +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(local_server=HostLocalServerOptions(), resume_session=factory) + ) + rpc.request.reset_mock() + assert await client._ahp_hosts.materialize(handoff(host, resume=True)) == { + "sessionId": "session" + } + rpc.request.assert_not_called() + + +@pytest.mark.parametrize("wrong", ["settings", "identity", "owner"]) +async def test_invalid_factory_result_is_rejected_and_released_without_destruction(wrong): + client, rpc = client_fixture() + other, _ = client_fixture() + released = Mock() + originals = [] + + async def create(request): + config = dict(request.config) + if wrong == "settings": + config["working_directory"] = "/wrong" + if wrong == "identity": + config["session_id"] = "wrong" + original = await (other if wrong == "owner" else client).create_session(**config) + originals.append(original) + return original + + host = await client.start_ahp_host( + 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)) + await asyncio.sleep(0) + released.assert_called_once_with(originals[0]) + assert not any( + call.args[0] in ("session.destroy", "session.detach") for call in rpc.request.call_args_list + ) + + +async def test_cancelled_handoff_unblocks_before_late_factory_and_releases_it_once(): + client, _ = client_fixture() + started, finish, delivered = asyncio.Event(), asyncio.Event(), asyncio.Event() + requests, originals, released = [], [], [] + + async def create(request): + requests.append(request) + originals.append(await client.create_session(**request.config)) + started.set() + await finish.wait() + return originals[0] + + def on_released(session): + released.append(session) + delivered.set() + + host = await client.start_ahp_host( + 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) + with pytest.raises(ValueError, match="handoff already exists"): + await client._ahp_hosts.materialize(handoff(host)) + release(client, host) + with pytest.raises(RuntimeError, match="handoff ended"): + await asyncio.wait_for(task, 1) + assert requests[0].cancellation_event.is_set() + assert released == [] + finish.set() + await asyncio.wait_for(delivered.wait(), 1) + client._ahp_hosts.disconnect() + assert released == originals + assert client._get_session("session") is originals[0] + + +@pytest.mark.parametrize("resume", [False, True]) +async def test_cancelled_factory_returns_rpc_error_and_releases_handoff(resume): + client, _ = client_fixture() + requests = [] + + async def factory(request): + requests.append(request) + work = asyncio.create_task(asyncio.sleep(0)) + work.cancel() + await work + + host = await client.start_ahp_host( + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=factory, + resume_session=factory, + ) + ) + rpc = JsonRpcClient(Mock()) + rpc._send_message = AsyncMock() + await asyncio.wait_for( + rpc._dispatch_request( + { + "jsonrpc": "2.0", + "id": "materialize", + "method": "host.materializeSession", + "params": handoff(host, resume=resume), + }, + client._ahp_hosts.materialize, + cancellation_id=None, + ), + 1, + ) + rpc._send_message.assert_awaited_once() + response = rpc._send_message.call_args.args[0] + assert response["id"] == "materialize" + assert response["error"]["code"] == -32603 + assert response["error"]["message"] == "AHP session factory was cancelled" + assert requests[0].cancellation_event.is_set() + assert not client._ahp_hosts._handoffs + + +async def test_start_failure_removes_factory_and_exit_callback(): + client, rpc = client_fixture() + exited = Mock() + rpc.request.side_effect = RuntimeError("bind failed") + with pytest.raises(RuntimeError, match="bind failed"): + 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() + + +@pytest.mark.parametrize("startup_fails", [False, True]) +@pytest.mark.parametrize("cancel_mode", ["cancel", "timeout"]) +async def test_cancelled_start_settles_before_cleanup_on_original_connection( + startup_fails, cancel_mode +): + client, rpc = client_fixture() + accepted, finish = asyncio.Event(), asyncio.Event() + disposed, finish_dispose = asyncio.Event(), asyncio.Event() + host_ids = [] + + async def request(method, params, **_): + if method == "host.start": + host_ids.append(params["hostId"]) + accepted.set() + await finish.wait() + if startup_fails: + raise RuntimeError("bind failed") + return {"hostId": params["hostId"], "url": "ws://127.0.0.1:12345"} + assert method == "host.dispose" + assert params == {"hostId": host_ids[0]} + disposed.set() + await finish_dispose.wait() + return {} + + rpc.request.side_effect = request + 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() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, 1) + else: + with pytest.raises(TimeoutError): + await asyncio.wait_for(task, 0) + assert host_ids[0] in client._ahp_hosts._hosts + replacement = Mock(request=AsyncMock()) + client._client = replacement + finish.set() + if not startup_fails: + await asyncio.wait_for(disposed.wait(), 1) + assert host_ids[0] in client._ahp_hosts._hosts + finish_dispose.set() + await asyncio.wait_for(asyncio.gather(*client._ahp_hosts._tasks), 1) + assert not client._ahp_hosts._hosts + assert disposed.is_set() is not startup_fails + replacement.request.assert_not_called() + + +@pytest.mark.parametrize("resume", [False, True]) +async def test_failed_factory_does_not_leave_capture_on_retained_original(resume): + client, rpc = client_fixture() + originals = [] + + async def create(request): + originals.append(await client.create_session(**request.config)) + raise RuntimeError("application setup failed") + + async def restore(request): + if originals: + return originals[0] + originals.append(await client.resume_session(request.session_id, **request.config)) + raise RuntimeError("application setup failed") + + host = await client.start_ahp_host( + 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)) + rpc.request.reset_mock() + assert await client._ahp_hosts.materialize( + handoff(host, resume=True, continuePendingWork=False, suppressResumeEvent=True) + ) == {"sessionId": "session"} + rpc.request.assert_not_called() + assert client._get_session("session") is originals[0] + + +async def test_factory_error_propagates_and_release_callback_errors_are_logged(caplog): + client, _ = client_fixture() + host = await client.start_ahp_host( + 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)) + + def fail_release(_): + raise ValueError("release failed") + + async def create(request): + return await client.create_session(**request.config) + + host = await client.start_ahp_host( + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + on_session_released=fail_release, + ) + ) + await client._ahp_hosts.materialize(handoff(host)) + release(client, host) + await asyncio.sleep(0) + assert "AHP session release callback failed" in caplog.text diff --git a/python/test_legacy_parameters.py b/python/test_legacy_parameters.py new file mode 100644 index 0000000000..5eced46e82 --- /dev/null +++ b/python/test_legacy_parameters.py @@ -0,0 +1,95 @@ +"""Existing callers keep their construction style when a request gains fields.""" + +import dataclasses + +import pytest + +from copilot.generated import rpc + + +def _contract() -> rpc.CatalogClientContract: + return rpc.CatalogClientContract( + protocol_version=3, required_capabilities=["mcp-install-planning"] + ) + + +def _candidate() -> rpc.MCPPlanInstallSourceCandidate: + return rpc.MCPPlanInstallSourceCandidate(candidate_handle="candidate", search_id="search") + + +def _positional(cls: type) -> list[str]: + return [field.name for field in dataclasses.fields(cls) if not field.kw_only] + + +def test_plan_install_positional_scope_still_binds_scope(): + request = rpc.MCPPlanInstallRequest(_contract(), _candidate(), rpc.MCPPlanScope.USER) + + assert request.scope is rpc.MCPPlanScope.USER + assert request.policy_session_id is None + assert "policySessionId" not in request.to_dict() + assert request.to_dict()["scope"] == "user" + + +def test_added_fields_are_keyword_only(): + assert _positional(rpc.MCPPlanInstallRequest) == ["contract", "source", "scope"] + assert _positional(rpc.CatalogSearchRequest) == ["contract", "query", "kinds", "limit", "page"] + with pytest.raises(TypeError): + rpc.MCPPlanInstallRequest(_contract(), _candidate(), rpc.MCPPlanScope.USER, "session") + + +def test_search_positional_arguments_keep_their_meaning(): + request = rpc.CatalogSearchRequest(_contract(), "catalogue query", None, 4) + + assert request.limit == 4 + assert request.policy_session_id is None + assert "policySessionId" not in request.to_dict() + + +def test_new_fields_round_trip_by_keyword(): + plan = rpc.MCPPlanInstallRequest( + _contract(), _candidate(), rpc.MCPPlanScope.USER, policy_session_id="session" + ) + search = rpc.CatalogSearchRequest(_contract(), "catalogue query", policy_session_id="session") + + assert rpc.MCPPlanInstallRequest.from_dict(plan.to_dict()) == plan + assert rpc.CatalogSearchRequest.from_dict(search.to_dict()) == search + assert plan.to_dict()["policySessionId"] == "session" + assert search.to_dict()["policySessionId"] == "session" + + +def test_existing_action_export_keeps_its_name(): + assert "Action" in rpc.__all__ + assert {member.value for member in rpc.Action} == {"preserve", "transform"} + assert rpc.ProtocolMarkerSectionOverride.__dataclass_fields__["action"].type in ( + rpc.Action, + "Action", + ) + + +def test_listed_server_positional_arguments_keep_their_meaning(): + metadata = rpc.McpServerMetadata(instructions="use it") + server = rpc.MCPServer("server", rpc.McpServerStatus.CONNECTED, "Server", "failed", metadata) + + assert server.server_metadata == metadata + assert server.owned is None + assert "owned" not in server.to_dict() + assert _positional(rpc.MCPServer) == [ + "name", + "status", + "display_name", + "error", + "server_metadata", + "source", + "source_plugin", + "source_plugin_version", + ] + + +def test_listed_server_owned_marker_round_trips_by_keyword(): + server = rpc.MCPServer.from_dict( + {"name": "server", "status": "stopped", "owned": {"installationId": "installation"}} + ) + + assert server.owned.installation_id == "installation" + assert rpc.MCPServer.from_dict(server.to_dict()) == server + assert server.to_dict()["owned"] == {"installationId": "installation"} 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 84979470c3..fcba3275df 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -2,15 +2,26 @@ import inspect import json -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, Mock import pytest +from copilot._jsonrpc import JsonRpcClient +from copilot.generated.rpc import ( + ClientGlobalApiHandlers, + CustomizationReloadOutcome, + CustomizationReloadStatus, + CustomizationReloadSubsystem, + register_client_global_api_handlers, +) from copilot.rpc import ( + AuthReadValue, BuiltinToolInputSchemaType, CommandsApi, CommandsInvokeRequest, CommandsRespondToQueuedCommandRequest, + HostDisposeRequest, + HostEmptyResult, LocalSessionMetadataValue, MCPServerConfigHTTP, QueuedCommandHandled, @@ -19,6 +30,7 @@ RemoteControlStatusResult, RemoteSessionMetadataValue, SandboxConfig, + ServerRpc, SessionList, SlashCommandTextResult, TaskAgentInfo, @@ -26,6 +38,54 @@ ) +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={})) + + result = await ServerRpc(client).host.dispose(HostDisposeRequest(host_id="host")) + + assert isinstance(result, HostEmptyResult) + assert result.to_dict() == {} + client.request.assert_awaited_once_with("host.dispose", {"hostId": "host"}) + + +async def test_host_shutdown_handler_round_trips_empty_acknowledgement(): + client = JsonRpcClient(Mock()) + client._send_message = AsyncMock() + host = Mock(shutdown=AsyncMock(return_value=HostEmptyResult())) + register_client_global_api_handlers(client, ClientGlobalApiHandlers(host=host)) + + await client._dispatch_request( + {"jsonrpc": "2.0", "id": "shutdown", "method": "host.shutdown", "params": {}}, + client.request_handlers["host.shutdown"], + cancellation_id=None, + ) + + host.shutdown.assert_awaited_once() + assert isinstance(host.shutdown.call_args.args[0], HostEmptyResult) + client._send_message.assert_awaited_once_with( + {"jsonrpc": "2.0", "id": "shutdown", "result": {}} + ) + + def test_sandbox_config_round_trips_allow_bypass_and_omits_when_absent(): configured = SandboxConfig(enabled=True, allow_bypass=True) @@ -51,6 +111,25 @@ def test_mcp_oauth_scopes_preserve_existing_positional_parameters(): assert config.oidc is True +def test_auth_read_value_preserves_existing_positional_parameters(): + assert list(inspect.signature(AuthReadValue).parameters) == [ + "kind", + "account", + "status", + "errors", + "auth_info", + ] + payload = { + "kind": "activeAccount", + "authInfo": {"type": "user", "host": "https://github.com", "login": "octocat"}, + } + result = AuthReadValue.from_dict(payload) + assert result.auth_info.login == "octocat" + assert result.status is None + assert result.errors is None + assert result.to_dict() == payload + + @pytest.mark.asyncio async def test_commands_invoke_deserializes_slash_command_result(): client = AsyncMock() 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_skill_provider.py b/python/test_skill_provider.py new file mode 100644 index 0000000000..8d7208b5eb --- /dev/null +++ b/python/test_skill_provider.py @@ -0,0 +1,673 @@ +"""Skill provider unit tests.""" + +import asyncio +import io +import json +from collections.abc import Awaitable, Callable +from typing import Any +from unittest.mock import AsyncMock, Mock + +import pytest + +from copilot import ( + CloudSessionOptions, + CopilotClient, + RuntimeConnection, + SkillProvider, + SkillProviderDescriptor, +) +from copilot._jsonrpc import JsonRpcClient, JsonRpcError +from copilot.session import CopilotSession + + +class _SyncSkillProvider: + def __init__( + self, + *, + skills: list[SkillProviderDescriptor] | None = None, + markdown: dict[str, str | None] | None = None, + ) -> None: + self.skills = skills + self.markdown = markdown or {} + self.list_calls = 0 + self.read_calls: list[str] = [] + + def list_skills(self) -> list[SkillProviderDescriptor] | None: + self.list_calls += 1 + return self.skills + + def read_skill(self, name: str) -> str | None: + self.read_calls.append(name) + return self.markdown.get(name) + + +class _AsyncSkillProvider: + def __init__( + self, + *, + skills: list[SkillProviderDescriptor] | None = None, + markdown: dict[str, str | None] | None = None, + ) -> None: + self._sync = _SyncSkillProvider(skills=skills, markdown=markdown) + + @property + def list_calls(self) -> int: + return self._sync.list_calls + + @property + def read_calls(self) -> list[str]: + return self._sync.read_calls + + async def list_skills(self) -> list[SkillProviderDescriptor] | None: + await asyncio.sleep(0) + return self._sync.list_skills() + + async def read_skill(self, name: str) -> str | None: + await asyncio.sleep(0) + return self._sync.read_skill(name) + + +class _FailingSkillProvider: + def list_skills(self) -> list[SkillProviderDescriptor]: + raise RuntimeError("secret-list-token") + + def read_skill(self, name: str) -> str: + raise RuntimeError("secret-read-token") + + +class _WriteOnlyProcess: + def __init__(self) -> None: + self.stdin = io.BytesIO() + self.stdout = io.BytesIO() + self.stderr = None + + def poll(self) -> None: + return None + + +def _descriptor(name: str, description: str = "Test skill") -> SkillProviderDescriptor: + return SkillProviderDescriptor(name=name, description=description) + + +def _decode_jsonrpc_output(process: _WriteOnlyProcess) -> dict[str, Any]: + _header, payload = process.stdin.getvalue().split(b"\r\n\r\n", 1) + return json.loads(payload) + + +async def _assert_no_provider_error(session: CopilotSession, provider: _SyncSkillProvider) -> None: + list_calls = provider.list_calls + read_calls = list(provider.read_calls) + with pytest.raises(JsonRpcError) as exc_info: + await session._handle_skill_provider_list() + assert exc_info.value.code == -32603 + assert exc_info.value.message == f"No skill provider for session: {session.session_id}" + assert exc_info.value.data is None + assert provider.list_calls == list_calls + assert provider.read_calls == read_calls + + +def _install_fake_client( + client: CopilotClient, + handler: Callable[[str, dict, dict[str, Any]], Awaitable[dict[str, Any]]], +) -> None: + async def request(method: str, params: dict, **kwargs: Any) -> dict[str, Any]: + return await handler(method, params, kwargs) + + client._client = Mock() + client._client.request = request + + +async def _default_request(method: str, params: dict, kwargs: dict[str, Any]) -> dict[str, Any]: + if method == "session.create": + result = {"sessionId": params.get("sessionId") or "created-session", "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + if method == "session.resume": + return {"sessionId": params["sessionId"], "workspacePath": None} + if method == "session.options.update": + return {"success": True} + return {} + + +def test_skill_provider_is_runtime_checkable_and_exported() -> None: + assert isinstance(_SyncSkillProvider(), SkillProvider) + assert SkillProviderDescriptor(name="demo", description="Demo").to_dict() == { + "description": "Demo", + "name": "demo", + } + + +@pytest.mark.asyncio +async def test_create_and_resume_send_skill_provider_flag_only_when_configured() -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + captured: list[tuple[str, dict]] = [] + + async def request(method: str, params: dict, kwargs: dict[str, Any]) -> dict[str, Any]: + captured.append((method, dict(params))) + return await _default_request(method, params, kwargs) + + _install_fake_client(client, request) + provider = _SyncSkillProvider(skills=[_descriptor("provided")]) + + await client.create_session(session_id="create-with-provider", skill_provider=provider) + await client.resume_session("resume-with-provider", skill_provider=provider) + await client.create_session(session_id="create-without-provider") + await client.resume_session("resume-without-provider") + + payloads = {(method, params["sessionId"]): params for method, params in captured} + assert payloads[("session.create", "create-with-provider")]["hasSkillProvider"] is True + assert payloads[("session.resume", "resume-with-provider")]["hasSkillProvider"] is True + assert "hasSkillProvider" not in payloads[("session.create", "create-without-provider")] + assert "hasSkillProvider" not in payloads[("session.resume", "resume-without-provider")] + + +@pytest.mark.asyncio +async def test_empty_mode_keeps_skill_provider_dormant_by_default() -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234"), mode="empty") + captured: list[tuple[str, dict]] = [] + + async def request(method: str, params: dict, kwargs: dict[str, Any]) -> dict[str, Any]: + captured.append((method, dict(params))) + return await _default_request(method, params, kwargs) + + _install_fake_client(client, request) + provider = _SyncSkillProvider(skills=[_descriptor("provided")]) + + await client.create_session( + session_id="empty-create", + available_tools=[], + skill_provider=provider, + ) + await client.resume_session( + "empty-resume", + available_tools=[], + skill_provider=provider, + ) + + payloads = {(method, params["sessionId"]): params for method, params in captured} + assert payloads[("session.create", "empty-create")]["enableSkills"] is False + assert payloads[("session.resume", "empty-resume")]["enableSkills"] is False + assert payloads[("session.create", "empty-create")]["hasSkillProvider"] is True + assert payloads[("session.resume", "empty-resume")]["hasSkillProvider"] is True + + +@pytest.mark.asyncio +async def test_cloud_create_rejects_skill_provider_before_connecting() -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + start_mock = AsyncMock() + setattr(client, "start", start_mock) + provider = _SyncSkillProvider(skills=[_descriptor("cloud")]) + + with pytest.raises(ValueError, match="Skill providers are not supported for cloud sessions\\."): + await client.create_session( + cloud=CloudSessionOptions(), + skill_provider=provider, + ) + + start_mock.assert_not_awaited() + assert provider.list_calls == 0 + assert provider.read_calls == [] + + +@pytest.mark.asyncio +async def test_early_skill_provider_callbacks_are_served_during_create_and_resume() -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + observed: list[dict] = [] + provider = _SyncSkillProvider( + skills=[_descriptor("early")], + markdown={"early": "early skill body"}, + ) + + async def request(method: str, params: dict, kwargs: dict[str, Any]) -> dict[str, Any]: + if method in ("session.create", "session.resume"): + observed.append( + await client._handle_skill_provider_list({"sessionId": params["sessionId"]}) + ) + observed.append( + await client._handle_skill_provider_read( + {"sessionId": params["sessionId"], "name": "early"} + ) + ) + return await _default_request(method, params, kwargs) + + _install_fake_client(client, request) + + await client.create_session(session_id="early-create", skill_provider=provider) + await client.resume_session("early-resume", skill_provider=provider) + + assert observed == [ + {"skills": [{"description": "Test skill", "name": "early"}]}, + {"markdown": "early skill body"}, + {"skills": [{"description": "Test skill", "name": "early"}]}, + {"markdown": "early skill body"}, + ] + + +@pytest.mark.parametrize("provider_cls", [_SyncSkillProvider, _AsyncSkillProvider]) +@pytest.mark.asyncio +async def test_skill_provider_dispatch_serializes_descriptors_and_reads_markdown( + provider_cls: type[_SyncSkillProvider] | type[_AsyncSkillProvider], +) -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + provider = provider_cls( + skills=[ + _descriptor("alpha", "Alpha skill"), + SkillProviderDescriptor( + name="beta", + description="Beta skill", + argument_hint="ARG", + disable_model_invocation=True, + user_invocable=False, + ), + ], + markdown={"alpha": "# Alpha"}, + ) + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(provider) + client._sessions["session-1"] = session + + listed = await client._handle_skill_provider_list({"sessionId": "session-1"}) + read = await client._handle_skill_provider_read({"sessionId": "session-1", "name": "alpha"}) + + assert listed == { + "skills": [ + {"description": "Alpha skill", "name": "alpha"}, + { + "argumentHint": "ARG", + "description": "Beta skill", + "disableModelInvocation": True, + "name": "beta", + "userInvocable": False, + }, + ] + } + assert read == {"markdown": "# Alpha"} + + +@pytest.mark.asyncio +async def test_skill_provider_null_list_becomes_empty_catalog() -> None: + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(_SyncSkillProvider(skills=None)) + + assert await session._handle_skill_provider_list() == {"skills": []} + + +@pytest.mark.asyncio +async def test_skill_provider_read_none_returns_null_markdown() -> None: + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(_SyncSkillProvider(markdown={"missing": None})) + + assert await session._handle_skill_provider_read("missing") == {"markdown": None} + + +@pytest.mark.asyncio +async def test_skill_provider_failures_are_generic_and_do_not_leak_provider_errors() -> None: + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(_FailingSkillProvider()) + + with pytest.raises(JsonRpcError) as list_error: + await session._handle_skill_provider_list() + with pytest.raises(JsonRpcError) as read_error: + await session._handle_skill_provider_read("secret") + + assert list_error.value.code == -32603 + assert list_error.value.message == "Skill provider listSkills failed" + assert list_error.value.data is None + assert "secret-list-token" not in str(list_error.value) + assert read_error.value.code == -32603 + assert read_error.value.message == "Skill provider readSkill failed" + assert read_error.value.data is None + assert "secret-read-token" not in str(read_error.value) + + +@pytest.mark.asyncio +async def test_skill_provider_failures_are_logged_with_session_id( + caplog: pytest.LogCaptureFixture, +) -> None: + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(_FailingSkillProvider()) + + with caplog.at_level("WARNING", logger="copilot.session"): + with pytest.raises(JsonRpcError): + await session._handle_skill_provider_list() + with pytest.raises(JsonRpcError): + await session._handle_skill_provider_read("secret") + + records = [r for r in caplog.records if r.getMessage().startswith("Skill provider")] + assert [r.getMessage() for r in records] == [ + "Skill provider listSkills failed", + "Skill provider readSkill failed", + ] + assert all(getattr(r, "session_id", None) == "session-1" for r in records) + assert [str(r.exc_info[1]) for r in records if r.exc_info] == [ + "secret-list-token", + "secret-read-token", + ] + + +class _SelfCancellingSkillProvider: + async def list_skills(self) -> list[SkillProviderDescriptor]: + raise asyncio.CancelledError + + async def read_skill(self, name: str) -> str: + raise asyncio.CancelledError + + +@pytest.mark.asyncio +async def test_skill_provider_cancelled_error_raised_by_provider_is_a_failure() -> None: + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(_SelfCancellingSkillProvider()) + + with pytest.raises(JsonRpcError) as list_error: + await session._handle_skill_provider_list() + with pytest.raises(JsonRpcError) as read_error: + await session._handle_skill_provider_read("cancelled") + + assert list_error.value.message == "Skill provider listSkills failed" + assert read_error.value.message == "Skill provider readSkill failed" + + +class _BlockingSkillProvider: + def __init__(self) -> None: + self.entered = asyncio.Event() + + async def list_skills(self) -> list[SkillProviderDescriptor]: + self.entered.set() + await asyncio.Event().wait() + return [] + + async def read_skill(self, name: str) -> str: + return "" + + +@pytest.mark.asyncio +async def test_skill_provider_dispatch_cancellation_propagates() -> None: + session = CopilotSession("session-1", Mock()) + provider = _BlockingSkillProvider() + session._register_skill_provider(provider) + + task = asyncio.create_task(session._handle_skill_provider_list()) + await provider.entered.wait() + task.cancel() + + with pytest.raises(asyncio.CancelledError): + await task + + +class _JsonRpcErrorSkillProvider: + def list_skills(self) -> list[SkillProviderDescriptor]: + raise JsonRpcError(-32000, "secret-list-token", data={"code": "provider_secret"}) + + def read_skill(self, name: str) -> str: + raise JsonRpcError(-32000, "secret-read-token", data={"code": "provider_secret"}) + + +@pytest.mark.asyncio +async def test_skill_provider_json_rpc_errors_from_provider_are_not_forwarded() -> None: + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(_JsonRpcErrorSkillProvider()) + + with pytest.raises(JsonRpcError) as list_error: + await session._handle_skill_provider_list() + with pytest.raises(JsonRpcError) as read_error: + await session._handle_skill_provider_read("secret") + + assert (list_error.value.code, list_error.value.message, list_error.value.data) == ( + -32603, + "Skill provider listSkills failed", + None, + ) + assert (read_error.value.code, read_error.value.message, read_error.value.data) == ( + -32603, + "Skill provider readSkill failed", + None, + ) + + +@pytest.mark.asyncio +async def test_skill_provider_unknown_session_and_no_provider_return_generic_errors() -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + + with pytest.raises(JsonRpcError) as unknown_error: + await client._handle_skill_provider_list({"sessionId": "missing-session"}) + assert (unknown_error.value.code, unknown_error.value.message, unknown_error.value.data) == ( + -32603, + "Session not found: missing-session", + None, + ) + + client._sessions["session-1"] = CopilotSession("session-1", Mock()) + with pytest.raises(JsonRpcError) as no_provider_error: + await client._handle_skill_provider_read({"sessionId": "session-1", "name": "demo"}) + assert ( + no_provider_error.value.code, + no_provider_error.value.message, + no_provider_error.value.data, + ) == ( + -32603, + "No skill provider for session: session-1", + None, + ) + + +@pytest.mark.parametrize("method", ["list", "read"]) +@pytest.mark.asyncio +async def test_skill_provider_invalid_params_are_json_rpc_invalid_params(method: str) -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + params = {"sessionId": "session-1"} if method == "read" else {} + handler = ( + client._handle_skill_provider_read + if method == "read" + else client._handle_skill_provider_list + ) + + with pytest.raises(JsonRpcError) as exc_info: + await handler(params) + + assert exc_info.value.code == -32602 + assert exc_info.value.message == "Invalid params" + assert exc_info.value.data is None + + +@pytest.mark.asyncio +async def test_skill_provider_read_none_is_written_to_json_rpc_result() -> None: + sdk_client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + process = _WriteOnlyProcess() + rpc_client = JsonRpcClient(process) + sdk_client._client = rpc_client + sdk_client._register_session_scoped_request_handlers() + session = CopilotSession("session-1", rpc_client) + session._register_skill_provider(_SyncSkillProvider(markdown={"missing": None})) + sdk_client._sessions["session-1"] = session + + await rpc_client._dispatch_request( + { + "jsonrpc": "2.0", + "id": 1, + "method": "skillProvider.read", + "params": {"sessionId": "session-1", "name": "missing"}, + }, + rpc_client.request_handlers["skillProvider.read"], + None, + ) + + response = _decode_jsonrpc_output(process) + assert response == { + "jsonrpc": "2.0", + "id": 1, + "result": {"markdown": None}, + } + + +@pytest.mark.parametrize("method", ["list", "read"]) +@pytest.mark.asyncio +async def test_skill_provider_task_is_cancelled_by_runtime_cancel_request( + method: str, caplog: pytest.LogCaptureFixture +) -> None: + entered = asyncio.Event() + cancelled = asyncio.Event() + + class BlockingProvider: + async def _block(self) -> Any: + entered.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + cancelled.set() + raise + + async def list_skills(self) -> list[SkillProviderDescriptor]: + return await self._block() + + async def read_skill(self, name: str) -> str | None: + return await self._block() + + sdk_client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + process = _WriteOnlyProcess() + rpc_client = JsonRpcClient(process) + rpc_client._loop = asyncio.get_running_loop() + sdk_client._client = rpc_client + sdk_client._register_session_scoped_request_handlers() + session = CopilotSession("session-1", rpc_client) + session._register_skill_provider(BlockingProvider()) + sdk_client._sessions["session-1"] = session + params = {"sessionId": "session-1", "name": "demo"} + + with caplog.at_level("WARNING", logger="copilot.session"): + rpc_client._handle_request( + {"jsonrpc": "2.0", "id": 7, "method": f"skillProvider.{method}", "params": params} + ) + await asyncio.wait_for(entered.wait(), timeout=5) + rpc_client._handle_cancel_request({"id": 7}) + await asyncio.wait_for(cancelled.wait(), timeout=5) + # The response is written before dispatch cleanup retires the cancellation entry. + async with asyncio.timeout(5): + while not process.stdin.getvalue() or 7 in rpc_client._incoming_request_cancellations: + await asyncio.sleep(0.01) + + assert _decode_jsonrpc_output(process) == { + "jsonrpc": "2.0", + "id": 7, + "error": {"code": -32800, "message": "Request cancelled", "data": None}, + } + assert not [r for r in caplog.records if r.getMessage().startswith("Skill provider")] + assert rpc_client._incoming_request_cancellations == {} + + +@pytest.mark.parametrize("teardown", ["disconnect", "mark", "force_stop", "delete", "close"]) +@pytest.mark.asyncio +async def test_skill_provider_is_cleared_on_teardown_paths(teardown: str) -> None: + provider = _SyncSkillProvider(skills=[_descriptor("demo")]) + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(provider) + + if teardown == "disconnect": + session._client.request = AsyncMock(return_value={"success": True}) + await session.disconnect() + elif teardown == "mark": + session._mark_disconnected() + elif teardown == "force_stop": + client._sessions["session-1"] = session + client._client = Mock() + client._client.stop = AsyncMock() + await client.force_stop() + elif teardown == "delete": + client._sessions["session-1"] = session + client._client = Mock() + client._client.request = AsyncMock(return_value={"success": True}) + await client.delete_session("session-1") + else: + client._sessions["session-1"] = session + client._client = Mock(_loop=asyncio.get_running_loop()) + client._handle_connection_close() + await asyncio.sleep(0) + + await _assert_no_provider_error(session, provider) + + +@pytest.mark.parametrize("operation", ["create", "resume"]) +@pytest.mark.asyncio +async def test_skill_provider_is_cleared_after_failed_create_or_resume(operation: str) -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + provider = _SyncSkillProvider(skills=[_descriptor("demo")]) + captured_session: CopilotSession | None = None + + async def request(method: str, params: dict, _kwargs: dict[str, Any]) -> dict[str, Any]: + nonlocal captured_session + if method == f"session.{operation}": + captured_session = client._sessions[params["sessionId"]] + raise RuntimeError("admission failed") + return {} + + _install_fake_client(client, request) + + with pytest.raises(RuntimeError, match="admission failed"): + if operation == "create": + await client.create_session(session_id="failed-session", skill_provider=provider) + else: + await client.resume_session("failed-session", skill_provider=provider) + + assert "failed-session" not in client._sessions + assert captured_session is not None + await _assert_no_provider_error(captured_session, provider) + + +@pytest.mark.asyncio +async def test_failed_resume_restores_the_resident_skill_provider() -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + resident = _SyncSkillProvider(skills=[_descriptor("demo")]) + replacement = _SyncSkillProvider(skills=[_descriptor("demo")]) + + async def request(method: str, params: dict, kwargs: dict[str, Any]) -> dict[str, Any]: + if method == "session.resume": + raise RuntimeError("resume failed") + return await _default_request(method, params, kwargs) + + _install_fake_client(client, request) + session = await client.create_session(session_id="session-1", skill_provider=resident) + + with pytest.raises(RuntimeError, match="resume failed"): + await client.resume_session("session-1", skill_provider=replacement) + + assert client._sessions["session-1"] is session + assert await client._handle_skill_provider_list({"sessionId": "session-1"}) == { + "skills": [{"name": "demo", "description": "Test skill"}] + } + assert (resident.list_calls, replacement.list_calls) == (1, 0) + + +@pytest.mark.asyncio +async def test_resume_rebinds_or_unbinds_skill_provider() -> None: + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + provider_a = _SyncSkillProvider( + skills=[_descriptor("demo")], + markdown={"demo": "from A"}, + ) + provider_b = _SyncSkillProvider( + skills=[_descriptor("demo")], + markdown={"demo": "from B"}, + ) + _install_fake_client(client, _default_request) + + session = CopilotSession("session-1", Mock()) + session._register_skill_provider(provider_a) + client._sessions["session-1"] = session + + await client.resume_session("session-1", skill_provider=provider_b) + assert await client._handle_skill_provider_read({"sessionId": "session-1", "name": "demo"}) == { + "markdown": "from B" + } + assert provider_a.read_calls == [] + + await client.resume_session("session-1") + with pytest.raises(JsonRpcError) as exc_info: + await client._handle_skill_provider_list({"sessionId": "session-1"}) + assert (exc_info.value.code, exc_info.value.message, exc_info.value.data) == ( + -32603, + "No skill provider for session: session-1", + None, + ) + assert provider_b.list_calls == 0 diff --git a/python/test_stdio_shutdown.py b/python/test_stdio_shutdown.py new file mode 100644 index 0000000000..1ce8388b1f --- /dev/null +++ b/python/test_stdio_shutdown.py @@ -0,0 +1,166 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Public lifecycle regressions for host cleanup after stdio EOF.""" + +import asyncio +import shutil +import subprocess +import sys +import time +from pathlib import Path + +import pytest + +from copilot import CopilotClient, RuntimeConnection + +_RUNTIME = Path(__file__).parent.parent / "test" / "harness" / "stdio-shutdown-runtime.cjs" + +_EXIT_PROBE = """ +const fs = require("node:fs"); +const { spawnSync } = require("node:child_process"); +const pid = Number(fs.readFileSync(process.argv[1], "utf8")); +const deadline = Date.now() + Number(process.argv[2]); +function check() { + try { + process.kill(pid, 0); + } catch (error) { + if (error.code === "ESRCH") return; + throw error; + } + // Force-stop does not reap children; a zombie has already exited. + if (process.platform === "linux") { + try { + const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8"); + // The parenthesized command name can itself contain spaces and parentheses. + if (stat.charAt(stat.lastIndexOf(")") + 2) === "Z") return; + } catch (error) { + if (error.code === "ENOENT") return; + throw error; + } + } else if (process.platform !== "win32") { + const status = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf8" }); + if (status.status === 0 && status.stdout.trim().startsWith("Z")) return; + } + if (Date.now() >= deadline) throw new Error("Child still running"); + setTimeout(check, 25); +} +check(); +""" + + +async def _assert_child_exited(directory: Path, wait_millis: int = 0) -> None: + node = shutil.which("node") + assert node is not None + result = await asyncio.to_thread( + subprocess.run, + [node, "-e", _EXIT_PROBE, str(directory / "pid"), str(wait_millis)], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +async def test_exit_probe_distinguishes_running_and_exited_child(tmp_path, monkeypatch): + node = shutil.which("node") + assert node is not None + if sys.platform == "linux": + # Linux must not depend on procps options absent from Alpine's BusyBox ps. + (tmp_path / "node").symlink_to(node) + monkeypatch.setenv("PATH", str(tmp_path)) + child = subprocess.Popen([node, "-e", "setInterval(() => {}, 1000)"]) + try: + (tmp_path / "pid").write_text(str(child.pid)) + with pytest.raises(AssertionError, match="Child still running"): + await _assert_child_exited(tmp_path) + child.kill() + # Keep the Popen alive without wait/poll so POSIX retains an exited zombie. + await _assert_child_exited(tmp_path, wait_millis=5000) + finally: + child.kill() + child.wait(timeout=10) + + +def _client(directory: Path, mode: str) -> CopilotClient: + node = shutil.which("node") + assert node is not None, "Node.js is required for the shared SDK shutdown fixture" + return CopilotClient( + connection=RuntimeConnection.for_stdio( + path=node, + args=[ + str(_RUNTIME), + str(directory / "cleanup.jsonl"), + mode, + str(directory / "pid"), + ], + ) + ) + + +@pytest.mark.parametrize("mode", ["stop", "dispose"]) +@pytest.mark.timeout(60) +async def test_graceful_shutdown_waits_for_host_cleanup(tmp_path, mode): + client = _client(tmp_path, mode) + try: + if mode == "dispose": + async with client: + assert not (tmp_path / "cleanup.jsonl").exists() + else: + await client.start() + assert not (tmp_path / "cleanup.jsonl").exists() + await client.stop() + assert (tmp_path / "cleanup.jsonl").read_text() == '{"type":"span"}\n' + await _assert_child_exited(tmp_path) + with pytest.raises(RuntimeError, match="Client is not connected"): + _ = client.rpc + finally: + await client.force_stop() + + +@pytest.mark.timeout(60) +async def test_force_stop_does_not_run_graceful_host_cleanup(tmp_path): + client = _client(tmp_path, "force") + try: + await client.start() + started = time.monotonic() + await asyncio.wait_for(client.force_stop(), timeout=30) + assert time.monotonic() - started < 10 + await _assert_child_exited(tmp_path, wait_millis=5000) + assert not (tmp_path / "cleanup.jsonl").exists() + with pytest.raises(RuntimeError, match="Client is not connected"): + _ = client.rpc + finally: + await client.force_stop() + + +@pytest.mark.timeout(60) +async def test_stop_terminates_uncooperative_child_after_grace_period(tmp_path): + client = _client(tmp_path, "fallback") + try: + await client.start() + started = time.monotonic() + await asyncio.wait_for(client.stop(), timeout=45) + assert time.monotonic() - started >= 10 + assert (tmp_path / "cleanup.jsonl").read_text() == '{"type":"span"}\n' + await _assert_child_exited(tmp_path) + with pytest.raises(RuntimeError, match="Client is not connected"): + _ = client.rpc + finally: + await client.force_stop() + + +@pytest.mark.timeout(60) +async def test_force_stop_cleans_up_after_startup_failure(tmp_path): + client = _client(tmp_path, "start-failure") + try: + with pytest.raises(RuntimeError, match="[Pp]rotocol"): + await client.start() + await asyncio.wait_for(client.force_stop(), timeout=30) + await _assert_child_exited(tmp_path, wait_millis=5000) + assert not (tmp_path / "cleanup.jsonl").exists() + with pytest.raises(RuntimeError, match="Client is not connected"): + _ = client.rpc + finally: + await client.force_stop() 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 new file mode 100644 index 0000000000..8056ca027b --- /dev/null +++ b/python/tests/test_installation_confirmation.py @@ -0,0 +1,503 @@ +from __future__ import annotations + +import asyncio +import os +import threading +from typing import Any, cast + +import pytest + +from copilot import ( + CopilotClient, + InstallationConfirmationContext, + RuntimeConnection, +) +from copilot._jsonrpc import JsonRpcClient, JsonRpcError +from copilot.rpc import InstallationConfirmationRequest, InstallationDecision + + +class _EndpointProcess: + def __init__(self, stdin, stdout) -> None: + self.stdin = stdin + self.stdout = stdout + self.stderr = None + self.returncode: int | None = None + + def poll(self) -> int | None: + return self.returncode + + def terminate(self) -> None: + self.returncode = 0 + for stream in (self.stdin, self.stdout): + try: + stream.close() + except OSError: + pass + + +class _Peer: + def __init__(self, client: JsonRpcClient) -> None: + self.client = client + + async def request(self, request_id: int | str, method: str, params: dict[str, Any]) -> Any: + assert self.client._loop is not None # noqa: SLF001 + future = self.client._loop.create_future() # noqa: SLF001 + with self.client._pending_lock: # noqa: SLF001 + pending_requests = cast( + dict[int | str, asyncio.Future[Any]], self.client.pending_requests + ) + pending_requests[request_id] = future + await self.client._send_message( # noqa: SLF001 + {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params} + ) + try: + return await asyncio.wait_for(future, timeout=5) + finally: + with self.client._pending_lock: # noqa: SLF001 + pending_requests = cast( + dict[int | str, asyncio.Future[Any]], self.client.pending_requests + ) + pending_requests.pop(request_id, None) + + async def notify(self, method: str, params: dict[str, Any]) -> None: + await self.client.notify(method, params) + + +class _Harness: + def __init__( + self, + client: CopilotClient, + sdk_rpc: JsonRpcClient, + peer_rpc: JsonRpcClient, + sdk_process: _EndpointProcess, + peer_process: _EndpointProcess, + ) -> None: + self.client = client + self.sdk_rpc = sdk_rpc + self.peer_rpc = peer_rpc + self.peer = _Peer(peer_rpc) + self.sdk_process = sdk_process + self.peer_process = peer_process + + async def close_peer_connection(self) -> None: + self.peer_process.stdin.close() + 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() + await self.sdk_rpc.stop() + + +def _pipe_process_pair() -> tuple[_EndpointProcess, _EndpointProcess]: + sdk_to_peer_read, sdk_to_peer_write = os.pipe() + peer_to_sdk_read, peer_to_sdk_write = os.pipe() + sdk_process = _EndpointProcess( + os.fdopen(sdk_to_peer_write, "wb", buffering=0), + os.fdopen(peer_to_sdk_read, "rb", buffering=0), + ) + peer_process = _EndpointProcess( + os.fdopen(peer_to_sdk_write, "wb", buffering=0), + os.fdopen(sdk_to_peer_read, "rb", buffering=0), + ) + return sdk_process, peer_process + + +def _request(name: str) -> dict[str, Any]: + return { + "confirmationId": f"challenge-{name}", + "operationId": name, + "policySessionId": "original-session", + "expiresAt": "2026-09-24T03:00:00Z", + "reviewFingerprint": f"fingerprint-{name}", + "review": { + "resource": "mcp", + "review": { + "action": "install", + "identity": {"canonicalName": "io.example/server", "serverName": "example"}, + "provenance": { + "authority": "cards.example.test", + "validatedAt": "2026-09-24T02:59:00Z", + "cardDigest": { + "algorithm": "sha256-rfc8785", + "value": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + "mediaType": "application/mcp-server-card+json", + }, + "target": {"scope": "user", "configKey": "example"}, + "policy": {"decision": "allowed", "source": "none"}, + "selectedChoice": { + "choiceId": "remote-choice", + "installMethod": "remote", + "endpoint": "https://example.test/mcp", + "transport": "streamable-http", + "requiredValues": [], + "secretPlaceholders": [], + }, + "configurationChange": { + "operation": "add", + "scope": "user", + "configKey": "example", + "changedFields": ["type", "url", "headers", "tools"], + "secretReferences": [], + }, + "inputs": [], + "suppliedSecrets": [], + "secretStorage": "keychain", + "effectiveConfiguration": { + "transport": "streamable-http", + "url": "https://example.test/mcp", + "headers": {"X-Region": "eu"}, + "tools": ["*"], + }, + }, + }, + } + + +async def _connect(handler=None) -> _Harness: + sdk_process, peer_process = _pipe_process_pair() + sdk_rpc = JsonRpcClient(sdk_process) + peer_rpc = JsonRpcClient(peer_process) + client = CopilotClient( + connection=RuntimeConnection.for_uri("localhost:1234"), + installation_confirmation_handler=handler, + ) + client._client = sdk_rpc # noqa: SLF001 + sdk_rpc.on_close = client._handle_connection_close # noqa: SLF001 + loop = asyncio.get_running_loop() + sdk_rpc.start(loop) + peer_rpc.start(loop) + client._register_client_global_handlers() # noqa: SLF001 + return _Harness(client, sdk_rpc, peer_rpc, sdk_process, peer_process) + + +async def _wait_for(predicate, *, timeout: float = 5.0) -> None: + deadline = asyncio.get_running_loop().time() + timeout + while not predicate(): + if asyncio.get_running_loop().time() >= deadline: + raise AssertionError("condition was not met before timeout") + 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]] = [] + + async def handler( + incoming: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ) -> InstallationDecision: + seen.append((incoming, context)) + assert not context.cancelled.is_set() + assert incoming.operation_id == "a" + assert incoming.policy_session_id == "original-session" + assert incoming.review.review.action.value == "install" + incoming.confirmation_id = "mutated" + incoming.review_fingerprint = "mutated" + return InstallationDecision.CONFIRM + + harness = await _connect(handler) + try: + response = await harness.peer.request(301, "installations.confirm", _request("a")) + assert response == { + "confirmationId": "challenge-a", + "reviewFingerprint": "fingerprint-a", + "decision": "confirm", + } + assert len(seen) == 1 + finally: + await harness.close() + + +async def test_keeps_concurrent_reviews_independent_and_serves_other_rpc() -> None: + decisions = { + "a": asyncio.get_running_loop().create_future(), + "b": asyncio.get_running_loop().create_future(), + } + seen: list[str] = [] + + def handler( + incoming: InstallationConfirmationRequest, + _context: InstallationConfirmationContext, + ): + seen.append(incoming.operation_id) + return decisions[incoming.operation_id] + + harness = await _connect(handler) + try: + first = asyncio.create_task( + harness.peer.request(401, "installations.confirm", _request("a")) + ) + second = asyncio.create_task( + harness.peer.request(402, "installations.confirm", _request("b")) + ) + await _wait_for(lambda: sorted(seen) == ["a", "b"]) + + with pytest.raises(JsonRpcError, match="No GitHub token provider"): + await harness.peer.request( + 403, + "gitHubToken.getToken", + {"registrationId": "unknown", "host": "github.com", "reason": "initial"}, + ) + + decisions["b"].set_result("decline") + assert await second == { + "confirmationId": "challenge-b", + "reviewFingerprint": "fingerprint-b", + "decision": "decline", + } + decisions["a"].set_result("confirm") + assert (await first)["confirmationId"] == "challenge-a" + finally: + await harness.close() + + +async def test_cancel_request_retires_only_cancelled_review_and_drops_late_decision() -> None: + late_decision = asyncio.get_running_loop().create_future() + contexts: dict[str, InstallationConfirmationContext] = {} + + def handler( + incoming: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ): + contexts[incoming.operation_id] = context + if incoming.operation_id == "a": + return late_decision + return "confirm" + + harness = await _connect(handler) + try: + first = asyncio.create_task( + harness.peer.request(501, "installations.confirm", _request("a")) + ) + await _wait_for(lambda: "a" in contexts) + await harness.peer.notify("$/cancelRequest", {"id": 501}) + with pytest.raises(JsonRpcError) as cancelled: + await first + assert cancelled.value.code == -32800 + assert contexts["a"].cancelled.is_set() + + late_decision.set_result("confirm") + response = await harness.peer.request(502, "installations.confirm", _request("b")) + assert response["confirmationId"] == "challenge-b" + assert not contexts["b"].cancelled.is_set() + finally: + await harness.close() + + +async def test_cancel_request_and_decision_ready_together_rejects_decision() -> None: + late_decision = asyncio.get_running_loop().create_future() + context: InstallationConfirmationContext | None = None + + def handler( + _incoming: InstallationConfirmationRequest, + incoming_context: InstallationConfirmationContext, + ): + nonlocal context + context = incoming_context + return late_decision + + harness = await _connect(handler) + try: + pending = asyncio.create_task( + harness.peer.request(551, "installations.confirm", _request("a")) + ) + await _wait_for(lambda: context is not None) + harness.sdk_rpc._handle_cancel_request({"id": 551}) # noqa: SLF001 + late_decision.set_result("confirm") + + with pytest.raises(JsonRpcError) as cancelled: + await pending + assert cancelled.value.code == -32800 + assert context is not None + assert context.cancelled.is_set() + finally: + await harness.close() + + +async def test_stale_unknown_and_string_cancel_id_do_not_touch_successor() -> None: + contexts: dict[str, InstallationConfirmationContext] = {} + + def handler( + incoming: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ): + contexts[incoming.operation_id] = context + return "decline" + + harness = await _connect(handler) + try: + await harness.peer.notify("$/cancelRequest", {"id": 601}) + await harness.peer.notify("$/cancelRequest", {"id": 999}) + await harness.peer.notify("$/cancelRequest", {"id": "602"}) + + response = await harness.peer.request(602, "installations.confirm", _request("b")) + assert response["decision"] == "decline" + assert not contexts["b"].cancelled.is_set() + finally: + await harness.close() + + +async def test_connection_close_cancels_all_reviews_and_isolated_to_original_connection() -> None: + decisions = { + "a": asyncio.get_running_loop().create_future(), + "b": asyncio.get_running_loop().create_future(), + } + originals: dict[str, InstallationConfirmationContext] = {} + + def first_handler( + incoming: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ): + originals[incoming.operation_id] = context + return decisions[incoming.operation_id] + + first = await _connect(first_handler) + second = await _connect(lambda _request, _context: "decline") + try: + pending_a = asyncio.create_task( + first.peer.request(701, "installations.confirm", _request("a")) + ) + pending_b = asyncio.create_task( + first.peer.request(702, "installations.confirm", _request("b")) + ) + await _wait_for(lambda: set(originals) == {"a", "b"}) + await first.close_peer_connection() + await _wait_for(lambda: all(context.cancelled.is_set() for context in originals.values())) + decisions["a"].set_result("confirm") + decisions["b"].set_result("decline") + pending_a.cancel() + pending_b.cancel() + await asyncio.gather(pending_a, pending_b, return_exceptions=True) + + response = await second.peer.request(703, "installations.confirm", _request("c")) + assert response["decision"] == "decline" + finally: + await first.close() + await second.close() + + +async def test_stopping_client_retires_pending_reviews() -> None: + decision = asyncio.get_running_loop().create_future() + context: InstallationConfirmationContext | None = None + + def handler(_incoming: InstallationConfirmationRequest, incoming_context): + nonlocal context + context = incoming_context + return decision + + harness = await _connect(handler) + try: + pending = asyncio.create_task( + harness.peer.request(801, "installations.confirm", _request("a")) + ) + await _wait_for(lambda: context is not None) + await harness.client.stop() + await _wait_for(lambda: context is not None and context.cancelled.is_set()) + assert context is not None + assert context.cancelled.is_set() + decision.set_result("confirm") + pending.cancel() + await asyncio.gather(pending, return_exceptions=True) + finally: + await harness.close() + + +async def test_refuses_missing_handler_invalid_review_handler_errors_and_unknown_decisions(): + missing = await _connect() + try: + with pytest.raises(JsonRpcError, match="No installations client-global handler registered"): + await missing.peer.request(901, "installations.confirm", _request("a")) + finally: + await missing.close() + + invalid = await _connect(lambda _request, _context: "confirm") + try: + with pytest.raises(JsonRpcError) as exc: + await invalid.peer.request(902, "installations.confirm", {"confirmationId": "only"}) + assert exc.value.code == -32602 + finally: + await invalid.close() + + failing = await _connect(lambda _request, _context: (_ for _ in ()).throw(RuntimeError("no"))) + try: + with pytest.raises(JsonRpcError, match="no"): + await failing.peer.request(903, "installations.confirm", _request("a")) + finally: + await failing.close() + + unknown = await _connect(lambda _request, _context: "approve") + try: + with pytest.raises(JsonRpcError, match="Invalid installation confirmation decision"): + await unknown.peer.request(904, "installations.confirm", _request("a")) + finally: + await unknown.close() + + +async def test_does_not_infer_optional_legacy_session_authority() -> None: + observed: list[str | None] = [] + + def handler( + incoming: InstallationConfirmationRequest, + _context: InstallationConfirmationContext, + ): + observed.append(incoming.policy_session_id) + return "decline" + + incoming = _request("legacy") + del incoming["policySessionId"] + harness = await _connect(handler) + try: + response = await harness.peer.request(1001, "installations.confirm", incoming) + assert response["decision"] == "decline" + assert observed == [None] + finally: + await harness.close() 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/BUILD.bazel b/rust/BUILD.bazel index 8f349d68e8..71aaa31530 100644 --- a/rust/BUILD.bazel +++ b/rust/BUILD.bazel @@ -1,24 +1,12 @@ -############################################################################### -# @generated -# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To -# regenerate this file, run the following: -# -# bazel run @@//bazel/3rdparty:vendor -############################################################################### - -load("@rules_rust//cargo:defs.bzl", "cargo_toml_env_vars") +# SDK targets are local even when a host dependency pins another SDK source. +# Maintained by bazel/3rdparty/postprocess-local-crates.mjs. load("//src/sdk/rust:sdk_targets.bzl", "sdk_targets") package(default_visibility = ["//visibility:public"]) -cargo_toml_env_vars( - name = "cargo_toml_env_vars", - src = "Cargo.toml", -) - alias( name = "github_copilot_sdk", - actual = "@copilot_agent_runtime//src/sdk/rust:github-copilot-sdk-local-runtime", + actual = ":github-copilot-sdk-local-runtime", tags = ["manual"], ) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 31d534820a..42b0f93e2c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,6 +17,44 @@ dependencies = [ "memchr", ] +[[package]] +name = "ahp" +version = "0.9.0" +source = "git+https://github.com/microsoft/agent-host-protocol?tag=rust%2Fv0.9.0#60706330f2f351b09f150d9a9c3c0eaedfc8e8b9" +dependencies = [ + "ahp-types", + "jiff", + "serde", + "serde_json", + "thiserror 2.0.20", + "tokio", + "tracing", +] + +[[package]] +name = "ahp-types" +version = "0.9.0" +source = "git+https://github.com/microsoft/agent-host-protocol?tag=rust%2Fv0.9.0#60706330f2f351b09f150d9a9c3c0eaedfc8e8b9" +dependencies = [ + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "ahp-ws" +version = "0.9.0" +source = "git+https://github.com/microsoft/agent-host-protocol?tag=rust%2Fv0.9.0#60706330f2f351b09f150d9a9c3c0eaedfc8e8b9" +dependencies = [ + "ahp", + "futures-util", + "serde_json", + "thiserror 2.0.20", + "tokio", + "tokio-tungstenite 0.29.0", + "url", +] + [[package]] name = "async-trait" version = "0.1.92" @@ -52,6 +90,12 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.2" @@ -145,6 +189,37 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "der" version = "0.8.2" @@ -419,6 +494,9 @@ dependencies = [ name = "github-copilot-sdk" version = "0.0.0-dev" dependencies = [ + "ahp", + "ahp-types", + "ahp-ws", "async-trait", "base64 0.23.1", "bytes", @@ -443,7 +521,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tokio-util", "tracing", "ureq", @@ -740,6 +818,43 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "js-sys" version = "0.3.105" @@ -870,7 +985,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cfg-if", "foreign-types", "libc", @@ -963,6 +1078,21 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -1046,7 +1176,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.2", ] [[package]] @@ -1177,7 +1307,7 @@ version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" dependencies = [ - "bitflags", + "bitflags 2.13.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1192,7 +1322,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -1284,7 +1414,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "core-foundation", "core-foundation-sys", "libc", @@ -1355,6 +1485,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "serial_test" version = "4.0.1" @@ -1656,7 +1797,19 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tungstenite", + "tungstenite 0.28.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.29.0", ] [[package]] @@ -1694,7 +1847,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -1773,6 +1926,22 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.20", +] + [[package]] name = "typed-path" version = "0.12.3" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 22cc081fd8..0b5b37304c 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" @@ -29,8 +31,9 @@ name = "github_copilot_sdk" [features] default = ["bundled-cli"] -bundled-cli = ["dep:tar", "dep:flate2", "dep:zip"] -in-process = ["dep:libloading"] +runtime = [] +bundled-cli = ["runtime", "dep:tar", "dep:flate2", "dep:zip"] +in-process = ["runtime", "dep:libloading"] local-runtime = ["in-process"] bundled-in-process = ["bundled-cli", "in-process"] derive = ["dep:schemars"] @@ -43,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"] } @@ -80,12 +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`) @@ -95,6 +110,10 @@ tokio = { version = "1", features = ["rt-multi-thread"] } name = "session_test" required-features = ["test-support"] +[[test]] +name = "runtime_host_e2e" +required-features = ["runtime", "test-support"] + [[test]] name = "protocol_version_test" required-features = ["test-support"] @@ -103,6 +122,10 @@ required-features = ["test-support"] name = "extension_launch_provider_test" required-features = ["test-support"] +[[test]] +name = "installation_confirmation_test" +required-features = ["test-support"] + [[test]] name = "extension_launch_provider_runtime_test" required-features = ["test-support"] @@ -121,9 +144,20 @@ 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"] + +[[test]] +name = "skill_provider_test" +required-features = ["test-support"] [build-dependencies] dirs = "5" flate2 = "1" diff --git a/rust/README.md b/rust/README.md index 5c8f701216..e2dcdb087e 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. @@ -51,6 +53,31 @@ Your Application The SDK manages the CLI process lifecycle: spawning, health-checking, and graceful shutdown. Communication uses [JSON-RPC 2.0](https://www.jsonrpc.org/specification) over stdin/stdout with `Content-Length` framing (the same protocol used by LSP). TCP transport is also supported. +Await `client.stop()` to flush host-owned telemetry: after requesting runtime +shutdown, the SDK closes its owned stdio child's stdin and waits up to 10 seconds +for cleanup and exit before falling back to termination. The shutdown RPC and +final process reap each have a separate 10-second bound. `force_stop()` and +dropping the last client remain immediate termination paths, not telemetry-flush +guarantees. External servers and in-process hosts retain their existing shutdown +behavior. + +### Externally supplied streams + +`default-features = false` builds an external-stream-only client: no runtime +download, binary discovery, launch implementation, or native runtime embedding. +Construct it with `Client::from_streams(reader, writer, cwd)` and explicitly call +`client.verify_protocol_version().await?` to perform the normal SDK handshake. +`Client::start` returns a configuration error in this build; it never falls back +to an installed or cached runtime. + +The default `bundled-cli` feature still enables runtime management. To manage a +runtime without embedding its bundle, use +`default-features = false, features = ["runtime"]`. Existing users of unbundled +`Client::start` should select this feature explicitly. +The `in-process` and `local-runtime` features also enable runtime management; +`local-runtime` continues to use application-supplied artifacts without downloading +them. Enabling `bundled-cli` alongside `local-runtime` retains normal bundling. + ## API Reference ### Client @@ -76,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 @@ -102,9 +136,182 @@ transports. | `extra_args` | `Vec` | Extra CLI flags | | `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` | | `extension_launch_provider` | `Option>` | Connection-global extension launch resolver | +| `installation_confirmation_handler` | `Option>` | Experimental connection-global human installation review | With the default `CliProgram::Resolve`, managed stdio and TCP transports resolve an explicit `CliProgram::Path(path)`, `COPILOT_CLI_PATH`, then the bundled `copilot-runtime` wrapper and adjacent `runtime.node`. In-process transport loads the native runtime library adjacent to that resolved runtime bundle. There is no PATH scanning. +#### AHP listeners (experimental) + +`Client::start_ahp_host` is a thin wrapper over the generated `host.start` +RPC. It is also available with `default-features = false`: the connected +runtime owns and launches the listener, not the SDK. + +```rust,no_run +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_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); +// Supply host.token to AHP clients when present; never log it. +host.dispose().await?; +# Ok(()) +# } +``` + +`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 +process-isolation boundary between the host and runtime. +Every explicit asynchronous `dispose()` call forwards `host.dispose`, +including concurrent or repeated calls, and returns the runtime's result. +There is no cached disposal, automatic retry, synthetic successful disposal, or closed +future. Dropping a handle does not dispose it or spawn cleanup work. +The owning `Client` connection controls runtime host lifetime; the handle +does not keep that client alive. + +`on_exit` receives an `AhpHostExit`, whose +`reason` is `AhpHostExitReason`. Registration precedes the start RPC so an +early exit is observable. Delivery is at most once; callback panics are +caught and logged. Start failure or cancellation releases registration. +If a cancelled start later succeeds, the SDK sends `host.dispose` after receiving +the start response so the listener is not orphaned. +On owner connection loss (including `force_stop`), already-received runtime +exit notifications are drained first. Each remaining callback receives +`OwnerDisconnected`, no exit code, and an explanation that runtime cleanup +cannot be acknowledged over the disconnected transport. This matches Node's +`onExit`: it does not claim listener cleanup completed or send additional disposal RPCs. +`Exited` reports hosting-task failure, not runtime process death; `exit_code` is `None`. +Do not capture the owning `Client` or its sessions in `on_exit`: the stored +closure would keep its own connection alive through a reference cycle. Send the +exit through a channel to application code instead. The same restriction applies +to session factories and release callbacks. + +See [Runtime-host integration tests](scripts/runtime-host-e2e.md) for source +and assembled-candidate validation using the shared replay snapshots. + +##### Application-owned AHP sessions + +Like Node's `createSession` / `onSessionReleased`, Rust's experimental +`with_create_session` / `with_on_session_released` let the application supply +ordinary SDK sessions without replacing their tools, hooks, or event routing: + +```rust,no_run +use std::sync::Arc; +use github_copilot_sdk::{AhpHostOptions, AhpSessionRequest, Client}; +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. + let config = request.config + .with_permission_handler(Arc::new(ApproveAllHandler)); + let session = Arc::new(client.create_session(config).await?); + // Retain an Arc in your application if it should outlive the handoff. + Ok(session) + }) + .with_on_session_released(|original| { + println!("AHP released session {}", original.id()); + // Any explicit disconnect/destroy is the application's decision. + }), +).await?; +host.dispose().await?; +# Ok(()) +# } +``` + +The factory may also implement the async `AhpSessionFactory` trait. It receives +an `AhpSessionRequest` containing a typed `SessionConfig` (no permission handler +installed) and a cooperative `cancellation_token`. Preserve the supplied +session ID, workspace, and host-selected configuration; create a **fresh** +session using the provided client and return `Arc`. The SDK +and runtime reject another client's session, a resumed wrapper, or changed +host-selected settings. Application settings not selected by the host remain +free to customize. The example's approve-all handler is for demonstrations, +not an override for managed approval. + +For durable application-owned sessions, register `with_resume_session` with an +async closure or an `AhpSessionResumeFactory` implementation. The closure receives +an `AhpSessionResumeRequest` and request-scoped `Client`. Preserve its typed +`ResumeSessionConfig`, restore application tools/hooks/handlers, and return +`Arc::new(client.resume_session(config).await?)`. The request also carries a +cooperative `cancellation_token`. +The callback can instead return a retained original `Arc` from the +owning client, subject to the same session identity and workspace checks. + +Only durable catalog entries marked as application-owned use this resume +factory. Restoration fails if the callback is missing; it does not fall back +to host-owned creation. Published resident sessions attach directly without invoking it or +reconfiguring their current registrations. The SDK retains the exact returned +`Arc` and applies the same cancellation, late completion, and release rules as +fresh handoffs; it never automatically disconnects or destroys the result. + +The runtime's handoff deadline is 30 seconds. Cancellation signals the request +token on release, host termination, or owner connection loss. Cancelling the +child token does not stop the host/session. Factories should cooperate with +cancellation, but a late successful return still triggers release with the +**same original `Arc` allocation**, exactly once per handoff. Factory errors +fail the AHP creation; factory and release-callback panics are contained. +Release never automatically invokes `disconnect` or `destroy`. As with any +Rust session, dropping its last `Arc` stops its local event loop, so retain a +clone in application state to continue using it. + +The release closure is **synchronous**, like `on_exit`. The SDK invokes these +closures on blocking workers, independently of its lossless internal host +lifecycle queue, so a slow callback cannot block transport routing or lose +another handoff's release. Applications choosing +asynchronous cleanup can move the original `Arc` into their own task: + +```rust,no_run +# use github_copilot_sdk::AhpHostOptions; +let options = AhpHostOptions::new().with_on_session_released(|original| { + tokio::spawn(async move { + if let Err(error) = original.disconnect().await { + eprintln!("application session cleanup failed: {error}"); + } + }); +}); +``` + +Host disposal does not await application-spawned cleanup; retain/join the task +in application state if shutdown must wait for it. + +Callback registrations are installed before `host.start` and removed on start +failure/cancellation or host/owner termination. Original-session retention is +owned by the application's `Client` handles, not by the shared connection: +session-internal clients cannot form a retention cycle. Dropping the last +owning client handle cancels pending factories and starts bounded host disposal +before releasing their retained originals. Cleanup and release callbacks use the +connection's originating Tokio runtime, even when the owner is dropped from +an ordinary thread. No session `disconnect` or `destroy` +is synthesized. An application-retained session still keeps its ordinary SDK +connection alive and remains usable after the host detaches. Explicitly dispose +hosts before dropping the client when shutdown must await cleanup. The in-process +host remains a distinct participant on the same runtime and session. + #### Extension launch provider Hosts that own legacy extension process assets can supply a typed, asynchronous @@ -227,6 +434,59 @@ session session.disconnect().await?; ``` +#### Skill providers (experimental) + +Hosts can supply a session-scoped skill catalog and lazy markdown reader with +`SkillProvider`. The provider is runtime-only: the SDK sends only +`hasSkillProvider: true` on `session.create` / `session.resume`, and routes +`skillProvider.list` and `skillProvider.read` callbacks back to the trait. + +```rust,no_run +use std::sync::Arc; +use async_trait::async_trait; +use github_copilot_sdk::skill_provider::{SkillProvider, SkillProviderDescriptor}; +use github_copilot_sdk::{Client, ClientOptions, Error, SessionConfig}; + +struct AppSkills; + +#[async_trait] +impl SkillProvider for AppSkills { + async fn list_skills( + &self, + ) -> Result, Error> { + Ok(vec![SkillProviderDescriptor { + name: "review".to_string(), + description: "Review the current change".to_string(), + ..Default::default() + }]) + } + + async fn read_skill(&self, name: &str) -> Result, Error> { + Ok((name == "review").then(|| "# Review\nInspect the diff carefully.".to_string())) + } +} + +# async fn example() -> Result<(), Error> { +let client = Client::start(ClientOptions::default()).await?; +let session = client + .create_session(SessionConfig::default().with_skill_provider(Arc::new(AppSkills))) + .await?; +# session.disconnect().await?; +# client.stop().await.ok(); +# Ok(()) +# } +``` + +In `ClientMode::Empty`, built-in skill loading defaults to disabled; set +`enable_skills` to `Some(true)` when the session should use provider-backed +skills in that mode. Providers are not persisted, so re-supply one with +`ResumeSessionConfig::with_skill_provider` on every resume. Cloud sessions do +not support skill providers. Each callback is dispatched on its own spawned +task, so provider implementations must be safe for concurrent calls. When the +runtime cancels a call, for example after its 30-second limit or when the +session disconnects, the SDK drops the provider future; await cancel-safe work +so that dropping it stops the lookup. + #### Typed RPC namespace High-level helpers are convenience wrappers over a fully-typed @@ -257,6 +517,126 @@ New RPCs land in the namespace immediately as the schema regenerates; helpers are added on top only when an ergonomic story is worth the maintenance. +#### Typed MCP installation and removal payloads (breaking change) + +Three payloads in the experimental MCP installation and removal workflow are now typed +unions instead of `serde_json::Value`, which brings Rust into line with the other SDKs. +This is the only generated-type change of its kind; every other generated type keeps its +released shape. + +| Field | Before | After | +| --- | --- | --- | +| `InstallationReview`, `InstallationConfirmationRequestReview` | struct with `serde_json::Value` payload | `InstallationReview` discriminated union (`Mcp` / `Skill`, by `resource`) whose MCP variant carries `McpInstallationReview` (`Install` / `Uninstall`, by `action`) and whose Skill variant carries `SkillInstallationReview` | +| `McpInstallPlan.transport_choices` | `Vec` | `Vec` (`Package` / `Remote`, by `installMethod`) | +| `McpInstallationManagementOutcomeOperation.operation` | `serde_json::Value` | `McpInstallationOperationStatus` (by `phase`) | + +Required discriminators reject missing or unknown values rather than selecting another +variant. Optional catalogue trust inside a review stays raw JSON so hosts can apply their +own bounds. These types do not imply that installation or activation is available on the +connected runtime. + +#### Installation confirmation (experimental) + +All six SDKs (Node.js, Python, Go, .NET, Java and Rust) provide this receiver with the +same semantics: each review gets one cancellation token, concurrent reviews are +independent, and a decision returned after cancellation is never sent. Without a +configured handler, an `installations.confirm` request is refused, which the +runtime treats as no consent. + +Set `ClientOptions::with_installation_confirmation_handler` to receive the +runtime's `installations.confirm` callback through +`installation_confirmation::InstallationConfirmationHandler`. The handler receives +the generated `InstallationConfirmationRequest` and an +`InstallationConfirmationContext`, and returns only an explicit +`InstallationDecision`. The SDK echoes the original challenge and review +fingerprint; it never infers approval. + +Match `operation_id` and `policy_session_id` against the original action on this +exact connection before presenting the complete review. Missing legacy session +metadata does not select a default session. Refuse unknown operations or +incomplete reviews. Concurrent reviews are independent and do not block the +request router. + +`context.cancellation()` is cancelled when the runtime retires the request, +including runtime-enforced expiry, or when the original connection closes. It +retires the pending handler future, so separately spawned UI work must observe +this signal too. Dropping an outbound installation or OAuth future does not +cancel that operation. + +Call `client.rpc().mcp().prepare_install(...)` before `apply_install(...)`. +Register its inert runtime-issued `operation_id`, original expiry and captured +session on this client before applying. Removal uses `plan_uninstall(...)` then +`apply_uninstall(...)`; its `operation_id` identifies the operation, while +`plan_handle` is the one-use removal input. Never interchange them. The +`installations()` namespace exposes `list`, `recover`, `status` and `cancel`. +Control uncertain work using its original connection and operation ID, without +selecting a replacement session or replaying apply. + +Owned OAuth uses `session.rpc().mcp().oauth().prepare_login(...)` to return +`login_id` before browser, network or cached-reconnect work. Keep that ID with +the original session and `expected_installation_id` for `login(...)` and +`cancel_login(...)`. Preparation freezes reauthentication and display options. +Dropping the login future is not a substitute for `cancel_login(...)`. +Manual MCP OAuth retains its direct `login(...)` path. + +These methods require a matching runtime and available owned-lifecycle support. +Capability negotiation does not promise availability; preserve typed refusals +instead of falling back to raw configuration writes. Generated presence and +transport tests do not establish live OAuth, activation or cross-process recovery. + +Experimental generated DTOs can gain fields and change raw unions to typed +variants. Existing exhaustive struct literals must add the new fields explicitly +(for example, `expected_installation_id: None` for a manual MCP request), or use +`..Default::default()` where that type supports it. This is a source migration, +not full source compatibility. Absent optional fields retain their wire omission +behaviour; existing handwritten builder calls remain compatible. + +#### Generated type-name migration + +Resolving a named object through a schema wrapper now uses the canonical schema +name. Where that resolution directly records the earlier containing-property +name, a generated `pub type` alias retains it. Aliases point directly to an emitted +type; conflicting names or targets fail generation rather than selecting one. +Nested helper names are not reconstructed by comparing old and new type graphs. + +The affected request/result surfaces are experimental. Earlier nested helpers +did not consistently repeat their owning type's experimental annotation. The +complete naming disposition is: + +| Earlier generated name | Canonical name | Disposition | +| --- | --- | --- | +| `InstallationConfirmationRequestReview` | `InstallationReview` | Direct alias; typed review migration below | +| `MetadataContextAttributionResultContextAttribution` | `SessionContextAttribution` | Direct alias | +| `MetadataContextInfoResultContextInfo` | `SessionContextInfo` | Direct alias | +| `SendMessagesRequestResponseFormat` | `ResponseFormat` | Direct alias | +| `SendRequestResponseFormat` | `ResponseFormat` | Direct alias | +| `SessionMetadataSnapshotWorkspace` | `WorkspaceSummary` | Direct alias | +| `UpdateSubagentSettingsRequestSubagents` | `SubagentSettings` | Direct alias | +| `SessionMetadataSnapshotResultWorkspace` | `WorkspaceSummary` | Direct alias | +| `SessionMetadataContextInfoResultContextInfo` | `SessionContextInfo` | Direct alias | +| `SessionMetadataGetContextAttributionResultContextAttribution` | `SessionContextAttribution` | Direct alias | +| `MetadataContextAttributionResultContextAttributionCategories` | `SessionContextAttributionCategories` | Import the canonical nested helper | +| `MetadataContextAttributionResultContextAttributionCompactions` | `SessionContextAttributionCompactions` | Import the canonical nested helper | +| `MetadataContextAttributionResultContextAttributionEntriesItem` | `SessionContextAttributionEntriesItem` | Import the canonical nested helper | +| `SessionMetadataGetContextAttributionResultContextAttributionCategories` | `SessionContextAttributionCategories` | Import the canonical nested helper | +| `SessionMetadataGetContextAttributionResultContextAttributionCompactions` | `SessionContextAttributionCompactions` | Import the canonical nested helper | +| `SessionMetadataGetContextAttributionResultContextAttributionEntriesItem` | `SessionContextAttributionEntriesItem` | Import the canonical nested helper | +| `InstallationConfirmationRequestReviewResource` | `InstallationReviewResource` | Import the canonical nested enum | +| `SendMessagesRequestResponseFormatType` | `ResponseFormatType` | Import the canonical nested enum | +| `SendRequestResponseFormatType` | `ResponseFormatType` | Import the canonical nested enum | + +Retaining a name does not restore an incorrect earlier field representation. +In particular, `InstallationReview` is now the required typed review union, not +arbitrary JSON. Existing MCP constructors should use +`InstallationReview::Mcp(...)` with `McpInstallationReview::Install(...)` or +`McpInstallationReview::Uninstall(...)`; verified Skill confirmations use the +new `InstallationReview::Skill(...)` variant with `SkillInstallationReview`. +Correctly nullable fields require handling `Option` even when the old generated +field incorrectly omitted it. The subagent-settings alias retains the same fields +and existing `Option`/JSON-null behaviour, including clearing an override with +`subagents: None`. These are specific migration rules, not blanket source +compatibility. + ### Handler Traits The SDK exposes five focused handler traits, one per CLI callback type. Implement only the traits you need and install each with the matching `SessionConfig` setter. Each trait has a single `async fn handle(...)` method: @@ -456,12 +836,14 @@ let session = client .await?; ``` -**Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. Return `HookOutput::None` for events you don't handle. +**Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`, `AgentStop`, `SubagentStart`, and `SubagentStop`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. `on_subagent_start` can inject `additional_context` into the child agent's initial prompt; `on_subagent_stop` can return a `decision` of `"block"` with a `reason` to continue the child or a `modified_response` to replace its final answer. Return `HookOutput::None` for events you don't handle. ### System Message Transforms Transforms customize system message sections during session creation. The SDK injects `action: "transform"` entries for each section ID your transform handles. +`last_instructions` includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section in customize mode also removes that guidance; a `SystemMessageTransform` handling this section receives its complete content, including the guidance, and any replacement it returns is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `runtime_instructions` is a separate section: removing it does not remove `last_instructions`. + ```rust,ignore use github_copilot_sdk::transforms::*; use async_trait::async_trait; @@ -568,6 +950,44 @@ The closure receives the full [`ToolInvocation`](crate::types::ToolInvocation) a Reach for the `ToolHandler` trait directly when you need shared state across multiple methods or want a named type that shows up by name in stack traces. +#### Replacing tools during a session + +`Session::set_tools` (experimental) replaces the complete set of tools this client supplies to a live session, together with the handlers that serve them. It takes the same `Tool` values as `with_tools`, and an empty collection removes all of this client's tools. Built-in, MCP, plugin, and extension tools, and tools that other connected clients supply, are unaffected. + +```rust,ignore +session.set_tools(vec![search_tool, filter_tool]).await?; +``` + +The agent sees the new tools from its next model request. The new handlers take effect as soon as the runtime accepts the replacement, and calls already running finish on the handlers that started them. If the runtime rejects the replacement, nothing changes. A model request already in flight was made with the previous tools, so the agent can still call a tool you removed; this session won't answer that call, so replace tools while the session is idle if a running turn might still call a tool you remove. See [Changing tools during a session](../docs/features/changing-tools.md) for the behavior shared by all SDKs. + +#### String-schema `apply_patch` overrides + +With the `derive` feature enabled, an explicit `apply_patch` override can use +`define_tool::` to declare a root string schema. The model sees a +required `input` property, but the runtime restores the scalar patch text before +dispatch. The typed handler receives a `String`, not an `{"input": ...}` object. +This example returns trimmed patch text; replace the handler body with your own +patch implementation: + +```rust,ignore +use github_copilot_sdk::tool::define_tool; +use github_copilot_sdk::ToolResult; + +let apply_patch = define_tool::( + "apply_patch", + "Apply a patch", + |_invocation, patch| async move { + Ok(ToolResult::Text(patch.trim().to_owned())) + }, +) +.with_overrides_built_in_tool(true); + +let config = config.with_tools(vec![apply_patch]); +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + ### Permission Policies Set a permission policy directly on `SessionConfig` with the chainable builders. They install a synthesized `PermissionHandler` so only permission requests are intercepted; every other event flows through unchanged. @@ -1002,6 +1422,16 @@ let session = client See [`examples/session_fs.rs`](examples/session_fs.rs) for a complete in-memory provider implementation. +To let the `view` tool read images stored only in that provider, enable +`SessionFsCapabilities::new().with_binary(true)` with +`SessionFsConfig::with_capabilities`, implement `SessionFsBinaryProvider` +(`read_file_bytes` and `write_file_bytes`), and return it from +`SessionFsProvider::binary`. A registered provider +without binary support does not fall back to a local file with the same +path. + +Binary reads and writes are limited to 50,330,880 raw bytes (approximately 48 MiB); +larger results return a filesystem error before encoding or decoding. - **Canvas action dispatch is a single trait method, not per-action closures.** The Node SDK binds an optional `handler` closure on each entry of a canvas's @@ -1098,14 +1528,15 @@ enabled, preserving bundled behavior for `--all-features` builds. `COPILOT_CLI_PATH` must point to the application's CLI entrypoint, with the compatible native runtime library next to it. -For builds that prefer a smaller artifact, disable the `bundled-cli` feature: +For managed transports without embedded artifacts, disable `bundled-cli` while +enabling `runtime`: ```toml -github-copilot-sdk = { version = "1", default-features = false } +github-copilot-sdk = { version = "1", default-features = false, features = ["runtime"] } ``` > **You become responsible for supplying the runtime at deployment.** With -> `bundled-cli` disabled, the produced binary does not contain these artifacts +> `runtime` enabled and `bundled-cli` disabled, the produced binary does not contain these artifacts > and will not search the system for them. For managed child-process transports, > supply a compatible wrapper pair via an explicit [`CliProgram::Path`]. > `COPILOT_CLI_PATH` remains a direct program override. @@ -1120,6 +1551,11 @@ github-copilot-sdk = { version = "1", default-features = false } > must either keep `bundled-cli` enabled or ship the runtime pair and set > `CliProgram::Path`. +With no features enabled (`default-features = false` alone), the SDK is +external-stream-only: `build.rs` does not acquire runtime artifacts. Use +`Client::from_streams`; `Client::start` returns `InvalidConfig` even if an +explicit program path is supplied. + ### How it works 1. **Version pin.** `build.rs` reads the CLI version from one of two sources: @@ -1127,7 +1563,21 @@ github-copilot-sdk = { version = "1", default-features = false } (present in published crate tarballs and vendored slots). - Otherwise, `../nodejs/package.json` (contributor build inside the github/copilot-sdk repo). - The resolved version is baked into the crate via `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` regardless of mode. 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 @@ -1140,14 +1590,16 @@ github-copilot-sdk = { version = "1", default-features = false } platform-native runtime library (`.dll`, `.so`, or `.dylib`). - **`local-runtime` on and `bundled-cli` off:** skips this acquisition step entirely because the application supplies the runtime package. - - **`bundled-cli` off:** downloads only the runtime package and extracts its + - **`runtime` off:** skips acquisition entirely; only externally supplied + streams are supported. + - **`runtime` on, `bundled-cli` and `local-runtime` off:** downloads only the runtime package and extracts its managed runtime artifacts directly into the platform cache using staging files and atomic renames. 3. **Runtime:** embedded CLI artifacts and build-time-extracted hostless runtime artifacts use separate versioned namespaces: - | OS | `bundled-cli` on | `bundled-cli` off | + | OS | `bundled-cli` on | `runtime` on, `bundled-cli` and `local-runtime` off | |----|------------------|-------------------| | macOS | `~/Library/Caches/github-copilot-sdk/cli//` | `~/Library/Caches/github-copilot-sdk/runtime//` | | Linux | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/cli//` | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/runtime//` | @@ -1158,6 +1610,10 @@ github-copilot-sdk = { version = "1", default-features = false } 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.): @@ -1171,7 +1627,7 @@ let options = ClientOptions::new() let client = Client::start(options).await?; ``` -With `bundled-cli` disabled the equivalent knob is the **`COPILOT_CLI_EXTRACT_DIR`** environment variable, which is honored symmetrically at build time (where `build.rs` writes the binary) and at runtime (where the resolver reads it). When set, the binary lives directly under the named directory (no per-version subdir). The most ergonomic way to pin it from a consumer crate is `.cargo/config.toml`: +With `runtime` enabled and both `bundled-cli` and `local-runtime` disabled, the equivalent knob is the **`COPILOT_CLI_EXTRACT_DIR`** environment variable, which is honored symmetrically at build time (where `build.rs` writes the binary) and at runtime (where the resolver reads it). When set, the binary lives directly under the named directory (no per-version subdir). The most ergonomic way to pin it from a consumer crate is `.cargo/config.toml`: ```toml # .cargo/config.toml at the consumer's repo root @@ -1195,12 +1651,12 @@ override for managed child-process consumers. It works regardless of the ### Resolution priority -For managed child-process transports, `Client::start` resolves the program in this order: +For managed child-process transports (`runtime` enabled), `Client::start` resolves the program in this order: 1. Explicit `CliProgram::Path(path)` on `ClientOptions::program`. 2. `COPILOT_CLI_PATH` environment variable, if it points at a real file. 3. **`bundled-cli` on:** the embedded wrapper pair, lazily extracted on first call. -4. **`bundled-cli` off:** the build-time-extracted wrapper pair in the per-user cache. +4. **`bundled-cli` and `local-runtime` off:** the build-time-extracted wrapper pair in the per-user cache. In-process transport loads the native runtime library adjacent to the runtime wrapper selected from `COPILOT_CLI_PATH`, the embedded runtime archive, or the @@ -1249,8 +1705,61 @@ In embed mode `build.rs` downloads both verified archives on every clean build by default. Set `BUNDLED_CLI_CACHE_DIR=` to cache them between builds (CI keys this on `-` for near-zero-cost rebuilds on cache hits). For Copilot CLI 1.0.83-5, the two upstream archives total roughly 132-157 MB per -platform before the runtime package is filtered. With `bundled-cli` 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 @@ -1263,7 +1772,8 @@ and `CARGO_CFG_TARGET_ENV` (cross-compilation works). | Feature | Default | Description | | ------- | ------- | ----------- | -| `bundled-cli` | ✓ | Embeds the managed wrapper pair and compatible CLI artifact. Disable via `default-features = false` when supplying the runtime explicitly. | +| `runtime` | ✓ (via `bundled-cli`) | Enables managed runtime startup and discovery. With no runtime feature, use `Client::from_streams`; no runtime artifacts are acquired. | +| `bundled-cli` | ✓ | Enables `runtime` and embeds the managed wrapper pair and compatible CLI artifact. | | `in-process` | — | Enables `Transport::InProcess` while preserving the selected runtime acquisition policy. | | `local-runtime` | — | Enables `in-process` and, when `bundled-cli` is disabled, disables SDK-managed runtime download, extraction, and embedding. The application must supply a compatible runtime package through `COPILOT_CLI_PATH`. | | `bundled-in-process` | — | Enables `in-process`, implies `bundled-cli`, and additionally embeds the platform-native runtime library. | @@ -1279,7 +1789,10 @@ github-copilot-sdk = { version = "1", features = ["bundled-in-process"] } # Enable the in-process transport with an application-supplied runtime. github-copilot-sdk = { version = "1", default-features = false, features = ["local-runtime"] } -# Opt out of bundling — supply the CLI explicitly at runtime. +# Opt out of bundling, but retain managed startup and build-machine runtime caching. +github-copilot-sdk = { version = "1", default-features = false, features = ["runtime"] } + +# External streams only — no managed startup or runtime acquisition. github-copilot-sdk = { version = "1", default-features = false } # Derive JSON Schema for tool parameters (adds to default bundled-cli). 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 8037daa22c..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; @@ -17,8 +19,23 @@ pub(crate) fn main() { println!("cargo:rerun-if-env-changed=BUNDLED_CLI_CACHE_DIR"); println!("cargo::rustc-check-cfg=cfg(has_bundled_cli)"); println!("cargo::rustc-check-cfg=cfg(has_extracted_cli)"); - println!("cargo:rerun-if-changed=cli-version.txt"); - println!("cargo:rerun-if-changed=cli-version-in-process.txt"); + + // Declare only the version sources that exist. Cargo treats a missing + // `rerun-if-changed` path as always stale, so for a local package (a path + // dependency, vendored copy, or source checkout) an absent snapshot or + // `../nodejs/package.json` would rerun this script, and recompile the + // crate, on every build. + let manifest_dir = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + for snapshot in ["cli-version.txt", "cli-version-in-process.txt"] { + if manifest_dir.join(snapshot).is_file() { + println!("cargo:rerun-if-changed={snapshot}"); + } + } + + if std::env::var_os("CARGO_FEATURE_RUNTIME").is_none() { + return; + } if std::env::var_os("CARGO_FEATURE_LOCAL_RUNTIME").is_some() && std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_none() @@ -29,18 +46,9 @@ pub(crate) fn main() { return; } - // Only declare the package metadata rerun when it actually exists. - // Cargo treats `rerun-if-changed` for a missing path as "always rerun" - // — so unconditionally declaring this on consumers without a sibling - // `nodejs/` (vendored slots, published crates) would force build.rs - // to re-run on every `cargo build` even when nothing has changed. // The package file is only the source-of-truth in this repo's // contributor builds; everywhere else the snapshot files are canonical. - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); - 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() { println!("cargo:rerun-if-changed={}", package_json.display()); } @@ -81,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, @@ -119,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, ); @@ -145,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"), @@ -171,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| { @@ -317,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", @@ -372,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!( @@ -408,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 { @@ -444,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 { @@ -484,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)] @@ -788,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}"); @@ -840,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; @@ -975,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/check-fresh-rebuild.sh b/rust/scripts/check-fresh-rebuild.sh new file mode 100755 index 0000000000..387c809e6f --- /dev/null +++ b/rust/scripts/check-fresh-rebuild.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# Fail when an unchanged rebuild reruns build.rs or recompiles the crate. +# +# Cargo checks `rerun-if-changed` paths only for local packages, such as a +# source checkout, a path dependency, or a vendored copy, and treats a missing +# path as always stale. Declaring an absent file therefore reruns build.rs and +# recompiles the crate and its dependents on every build, while registry and +# git consumers are unaffected. Checking this crate from its own directory +# exercises the same local-package behavior as a path dependency. +# +# Usage: scripts/check-fresh-rebuild.sh [cargo check options] +# Example: scripts/check-fresh-rebuild.sh --no-default-features --features test-support,derive + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}/.." + +cargo check "$@" +if ! output="$(cargo check --verbose --color never "$@" 2>&1)"; then + printf '%s\n' "${output}" >&2 + exit 1 +fi +if ! grep -Eq '^ *Fresh github-copilot-sdk v' <<<"${output}"; then + grep -E '^ *[A-Z][a-z]+ github-copilot-sdk v' <<<"${output}" >&2 || printf '%s\n' "${output}" >&2 + echo "error: an unchanged rebuild did not keep github-copilot-sdk fresh" >&2 + exit 1 +fi +echo "An unchanged rebuild kept github-copilot-sdk fresh." diff --git a/rust/scripts/runtime-host-e2e.md b/rust/scripts/runtime-host-e2e.md new file mode 100644 index 0000000000..8780b4dfc3 --- /dev/null +++ b/rust/scripts/runtime-host-e2e.md @@ -0,0 +1,124 @@ +# Runtime-supervised AHP integration tests + +`tests/runtime_host_e2e.rs` exercises the real Rust SDK, a separate +`copilot-runtime` process, its provider library, and its in-process AHP +WebSocket listener. The AHP client is the standard upstream Rust +`ahp`/`ahp-ws` client, pinned to `rust/v0.9.0`. + +These tests reuse `tests/e2e/support.rs` and the existing Node CapiProxy, including +its normal inference matcher. Every inference uses model `claude-sonnet-5`, +prompt `What is 2+2?`, and the unchanged shared recording: + +``` +test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml +``` + +`GITHUB_ACTIONS=true` enforces replay-only operation. There is no AHP mock, +alternative inference server, generated recording, or real GitHub credential. +The suite does not build native artifacts or download a replacement runtime. + +## Prerequisites + +- Linux with `/proc` available, Rust's pinned toolchain, Node, and the SDK's + existing installed Node test dependencies (`nodejs/node_modules/.bin/tsx`). +- Final local builds of `copilot-runtime` and `runtime.node` with the host library linked in. + Build/freeze those separately before running the tests. +- A workspace-local scratch directory selected by `TMPDIR`. The existing + integration harness puts its isolated homes and working directories there. + +## Source artifacts + +Use the `copilot-agent-runtime` checkout with its locked `copilotd-hosting` +library. The SDK is the runtime repository's `src/sdk` subtree. From +`copilot-agent-runtime/src/sdk/rust/`, substitute absolute local-build paths: + +```sh +mkdir -p ../.runtime-host-test-work/rust +TMPDIR="$(cd ../.runtime-host-test-work/rust && pwd)" \ +GITHUB_ACTIONS=true COPILOT_RUNTIME_HOST_E2E=1 \ +COPILOT_CLI_PATH=/absolute/local/runtime/copilot-runtime \ +COPILOT_RUNTIME_PROVIDER_LIB=/absolute/local/runtime/runtime.node \ +cargo test --locked --no-default-features --features local-runtime,test-support \ + --test runtime_host_e2e -- --ignored --test-threads=1 +``` + +## Assembled unpublished candidate + +Use the **same candidate manifest already produced for the Node runtime-host +E2Es**, with source commits matching the runtime checkout and its locked hosting library. Its version 3 source +identity records the SDK as the runtime repository's `src/sdk` subtree at the +same commit. There is no separate Rust package staging or attestation format. +Use `samples/runtime-host/stage-candidate.ts RUNTIME_CHECKOUT OUTPUT_DIRECTORY` +from the SDK root after building the runtime launcher, provider, and `dist-cli` +assets. Staging needs only runtime artifacts: `copilotd-hosting` is linked into +the provider. See the sample README for source attestation and packaging details. + +```sh +mkdir -p ../.runtime-host-test-work/rust +TMPDIR="$(cd ../.runtime-host-test-work/rust && pwd)" \ +GITHUB_ACTIONS=true COPILOT_RUNTIME_HOST_E2E=1 \ +COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST=/absolute/existing/candidate/candidate.json \ +cargo test --locked --no-default-features --features local-runtime,test-support \ + --test runtime_host_e2e -- --ignored --test-threads=1 +``` + +The small `tests/e2e/runtime_host_candidate.mts` bridge invokes the existing +Node `candidateHostArtifacts` helper. That helper verifies source/checksum +metadata and invokes the actual SDK materializer. Rust launches the returned +materialized runtime and removes development overrides; +Linux process checks prove it loaded the materialized adjacent assets. + +## Coverage and focused checks + +Focused tests cover streamed AHP inference beside an ordinary SDK session, +shared runtime session visibility, durable list/resume after disposal and +repeated listener disposal, explicit base-directory persistence across runtime restarts, catalog +writer exclusion, owner disconnect cleanup, cross-owner disposal rejection, +occupied-port startup failure and recovery, concurrent/repeated direct disposal, listener +hostname/ports/tokens (including IPv6, DNS names, missing/wrong tokens, and +fractional ports through raw RPC) and invalid combinations, protected-resource +authentication when connection-token checks are disabled, and graceful runtime +shutdown. Checks assert that the host PID is absent, the expected +provider is loaded, no host child or second runtime is spawned, the runtime owns +the listener's socket inode, existing AHP clients disconnect, the TCP listener +closes, and tokens do not leak in runtime +arguments. +The runtime must survive listener disposal and external owner disconnect. +Only runtime shutdown or dropping its owning client requires process reaping. +There is no host process-isolation boundary: killing the runtime also ends its +listeners. No task-specific crash injection is currently available, so these tests +do not claim unexpected-task-failure or process-crash isolation coverage. + +The public callback cases in `tests/e2e/runtime_host_callbacks.rs` additionally +exercise application-owned sessions, exact original `Arc` release, application +tool execution and hooks, preserved prompt content, genuine host-attachment +failure (a conflicting tool), late factory completion after disposal, +listener disposal (including a pending factory), external owner connection +termination, and reclamation/reaping after dropping all owning client handles. Tool turns +reuse the canonical `multi_client/both_clients_see_tool_request_and_completion_events` +snapshot. Unit/wire tests also cover factory failure/panic, cancellation, +configuration changes, wrong-owner sessions, release races, and duplicate release. +The focused `runtime_host/app_resume_callback_composes_tools_after_history` replay +also verifies application-owned creation, persistence, complete runtime shutdown +and reaping, and restoration through the resume callback in a new runtime. +Configuration and history survive. The reconnected AHP client uses the same client +ID and advertises its persisted `client_echo` tool; that tool and the restored +application `magic_number` tool each execute once, with both actual results +asserted in the next inference request. This complements the existing +listener-resume, retained-object, and resident-publication coverage rather than +replacing it. +Ownership regressions also prove that application-retained sessions remain usable +after owner drop, and a blocked release callback plus 2,048 unrelated notifications +cannot lose or delay another handoff's cancellation/release. + +The tests are ignored by ordinary Cargo runs. To compile without running native +artifacts: + +```sh +cargo test --locked --no-default-features --features local-runtime,test-support \ + --test runtime_host_e2e --no-run +``` + +To run one case, add its name before `--`; retain `--ignored`. Run the complete +suite against **both final source and final candidate artifacts** before +claiming runtime-host parity. 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/sdk_targets.bzl b/rust/sdk_targets.bzl index eab7b8d5c1..4d209c9cb7 100644 --- a/rust/sdk_targets.bzl +++ b/rust/sdk_targets.bzl @@ -50,6 +50,7 @@ def sdk_targets(): "derive", "in-process", "local-runtime", + "runtime", "test-support", ] @@ -95,6 +96,7 @@ def sdk_targets(): crate_features = [ "in-process", "local-runtime", + "runtime", ], crate_name = "github_copilot_sdk", crate_root = "src/lib.rs", @@ -104,7 +106,7 @@ def sdk_targets(): "--check-cfg=cfg(has_bundled_cli)", "--check-cfg=cfg(has_extracted_cli)", "--check-cfg=cfg(test)", - '--check-cfg=cfg(feature,values("bundled-cli","bundled-in-process","derive","in-process","local-runtime","test-support"))', + '--check-cfg=cfg(feature,values("bundled-cli","bundled-in-process","derive","in-process","local-runtime","runtime","test-support"))', ], visibility = ["//visibility:public"], deps = _LOCAL_RUNTIME_DEPS, diff --git a/rust/src/ahp_host.rs b/rust/src/ahp_host.rs new file mode 100644 index 0000000000..2b42b86ff0 --- /dev/null +++ b/rust/src/ahp_host.rs @@ -0,0 +1,1010 @@ +//! Experimental runtime-owned Agent Host Protocol (AHP) listeners. + +use std::collections::HashMap; +use std::future::Future; +use std::sync::{Arc, Weak}; + +use async_trait::async_trait; +use futures_util::FutureExt; +use parking_lot::Mutex; +use serde_json::Value; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::warn; + +/// Experimental AHP host exit reason, including local owner disconnection. +pub use crate::generated::api_types::HostExitReason as AhpHostExitReason; +/// Experimental listener-task exit payload or local owner-disconnection report. +/// `Exited` means hosting-task failure, not runtime process death; `exit_code` is `None`. +pub use crate::generated::api_types::HostExitedNotification as AhpHostExit; +use crate::generated::api_types::{ + HostDisposeRequest, HostPublishSessionRequest, HostPublishSessionResult, + HostSessionCreateCallback, HostSessionReleasedNotification, HostStartRequest, HostStartResult, +}; +use crate::session::Session; +use crate::{ + Client, ClientInner, Error, ErrorKind, ProtocolErrorKind, ResumeSessionConfig, SessionConfig, +}; + +/// Experimental local callback for a host's exit or owner disconnection. +pub type AhpHostExitCallback = Arc; + +/// One request to create an application-owned session for an AHP participant. +/// +/// **Experimental.** Preserve the supplied configuration, especially the session +/// ID and working directory. Add application tools, hooks and handlers before +/// calling [`Client::create_session`] on the supplied request-scoped client. +#[non_exhaustive] +pub struct AhpSessionRequest { + /// Host-selected settings, with no permission handler installed. + pub config: SessionConfig, + /// Cancelled when the handoff ends, including timeout or owner loss. + /// + /// This is a child token: cancelling it does not cancel the host or session. + /// Cancellation is cooperative. A session returned after cancellation is + /// still passed to the release callback exactly once. + pub cancellation_token: CancellationToken, +} + +/// Host-selected settings for resuming a durable application-owned AHP session. +#[non_exhaustive] +pub struct AhpSessionResumeRequest { + /// Preserve these settings and add application tools, hooks and handlers. + pub config: ResumeSessionConfig, + /// Cooperative cancellation; late returned sessions are still released once. + pub cancellation_token: CancellationToken, +} + +/// Resumes application-owned AHP sessions. Published resident sessions bypass this callback. +#[async_trait] +pub trait AhpSessionResumeFactory: Send + Sync + 'static { + /// Return the original resumed allocation, or a retained attached original. + async fn resume_session( + &self, + request: AhpSessionResumeRequest, + client: Client, + ) -> Result, Error>; +} + +#[async_trait] +impl AhpSessionResumeFactory for F +where + F: Fn(AhpSessionResumeRequest, Client) -> Fut + Send + Sync + 'static, + Fut: Future, Error>> + Send, +{ + async fn resume_session( + &self, + request: AhpSessionResumeRequest, + client: Client, + ) -> Result, Error> { + self(request, client).await + } +} + +/// Creates ordinary SDK sessions for a runtime-supervised AHP host. +/// +/// **Experimental.** Return the original session in an [`Arc`], not a resumed +/// wrapper. The SDK retains that exact allocation until the participation ends. +/// The runtime checks that it is a fresh resident session on this connection. +/// Implementations must not capture the owning client; use the request-scoped +/// client argument to avoid a reference cycle. Async closures are supported. +#[async_trait] +pub trait AhpSessionFactory: Send + Sync + 'static { + /// Materialize the host-selected session, preserving its configuration. + async fn create_session( + &self, + request: AhpSessionRequest, + client: Client, + ) -> Result, Error>; +} + +#[async_trait] +impl AhpSessionFactory for F +where + F: Fn(AhpSessionRequest, Client) -> Fut + Send + Sync + 'static, + Fut: Future, Error>> + Send, +{ + async fn create_session( + &self, + request: AhpSessionRequest, + client: Client, + ) -> Result, Error> { + self(request, client).await + } +} + +/// Receives the exact original session once per ended handoff. +/// +/// Runs on a blocking worker, or synchronously if that worker is cancelled during +/// runtime shutdown, outside SDK locks; panics are caught and logged. The SDK never +/// calls `destroy` or `disconnect` on release. Retain an application [`Arc`] if +/// the session should outlive the handoff, or perform cleanup here explicitly. +/// To choose asynchronous cleanup, spawn a task that owns the released `Arc`: +/// +/// ```no_run +/// use github_copilot_sdk::AhpHostOptions; +/// +/// let options = AhpHostOptions::new().with_on_session_released(|original| { +/// tokio::spawn(async move { +/// if let Err(error) = original.disconnect().await { +/// eprintln!("application session cleanup failed: {error}"); +/// } +/// }); +/// }); +/// ``` +/// +/// The closure is synchronous. Spawned cleanup is application-owned; host +/// disposal does not await it. Cleanup that needs a runtime must account for +/// the originating runtime already being shut down. +pub type AhpSessionReleasedCallback = Arc) + Send + Sync>; + +/// Options for [`Client::start_ahp_host`]. +/// +/// **Experimental.** May change or be removed in future releases. +/// 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 { + /// 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 + /// cleaned up. Already-received runtime exit notifications take precedence. + /// `Exited` reports hosting-task failure, not process death; `exit_code` is `None`. + /// Panics are caught and logged, as for other SDK notification callbacks. + /// Do not capture the owning `Client` (or a session holding its connection): + /// that creates a reference cycle. Forward the exit to application code + /// through a channel instead. + pub on_exit: Option, + /// Optional application session factory, never serialized. + pub create_session: Option>, + /// Optional application resume callback, never serialized. + pub resume_session: Option>, + /// Optional release notification for the original application session. + pub on_session_released: Option, +} + +impl std::fmt::Debug for AhpHostOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AhpHostOptions") + .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()) + .field("on_session_released", &self.on_session_released.is_some()) + .finish() + } +} + +impl AhpHostOptions { + /// Create options with no transports enabled. Select at least one before starting. + pub fn new() -> Self { + Self::default() + } + + /// 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 + } + + /// 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 + } + + /// Set the local exit callback. + /// + /// Do not capture the owning client or its sessions; use a channel to notify + /// application code without keeping the connection alive. + pub fn with_on_exit(mut self, callback: impl Fn(AhpHostExit) + Send + Sync + 'static) -> Self { + self.on_exit = Some(Arc::new(callback)); + self + } + + /// Create application-owned sessions using a factory or async closure. + /// + /// Use the supplied client rather than capturing this host's owner. + /// + /// ```no_run + /// use std::sync::Arc; + /// use github_copilot_sdk::{AhpHostOptions, AhpSessionRequest, Client}; + /// + /// # 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?)) + /// }) + /// .with_on_session_released(|session| println!("released {}", session.id())), + /// ).await?; + /// host.dispose().await?; + /// # Ok(()) + /// # } + /// ``` + pub fn with_create_session(mut self, factory: impl AhpSessionFactory) -> Self { + self.create_session = Some(Arc::new(factory)); + self + } + + /// Resume durable app-owned AHP sessions with a factory or async closure. + /// + /// Use the request-scoped client and return the original `Arc`, + /// or return an existing attached original without reconfiguring it. + /// Published resident sessions attach directly, without calling this factory. + pub fn with_resume_session(mut self, factory: impl AhpSessionResumeFactory) -> Self { + self.resume_session = Some(Arc::new(factory)); + self + } + + /// Receive the original session when its AHP participation ends. + /// + /// This does not end the application's own participation. + pub fn with_on_session_released( + mut self, + callback: impl Fn(Arc) + Send + Sync + 'static, + ) -> Self { + self.on_session_released = Some(Arc::new(callback)); + self + } +} + +/// A small handle to a runtime-owned AHP listener. +/// +/// **Experimental.** May change or be removed in future releases. +/// The owning [`Client`] connection controls the listener's lifetime. This +/// handle does not keep the client alive. Dropping it performs no RPC or +/// background cleanup; call [`dispose`](Self::dispose) explicitly instead. +/// Dropping the last owning client handle disposes callback-backed hosts; +/// application-retained sessions still own their ordinary SDK connection. +#[derive(Clone)] +#[non_exhaustive] +pub struct AhpHost { + /// Runtime host identifier. + pub host_id: 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. + pub token: Option, + client: Weak, +} + +impl std::fmt::Debug for AhpHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + 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() + } +} + +impl AhpHost { + /// Publish a resident session already attached to this client until this listener stops. + pub async fn publish_session( + &self, + session_id: impl Into, + ) -> Result { + let client = Client::from_inner(self.client.upgrade().ok_or_else(|| { + Error::from(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)) + })?); + client + .rpc() + .host() + .publish_session(HostPublishSessionRequest { + host_id: self.host_id.clone(), + session_id: session_id.into(), + }) + .await + } + + /// Forward `host.dispose` to the runtime. + /// + /// Every call makes its own RPC, including repeated and concurrent calls. + /// Results and errors come from the runtime; no result is cached and no + /// exit notification is synthesized. + pub async fn dispose(&self) -> Result<(), Error> { + let client = Client::from_inner(self.client.upgrade().ok_or_else(|| { + Error::from(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)) + })?); + client + .rpc() + .host() + .dispose(HostDisposeRequest { + host_id: self.host_id.clone(), + }) + .await?; + Ok(()) + } +} + +pub(crate) type ExitCallbacks = Mutex>; + +fn invoke_exit_callback(callback: AhpHostExitCallback, exit: AhpHostExit) { + tokio::task::spawn_blocking(move || { + if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(exit))).is_err() { + warn!("host.exited callback panicked; continuing notification routing"); + } + }); +} + +// Keep the committed start response observable after caller cancellation, so a +// successful late start can be disposed rather than becoming an orphan. +struct PendingStart { + callbacks: Arc, + sessions: Arc, + host_id: Option, + start: Option>>, + client: Weak, +} + +impl Drop for PendingStart { + fn drop(&mut self) { + if let Some(host_id) = &self.host_id { + let callback = self.callbacks.lock().remove(host_id); + drop(callback); + self.sessions.release_host(host_id); + } + if let Some(start) = self.start.take() { + let client = self.client.clone(); + self.sessions.2.spawn(async move { + if let Ok(Ok(result)) = start.await + && let Some(inner) = client.upgrade() + { + let client = Client::from_inner(inner); + if let Err(error) = client + .rpc() + .host() + .dispose(HostDisposeRequest { + host_id: result.host_id, + }) + .await + { + warn!(%error, "cancelled AHP start cleanup failed"); + } + } + }); + } + } +} + +impl Client { + pub(crate) fn host_sessions(&self) -> Result, Error> { + self.ahp_host_sessions + .clone() + .or_else(|| self.inner.ahp_host_sessions.upgrade()) + .ok_or_else(|| handoff_error("AHP owning client has been dropped")) + } + + /// Start a runtime-owned AHP listener through the generated `host.start` RPC. + /// + /// **Experimental.** May change or be removed in future releases. + /// The runtime supplies listener defaults and validates all listener options. + /// A local exit callback is registered before sending the request, so it can + /// receive an exit that arrives before the start response. + /// 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(), + sessions: self.host_sessions()?, + host_id: Some(host_id.clone()), + start: None, + client: Arc::downgrade(&self.inner), + }; + if let Some(callback) = options.on_exit { + pending.callbacks.lock().insert(host_id.clone(), callback); + } + let session_factory = options.create_session.as_ref().map(|_| true); + let resume_factory = options.resume_session.as_ref().map(|_| true); + if session_factory.is_some() || resume_factory.is_some() { + pending.sessions.0.lock().factories.insert( + host_id.clone(), + FactoryRegistration { + create: options.create_session, + resume: options.resume_session, + on_released: options.on_session_released, + }, + ); + } + let request = self.inner.rpc.send_request_with_inline_callback( + "host.start", + Some(serde_json::to_value(HostStartRequest { + host_id, + local_server: options.local_server, + github_environment: options.github_environment, + session_factory, + resume_factory, + })?), + None, + ); + pending.start = Some(tokio::spawn(async move { + let response = request.await?; + if let Some(error) = response.error { + return Err(Error::from_rpc(error.code, error.message, error.data)); + } + Ok(serde_json::from_value( + response.result.unwrap_or(Value::Null), + )?) + })); + let result = pending + .start + .as_mut() + .expect("start task was just registered") + .await; + pending.start = None; + let result = result.map_err(|_| handoff_error("AHP host start task failed"))??; + pending.host_id = None; + 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), + }) + } + + pub(crate) fn spawn_ahp_host_dispatcher( + &self, + mut notifications: mpsc::UnboundedReceiver, + ) { + let callbacks = self.inner.ahp_host_callbacks.clone(); + let sessions = self.inner.ahp_host_sessions.clone(); + let closed = self.inner.rpc.connection_closed_token(); + tokio::spawn(async move { + loop { + let notification = tokio::select! { + // Drain received exits before reporting transport loss. + biased; + notification = notifications.recv() => notification, + _ = closed.cancelled() => break, + }; + match notification { + Some(notification) if notification.method == "host.exited" => { + let Some(params) = notification.params else { + continue; + }; + let exit = match serde_json::from_value::(params) { + Ok(exit) => exit, + Err(error) => { + warn!(%error, "failed to deserialize host.exited notification"); + continue; + } + }; + if let Some(sessions) = sessions.upgrade() { + sessions.release_host(&exit.host_id); + } + let callback = callbacks.lock().remove(&exit.host_id); + if let Some(callback) = callback { + invoke_exit_callback(callback, exit); + } + } + Some(notification) if notification.method == "host.sessionReleased" => { + if let Some(params) = notification.params + && let Ok(released) = + serde_json::from_value::(params) + && let Some(sessions) = sessions.upgrade() + { + sessions.release(&released.host_id, &released.handoff_id); + } + } + Some(_) => {} + None => break, + } + } + if let Some(sessions) = sessions.upgrade() { + sessions.release_all(); + } + let callbacks = std::mem::take(&mut *callbacks.lock()); + for (host_id, callback) in callbacks { + invoke_exit_callback( + callback, + AhpHostExit { + host_id, + reason: AhpHostExitReason::OwnerDisconnected, + exit_code: None, + error: Some( + "Owner connection closed; runtime cleanup cannot be acknowledged on this connection." + .to_owned(), + ), + }, + ); + } + }); + } +} + +#[derive(Clone)] +struct FactoryRegistration { + create: Option>, + resume: Option>, + on_released: Option, +} + +#[derive(Default)] +struct HostSessionState { + factories: HashMap, + handoffs: HashMap>, +} + +impl Drop for HostSessionState { + fn drop(&mut self) { + // Also runs when owner-drop cleanup is cancelled before its first poll. + for handoff in self.handoffs.values() { + handoff.release(); + } + } +} + +pub(crate) struct HostSessions( + Mutex, + Mutex>, + tokio::runtime::Handle, +); + +impl Drop for HostSessions { + fn drop(&mut self) { + let state = std::mem::take(self.0.get_mut()); + for handoff in state.handoffs.values() { + handoff.cancellation.cancel(); + } + // Only the public Client handles own this retention root. Session clients + // own the connection without pointing back here. A finite teardown task + // keeps originals alive until their hosts detach, without a reference cycle. + if !state.factories.is_empty() + && let Some(inner) = self.1.get_mut().upgrade() + { + self.2.spawn(async move { + let client = Client::from_inner(inner); + for host_id in state.factories.keys() { + let disposal = async { + client + .rpc() + .host() + .dispose(HostDisposeRequest { + host_id: host_id.clone(), + }) + .await + }; + if !matches!( + tokio::time::timeout(std::time::Duration::from_secs(10), disposal).await, + Ok(Ok(_)) + ) { + warn!("AHP owner-drop host cleanup failed"); + } + } + drop(state); + }); + } + } +} + +struct Handoff { + host_id: String, + requested_session_id: String, + cancellation: CancellationToken, + on_released: Option, + session: Mutex>>, + runtime: tokio::runtime::Handle, +} + +// Dropping a cancelled blocking task must still deliver its release notification. +struct ReleaseNotification { + callback: AhpSessionReleasedCallback, + session: Option>, +} + +impl Drop for ReleaseNotification { + fn drop(&mut self) { + if let Some(session) = self.session.take() + && std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.callback)(session))) + .is_err() + { + warn!("AHP session release callback panicked"); + } + } +} + +impl Handoff { + fn notify_released(&self, session: Arc) { + if let Some(callback) = self.on_released.clone() { + let notification = ReleaseNotification { + callback, + session: Some(session), + }; + self.runtime.spawn_blocking(move || drop(notification)); + } + } + + fn release(&self) { + let session = { + let mut session = self.session.lock(); + self.cancellation.cancel(); + session.take() + }; + if let Some(session) = session { + self.notify_released(session); + } + } + + fn retain(&self, session: Arc) -> Result<(), Error> { + { + let mut retained = self.session.lock(); + if !self.cancellation.is_cancelled() { + *retained = Some(session); + return Ok(()); + } + } + self.notify_released(session); + Err(handoff_error("AHP session handoff ended")) + } +} + +impl HostSessions { + pub(crate) fn new() -> Self { + Self( + Mutex::default(), + Mutex::default(), + tokio::runtime::Handle::current(), + ) + } + + pub(crate) fn set_owner(&self, owner: Weak) { + *self.1.lock() = owner; + } + + fn release(&self, host_id: &str, handoff_id: &str) { + let handoff = { + let mut state = self.0.lock(); + if !state.factories.contains_key(host_id) + || state + .handoffs + .get(handoff_id) + .is_some_and(|h| h.host_id != host_id) + { + return; + } + state.handoffs.remove(handoff_id) + }; + if let Some(handoff) = handoff { + handoff.release(); + } + } + + fn release_host(&self, host_id: &str) { + let (factory, handoffs) = { + let mut state = self.0.lock(); + let factory = state.factories.remove(host_id); + let ids: Vec<_> = state + .handoffs + .iter() + .filter(|(_, h)| h.host_id == host_id) + .map(|(id, _)| id.clone()) + .collect(); + let handoffs: Vec<_> = ids + .iter() + .filter_map(|id| state.handoffs.remove(id)) + .collect(); + (factory, handoffs) + }; + drop(factory); + for handoff in handoffs { + handoff.release(); + } + } + + fn release_all(&self) { + let state = std::mem::take(&mut *self.0.lock()); + drop(state); + } + + pub(crate) fn expects_session(&self, id: Option<&crate::SessionId>) -> bool { + id.is_some_and(|id| { + self.0 + .lock() + .handoffs + .values() + .any(|h| h.requested_session_id == id.as_str()) + }) + } +} + +fn handoff_error(message: &'static str) -> Error { + Error::with_message(ErrorKind::InvalidConfig, message) +} + +impl Client { + pub(crate) fn register_ahp_session_factory(&self) -> Result<(), Error> { + let client = Arc::downgrade(&self.inner); + self.inner.rpc.register_request_handler( + "host.materializeSession", + Arc::new(move |params| { + // Register before the reader advances to a release notification, + // not when the spawned application task gets its first poll. + let prepared = (|| { + let inner = client + .upgrade() + .ok_or_else(|| handoff_error("AHP owner connection is closed"))?; + let client = Client::from_inner(inner); + let params = serde_json::from_value(params)?; + let (factory, handoff) = client.prepare_ahp_session(¶ms)?; + Ok::<_, Error>((client, params, factory, handoff)) + })(); + Box::pin(async move { + let (client, params, factory, handoff) = prepared?; + client + .materialize_ahp_session(params, factory, handoff) + .await + }) + }), + ) + } + + fn prepare_ahp_session( + &self, + params: &HostSessionCreateCallback, + ) -> Result<(FactoryRegistration, Arc), Error> { + let resume = params.resume.unwrap_or(false); + let requested_session_id = params + .config + .get("sessionId") + .and_then(Value::as_str) + .ok_or_else(|| handoff_error("AHP session configuration requires sessionId"))? + .to_owned(); + let (factory, handoff) = { + let sessions = self.host_sessions()?; + let mut state = sessions.0.lock(); + let factory = state + .factories + .get(¶ms.host_id) + .cloned() + .ok_or_else(|| handoff_error("AHP session factory is unavailable"))?; + if (resume && factory.resume.is_none()) || (!resume && factory.create.is_none()) { + return Err(handoff_error("AHP session factory is unavailable")); + } + if state.handoffs.contains_key(¶ms.handoff_id) { + return Err(handoff_error("AHP session handoff already exists")); + } + let handoff = Arc::new(Handoff { + host_id: params.host_id.clone(), + requested_session_id, + cancellation: CancellationToken::new(), + on_released: factory.on_released.clone(), + session: Mutex::new(None), + runtime: sessions.2.clone(), + }); + state + .handoffs + .insert(params.handoff_id.clone(), handoff.clone()); + (factory, handoff) + }; + Ok((factory, handoff)) + } + + async fn materialize_ahp_session( + &self, + params: HostSessionCreateCallback, + factory: FactoryRegistration, + handoff: Arc, + ) -> Result { + if handoff.cancellation.is_cancelled() { + return Err(handoff_error("AHP session handoff ended")); + } + let resume = params.resume.unwrap_or(false); + let cancellation_token = handoff.cancellation.child_token(); + let client = self.clone(); + let entry = handoff.clone(); + // Do not drop the application future when cancellation wins: its eventual + // original session must still be delivered to on_session_released. + let work = tokio::spawn(async move { + let result = std::panic::AssertUnwindSafe(async { + if resume { + let request = AhpSessionResumeRequest { + config: resume_config_from_host(¶ms.config)?, + cancellation_token, + }; + factory + .resume + .as_ref() + .ok_or_else(|| handoff_error("AHP resume factory is unavailable"))? + .resume_session(request, client.clone()) + .await + } else { + let request = AhpSessionRequest { + config: config_from_host(¶ms.config)?, + cancellation_token, + }; + factory + .create + .as_ref() + .ok_or_else(|| handoff_error("AHP create factory is unavailable"))? + .create_session(request, client.clone()) + .await + } + }) + .catch_unwind() + .await + .map_err(|_| handoff_error("AHP session factory panicked"))?; + let session = result?; + entry.retain(session.clone())?; + session.validate_ahp_handoff(&client, ¶ms.config, resume)?; + Ok(serde_json::json!({"sessionId": session.id()})) + }); + let result = tokio::select! { + biased; + _ = handoff.cancellation.cancelled() => Err(handoff_error("AHP session handoff ended")), + result = work => result.unwrap_or_else(|_| Err(handoff_error("AHP session factory task failed"))), + }; + if result.is_err() + && let Some(sessions) = self.inner.ahp_host_sessions.upgrade() + { + sessions.release(¶ms.host_id, ¶ms.handoff_id); + } + result + } +} + +// These are SDK config names, not session.create wire names (notably +// enableMcpApps and the options.update settings). Reject new unknown settings +// rather than silently losing a host-selected constraint. +macro_rules! host_settings { + ($apply:ident) => { + $apply! { + "workingDirectory" => working_directory, + "configDir" => config_directory, + "enableExperimentalMode" => enable_experimental_mode, + "infiniteSessions" => infinite_sessions, + "additionalDirectories" => additional_directories, + "streaming" => streaming, + "gitHubToken" => github_token, + "mcpOAuthTokenStorage" => mcp_oauth_token_storage, + "enableConfigDiscovery" => enable_config_discovery, + "featureFlags" => feature_flags, + "enableManagedSettings" => enable_managed_settings, + "enableSessionStore" => enable_session_store, + "availableTools" => available_tools, + "excludedTools" => excluded_tools, + "allowedModels" => allowed_models, + "systemMessage" => system_message, + "skipCustomInstructions" => skip_custom_instructions, + "customAgentsLocalOnly" => custom_agents_local_only, + "coauthorEnabled" => coauthor_enabled, + "manageScheduleEnabled" => manage_schedule_enabled, + "memory" => memory, + "pluginDirectories" => plugin_directories, + "skillDirectories" => skill_directories, + "instructionDirectories" => instruction_directories, + "enableMcpApps" => enable_mcp_apps, + "model" => model, + "reasoningEffort" => reasoning_effort, + "contextTier" => context_tier, + } + }; +} + +fn config_from_host(settings: &HashMap) -> Result { + let mut config = SessionConfig::default(); + macro_rules! decode { + ($($name:literal => $field:ident,)*) => { + for (key, value) in settings { + match key.as_str() { + "sessionId" => config.session_id = serde_json::from_value(value.clone())?, + $($name => config.$field = serde_json::from_value(value.clone())?,)* + _ => return Err(handoff_error("Unsupported AHP session configuration setting")), + } + } + }; + } + host_settings!(decode); + Ok(config) +} + +pub(crate) fn config_for_host(config: &SessionConfig) -> Result { + let mut settings = serde_json::Map::new(); + if let Some(id) = &config.session_id { + settings.insert("sessionId".into(), serde_json::to_value(id)?); + } + macro_rules! encode { + ($($name:literal => $field:ident,)*) => { + $(if let Some(value) = &config.$field { + settings.insert($name.to_owned(), serde_json::to_value(value)?); + })* + }; + } + host_settings!(encode); + Ok(Value::Object(settings)) +} + +fn resume_config_from_host( + settings: &HashMap, +) -> Result { + let id = settings + .get("sessionId") + .and_then(Value::as_str) + .ok_or_else(|| handoff_error("AHP resume requires sessionId"))?; + let mut config = ResumeSessionConfig::new(id.into()); + macro_rules! decode { + ($($name:literal => $field:ident,)*) => { + for (key, value) in settings { + match key.as_str() { + "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")), + } + } + }; + } + host_settings!(decode); + Ok(config) +} + +pub(crate) fn resume_config_for_host(config: &ResumeSessionConfig) -> Result { + let mut settings = serde_json::Map::new(); + settings.insert( + "sessionId".into(), + serde_json::to_value(&config.session_id)?, + ); + macro_rules! encode { + ($($name:literal => $field:ident,)*) => { + $(if let Some(value) = &config.$field { + settings.insert($name.to_owned(), serde_json::to_value(value)?); + })* + }; + } + host_settings!(encode); + if let Some(value) = config.continue_pending_work { + settings.insert("continuePendingWork".into(), Value::Bool(value)); + } + if let Some(value) = config.suppress_resume_event { + settings.insert("suppressResumeEvent".into(), Value::Bool(value)); + } + if let Some(value) = config.allow_transcript_recovery { + settings.insert("allowTranscriptRecovery".into(), Value::Bool(value)); + } + Ok(Value::Object(settings)) +} + +pub(crate) fn contains_settings(actual: Option<&Value>, expected: &Value) -> bool { + if let Some(expected) = expected.as_object() { + actual.and_then(Value::as_object).is_some_and(|actual| { + expected + .iter() + .all(|(key, value)| contains_settings(actual.get(key), value)) + }) + } else { + actual == Some(expected) + } +} +#[cfg(test)] +mod tests; diff --git a/rust/src/ahp_host/factory_tests.rs b/rust/src/ahp_host/factory_tests.rs new file mode 100644 index 0000000000..c5140f72f5 --- /dev/null +++ b/rust/src/ahp_host/factory_tests.rs @@ -0,0 +1,762 @@ +use super::*; + +#[path = "lifetime_tests.rs"] +mod lifetime_tests; + +impl Peer { + async fn materialize(&mut self, host_id: &str, handoff: &str, config: Value) { + self.send(json!({ + "jsonrpc": "2.0", "id": 900, "method": "host.materializeSession", + "params": {"hostId": host_id, "handoffId": handoff, "config": config} + })) + .await; + } + + async fn release(&mut self, host_id: &str, handoff: &str) { + self.send(json!({ + "jsonrpc": "2.0", "method": "host.sessionReleased", + "params": {"hostId": host_id, "handoffId": handoff} + })) + .await; + } + + async fn created(&mut self) -> Value { + let request = self.request().await; + assert_eq!(request["method"], "session.create"); + self.respond( + &request, + json!({"sessionId": request["params"]["sessionId"]}), + ) + .await; + request + } +} + +fn factory_options() -> ( + AhpHostOptions, + mpsc::UnboundedReceiver>, + mpsc::UnboundedReceiver>, +) { + let (created_tx, created) = mpsc::unbounded_channel(); + let (released_tx, released) = mpsc::unbounded_channel(); + let options = local_options() + .with_create_session(move |request: AhpSessionRequest, client: Client| { + let created_tx = created_tx.clone(); + async move { + assert!(!request.cancellation_token.is_cancelled()); + assert!(request.config.permission_handler.is_none()); + let session = Arc::new(client.create_session(request.config).await?); + created_tx.send(session.clone()).unwrap(); + Ok(session) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + (options, created, released) +} + +#[tokio::test] +async fn owner_loss_cancels_pending_factory_but_releases_its_late_original() { + let (client, mut peer) = fixture(); + let (ready_tx, mut ready) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let finish = CancellationToken::new(); + let gate = finish.clone(); + let options = local_options() + .with_create_session(move |request: AhpSessionRequest, client: Client| { + let ready_tx = ready_tx.clone(); + let gate = gate.clone(); + async move { + let original = Arc::new(client.create_session(request.config).await?); + ready_tx + .send((original.clone(), request.cancellation_token)) + .unwrap(); + gate.cancelled().await; + Ok(original) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize( + &host.host_id, + "pending-owner-loss", + json!({"sessionId":"pending"}), + ) + .await; + peer.created().await; + let (original, cancellation) = receive(&mut ready).await; + client.force_stop(); + timeout(TIMEOUT, cancellation.cancelled()).await.unwrap(); + assert!(released.try_recv().is_err()); + finish.cancel(); + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); +} + +async fn start_factory(client: &Client, peer: &mut Peer, options: AhpHostOptions) -> AhpHost { + let pending = start(client, options); + let request = peer.request().await; + assert_eq!(request["params"]["sessionFactory"], true); + assert!(request["params"].get("createSession").is_none()); + peer.started(&request, None).await; + pending.await.unwrap().unwrap() +} + +async fn receive(receiver: &mut mpsc::UnboundedReceiver) -> T { + timeout(TIMEOUT, receiver.recv()).await.unwrap().unwrap() +} + +#[tokio::test] +async fn resume_can_return_retained_original_after_creation_handoff_released() { + let (client, mut peer) = fixture(); + let (options, mut created, mut released) = factory_options(); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize(&host.host_id, "create", json!({"sessionId": "retained"})) + .await; + peer.created().await; + assert_eq!( + peer.request().await["result"], + json!({"sessionId": "retained"}) + ); + let original = receive(&mut created).await; + peer.release(&host.host_id, "create").await; + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + let retained = original.clone(); + let (released_tx, mut resumed_released) = mpsc::unbounded_channel(); + let pending = start( + &client, + local_options() + .with_resume_session(move |_: AhpSessionResumeRequest, _: Client| { + let retained = retained.clone(); + async move { Ok(retained) } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }), + ); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + peer.send(json!({ + "jsonrpc": "2.0", "id": 901, "method": "host.materializeSession", + "params": {"hostId": host.host_id, "handoffId": "resume", "resume": true, + "config": {"sessionId": "retained", "continuePendingWork": false}} + })) + .await; + let response = peer.request().await; + assert_eq!(response["id"], 901); + assert_eq!(response["result"], json!({"sessionId": "retained"})); + peer.release(&host.host_id, "resume").await; + assert!(Arc::ptr_eq( + &original, + &receive(&mut resumed_released).await + )); + client.force_stop(); +} + +#[tokio::test] +async fn resume_rejects_superseded_retained_original() { + let (client, mut peer) = fixture(); + let (options, mut created, mut released) = factory_options(); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize(&host.host_id, "create", json!({"sessionId": "retained"})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + let original = receive(&mut created).await; + peer.release(&host.host_id, "create").await; + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + + let resuming = { + let client = client.clone(); + tokio::spawn(async move { + client + .resume_session(ResumeSessionConfig::new("retained".into())) + .await + }) + }; + let request = peer.request().await; + assert_eq!(request["method"], "session.resume"); + peer.respond(&request, json!({"sessionId": "retained"})) + .await; + let reload = peer.request().await; + assert_eq!(reload["method"], "session.skills.reload"); + peer.respond(&reload, json!({})).await; + let current = timeout(TIMEOUT, resuming).await.unwrap().unwrap().unwrap(); + assert!(!original.cancellation_token().is_cancelled()); + let retained = original.clone(); + let (released_tx, mut resumed_released) = mpsc::unbounded_channel(); + let pending = start( + &client, + local_options() + .with_resume_session(move |_: AhpSessionResumeRequest, _: Client| { + let retained = retained.clone(); + async move { Ok(retained) } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }), + ); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + peer.send(json!({ + "jsonrpc": "2.0", "id": 901, "method": "host.materializeSession", + "params": {"hostId": host.host_id, "handoffId": "resume", "resume": true, + "config": {"sessionId": "retained"}} + })) + .await; + let response = peer.request().await; + assert_eq!(response["id"], 901); + assert!(response.get("error").is_some()); + assert!(Arc::ptr_eq( + &original, + &receive(&mut resumed_released).await + )); + let expected = serde_json::from_value(json!({"sessionId": "retained"})).unwrap(); + assert!( + current + .validate_ahp_handoff(&client, &expected, true) + .is_ok() + ); + client.force_stop(); +} + +#[tokio::test] +async fn resume_factory_registered_before_start_retains_original_and_preserves_settings() { + let (client, mut peer) = fixture(); + let (created_tx, mut created) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let pending = start( + &client, + local_options() + .with_resume_session(move |request: AhpSessionResumeRequest, client: Client| { + let created_tx = created_tx.clone(); + async move { + let session = Arc::new(client.resume_session(request.config).await?); + created_tx.send(session.clone()).unwrap(); + Ok(session) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }), + ); + let start = peer.request().await; + let host_id = start["params"]["hostId"].as_str().unwrap(); + assert_eq!(start["params"]["resumeFactory"], true); + assert!(start["params"].get("sessionFactory").is_none()); + peer.send(json!({ + "jsonrpc": "2.0", "id": 900, "method": "host.materializeSession", + "params": { + "hostId": host_id, "handoffId": "resume", "resume": true, + "config": { + "sessionId": "durable", "workingDirectory": "/workspace", + "continuePendingWork": false, "suppressResumeEvent": true + } + } + })) + .await; + let resume = peer.request().await; + assert_eq!(resume["method"], "session.resume"); + assert_eq!(resume["params"]["sessionId"], "durable"); + assert_eq!(resume["params"]["workingDirectory"], "/workspace"); + assert_eq!(resume["params"]["continuePendingWork"], false); + assert_eq!(resume["params"]["disableResume"], true); + peer.respond(&resume, json!({"sessionId": "durable"})).await; + let reload = peer.request().await; + assert_eq!(reload["method"], "session.skills.reload"); + peer.respond(&reload, json!({})).await; + let response = peer.request().await; + assert_eq!( + response["result"], + json!({"sessionId": "durable"}), + "{response}" + ); + let original = receive(&mut created).await; + peer.started(&start, None).await; + pending.await.unwrap().unwrap(); + peer.release(host_id, "resume").await; + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + peer.release(host_id, "resume").await; + client.force_stop(); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); +} + +#[tokio::test] +async fn resume_never_falls_back_to_create_factory() { + let (client, mut peer) = fixture(); + 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", + "params": {"hostId": host.host_id, "handoffId": "resume", "resume": true, + "config": {"sessionId": "durable"}} + })) + .await; + assert!( + peer.request().await["error"]["message"] + .as_str() + .unwrap() + .contains("unavailable") + ); + assert!(client.host_sessions().unwrap().0.lock().handoffs.is_empty()); + client.force_stop(); +} + +#[tokio::test] +async fn cancelled_resume_releases_late_original_once() { + let (client, mut peer) = fixture(); + let (ready_tx, mut ready) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let finish = CancellationToken::new(); + let gate = finish.clone(); + let options = local_options() + .with_resume_session(move |request: AhpSessionResumeRequest, client: Client| { + let ready_tx = ready_tx.clone(); + let gate = gate.clone(); + async move { + let original = Arc::new(client.resume_session(request.config).await?); + ready_tx + .send((original.clone(), request.cancellation_token)) + .unwrap(); + gate.cancelled().await; + Ok(original) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + peer.send(json!({ + "jsonrpc": "2.0", "id": 900, "method": "host.materializeSession", + "params": {"hostId": host.host_id, "handoffId": "late-resume", "resume": true, + "config": {"sessionId": "durable"}} + })) + .await; + let request = peer.request().await; + assert_eq!(request["method"], "session.resume"); + peer.respond(&request, json!({"sessionId": "durable"})) + .await; + let reload = peer.request().await; + assert_eq!(reload["method"], "session.skills.reload"); + peer.respond(&reload, json!({})).await; + let (original, cancellation) = receive(&mut ready).await; + peer.release(&host.host_id, "late-resume").await; + assert!(peer.request().await.get("error").is_some()); + assert!(cancellation.is_cancelled()); + assert!(released.try_recv().is_err()); + finish.cancel(); + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + client.force_stop(); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); +} + +#[tokio::test] +async fn factory_registered_before_start_response_retains_exact_original_once() { + let (client, mut peer) = fixture(); + let (options, mut created, mut released) = factory_options(); + let pending = start(&client, options); + let request = peer.request().await; + let host_id = request["params"]["hostId"].as_str().unwrap(); + assert_eq!(request["params"]["sessionFactory"], true); + peer.materialize( + host_id, + "handoff", + json!({ + "sessionId": "requested", "workingDirectory": "/workspace", "streaming": true, + "enableMcpApps": true, "enableConfigDiscovery": false, + "mcpOAuthTokenStorage": "in-memory" + }), + ) + .await; + let create = peer.created().await; + assert_eq!(create["params"]["workingDirectory"], "/workspace"); + assert_eq!(create["params"]["requestMcpApps"], true); + assert_eq!(create["params"]["requestPermission"], false); + assert_eq!(create["params"]["mcpOAuthTokenStorage"], "in-memory"); + assert!(create["params"].get("mcpOauthTokenStorage").is_none()); + assert_eq!( + peer.request().await["result"], + json!({"sessionId": "requested"}) + ); + let original = receive(&mut created).await; + peer.started(&request, None).await; + pending.await.unwrap().unwrap(); + peer.release("other-host", "handoff").await; + peer.release(host_id, "handoff").await; + let returned = receive(&mut released).await; + assert!(Arc::ptr_eq(&original, &returned)); + peer.release(host_id, "handoff").await; + peer.exited(host_id).await; + // Round-trip barrier also proves that release did not send any session RPC. + let session = original.clone(); + let events = tokio::spawn(async move { session.get_events().await }); + let request = peer.request().await; + assert_eq!(request["method"], "session.getMessages"); + peer.respond(&request, json!({"events": []})).await; + events.await.unwrap().unwrap(); + assert!(released.try_recv().is_err()); + assert!(client.host_sessions().unwrap().0.lock().handoffs.is_empty()); +} + +#[tokio::test] +async fn cancellation_responds_promptly_and_releases_late_original_once() { + let (client, mut peer) = fixture(); + let (ready_tx, mut ready) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let finish = CancellationToken::new(); + let gate = finish.clone(); + let options = local_options() + .with_create_session(move |request: AhpSessionRequest, client: Client| { + let ready_tx = ready_tx.clone(); + let gate = gate.clone(); + async move { + let original = Arc::new(client.create_session(request.config).await?); + ready_tx + .send((original.clone(), request.cancellation_token.clone())) + .unwrap(); + gate.cancelled().await; + Ok(original) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize(&host.host_id, "late", json!({"sessionId": "late-session"})) + .await; + peer.created().await; + let (original, cancelled) = receive(&mut ready).await; + peer.release(&host.host_id, "late").await; + let response = peer.request().await; + assert_eq!(response["id"], 900); + assert!(response.get("error").is_some()); + assert!(cancelled.is_cancelled()); + assert!(released.try_recv().is_err()); + finish.cancel(); + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + peer.exited(&host.host_id).await; + client.force_stop(); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); +} + +#[tokio::test] +async fn callback_failure_and_panic_fail_handoff_without_release() { + for panic in [false, true] { + let (client, mut peer) = fixture(); + let options = local_options().with_create_session( + move |_: AhpSessionRequest, _: Client| async move { + assert!(!panic, "application panic"); + Err(handoff_error("application failure")) + }, + ); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize(&host.host_id, "failed", json!({"sessionId": "failed"})) + .await; + assert!(peer.request().await.get("error").is_some()); + assert!(client.host_sessions().unwrap().0.lock().handoffs.is_empty()); + client.force_stop(); + } +} + +#[tokio::test] +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 = local_options() + .with_create_session( + |mut request: AhpSessionRequest, client: Client| async move { + request.config.skip_custom_instructions = None; + Ok(Arc::new(client.create_session(request.config).await?)) + }, + ) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize( + &host.host_id, + "altered", + json!({ + "sessionId": "altered", "skipCustomInstructions": true + }), + ) + .await; + peer.created().await; + let error = peer.request().await; + assert!( + error["error"]["message"] + .as_str() + .unwrap() + .contains("preserve") + ); + assert_eq!(receive(&mut released).await.id().as_str(), "altered"); + client.force_stop(); +} + +#[tokio::test] +async fn host_exit_transport_loss_and_force_stop_release_original_once() { + for end in ["host-exit", "transport-loss", "force-stop"] { + let (client, mut peer) = fixture(); + let (options, mut created, mut released) = factory_options(); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize(&host.host_id, "ending", json!({"sessionId": "ending"})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + let original = receive(&mut created).await; + match end { + "host-exit" => peer.exited(&host.host_id).await, + "transport-loss" => { + peer.write.shutdown().await.unwrap(); + } + _ => client.force_stop(), + } + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + client.force_stop(); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); + assert!( + client + .host_sessions() + .unwrap() + .0 + .lock() + .factories + .is_empty() + ); + } +} + +#[tokio::test] +async fn failed_and_cancelled_start_remove_factory() { + for cancel in [false, true] { + let (client, mut peer) = fixture(); + let (options, _, mut released) = factory_options(); + let pending = start(&client, options); + let request = peer.request().await; + if cancel { + pending.abort(); + assert!(pending.await.unwrap_err().is_cancelled()); + peer.started(&request, None).await; + let disposal = peer.request().await; + assert_eq!(disposal["method"], "host.dispose"); + peer.respond(&disposal, json!({})).await; + } else { + peer.send(json!({"jsonrpc":"2.0", "id": request["id"], + "error": {"code": -32000, "message": "start failed"}})) + .await; + assert!(pending.await.unwrap().is_err()); + } + assert!( + client + .host_sessions() + .unwrap() + .0 + .lock() + .factories + .is_empty() + ); + peer.materialize( + request["params"]["hostId"].as_str().unwrap(), + "after-failure", + json!({"sessionId":"after-failure"}), + ) + .await; + assert!(peer.request().await.get("error").is_some()); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); + } +} + +#[test] +fn host_settings_round_trip_without_callbacks_or_losing_constraints() { + let value = json!({ + "sessionId": "id", "workingDirectory": "/work", "additionalDirectories": ["/extra"], + "configDir": "/config", "enableExperimentalMode": true, + "infiniteSessions": {"enabled": false, "backgroundCompactionThreshold": 0.7}, + "streaming": true, "gitHubToken": "test", "mcpOAuthTokenStorage": "in-memory", + "enableConfigDiscovery": false, "featureFlags": {"flag": true}, + "enableManagedSettings": false, "enableSessionStore": true, + "availableTools": ["read_file"], "excludedTools": ["shell"], "allowedModels": ["model"], + "systemMessage": {"mode":"append","content":"app"}, "skipCustomInstructions": true, + "customAgentsLocalOnly": true, "coauthorEnabled": false, "manageScheduleEnabled": false, + "memory": {"enabled":false}, "pluginDirectories": ["/plugins"], + "skillDirectories": ["/skills"], "instructionDirectories": ["/instructions"], + "enableMcpApps": true, "model":"model", "reasoningEffort":"high", "contextTier":"long_context" + }); + let config = config_from_host(&serde_json::from_value(value.clone()).unwrap()).unwrap(); + assert!(config.permission_handler.is_none()); + assert_eq!(config.config_directory, Some(PathBuf::from("/config"))); + assert_eq!(config.enable_experimental_mode, Some(true)); + assert_eq!( + config.infinite_sessions.as_ref().unwrap().enabled, + Some(false) + ); + assert_eq!(config_for_host(&config).unwrap(), value); + 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)); + assert_eq!( + resume.infinite_sessions.as_ref().unwrap().enabled, + Some(false) + ); + 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}})), + &json!({"a":{"b":true}}) + )); + assert!(!contains_settings( + Some(&json!({"a":["b","c"]})), + &json!({"a":["b"]}) + )); + + let mut resume = crate::ResumeSessionConfig::new(crate::SessionId::from("id")); + resume.mcp_oauth_token_storage = Some("in-memory".into()); + let (wire, _) = resume.into_wire().unwrap(); + let wire = serde_json::to_value(wire).unwrap(); + assert_eq!(wire["mcpOAuthTokenStorage"], "in-memory"); + assert!(wire.get("mcpOauthTokenStorage").is_none()); +} + +#[tokio::test] +async fn release_before_request_poll_prevents_factory_invocation() { + let (client, mut peer) = fixture(); + let (options, mut created, _) = factory_options(); + let host = start_factory(&client, &mut peer, options).await; + let params: HostSessionCreateCallback = serde_json::from_value(json!({ + "hostId": host.host_id, "handoffId": "ended", "config": {"sessionId":"ended"} + })) + .unwrap(); + let (factory, handoff) = client.prepare_ahp_session(¶ms).unwrap(); + assert!(client.prepare_ahp_session(¶ms).is_err()); + client + .host_sessions() + .unwrap() + .release(&host.host_id, "ended"); + assert!( + client + .materialize_ahp_session(params, factory, handoff) + .await + .is_err() + ); + assert!(created.try_recv().is_err()); + client.force_stop(); +} + +#[tokio::test] +async fn completed_handoffs_and_unknown_releases_leave_no_history() { + let (client, mut peer) = fixture(); + let (options, mut created, mut released) = factory_options(); + let host = start_factory(&client, &mut peer, options).await; + for n in 0..128 { + let id = format!("session-{n}"); + peer.materialize(&host.host_id, &id, json!({"sessionId": id})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + let original = receive(&mut created).await; + peer.release(&host.host_id, &id).await; + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + // Duplicate and unknown releases must not allocate tombstones. + let sessions = client.host_sessions().unwrap(); + sessions.release(&host.host_id, &id); + sessions.release(&host.host_id, &format!("unknown-{n}")); + assert!(sessions.0.lock().handoffs.is_empty()); + } + client.force_stop(); +} + +#[tokio::test] +async fn callback_returning_another_clients_session_is_released_not_destroyed() { + let (other, mut other_peer) = fixture(); + let create = tokio::spawn(async move { + other + .create_session(SessionConfig::default().with_session_id("foreign")) + .await + .unwrap() + }); + other_peer.created().await; + let original = Arc::new(create.await.unwrap()); + let (client, mut peer) = fixture(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let session = original.clone(); + let host = start_factory( + &client, + &mut peer, + local_options() + .with_create_session(move |_: AhpSessionRequest, _: Client| { + let session = session.clone(); + async move { Ok(session) } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }), + ) + .await; + peer.materialize(&host.host_id, "foreign", json!({"sessionId":"foreign"})) + .await; + assert!(peer.request().await.get("error").is_some()); + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + let session = original.clone(); + let events = tokio::spawn(async move { session.get_events().await }); + let request = other_peer.request().await; + assert_eq!(request["method"], "session.getMessages"); + other_peer.respond(&request, json!({"events":[]})).await; + events.await.unwrap().unwrap(); + client.force_stop(); +} + +#[tokio::test] +async fn release_callback_panic_does_not_prevent_other_handoff_cleanup() { + let (client, mut peer) = fixture(); + let calls = Arc::new(AtomicUsize::new(0)); + let counter = calls.clone(); + let (options, mut created, _) = factory_options(); + let host = start_factory( + &client, + &mut peer, + options.with_on_session_released(move |_| { + counter.fetch_add(1, Ordering::SeqCst); + panic!("release panic"); + }), + ) + .await; + let mut originals = Vec::new(); + for id in ["one", "two"] { + peer.materialize(&host.host_id, id, json!({"sessionId":id})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + originals.push(receive(&mut created).await); + } + peer.exited(&host.host_id).await; + timeout(TIMEOUT, async { + while calls.load(Ordering::SeqCst) != 2 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + client.force_stop(); + assert!(client.host_sessions().unwrap().0.lock().handoffs.is_empty()); +} diff --git a/rust/src/ahp_host/lifetime_tests.rs b/rust/src/ahp_host/lifetime_tests.rs new file mode 100644 index 0000000000..e4699008d3 --- /dev/null +++ b/rust/src/ahp_host/lifetime_tests.rs @@ -0,0 +1,188 @@ +use super::*; + +#[test] +fn runtime_shutdown_cannot_lose_retained_session_release() { + for drop_order in ["after-shutdown", "queued-cleanup", "pending-cleanup"] { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (client, peer, original, mut released) = runtime.block_on(async { + let (client, mut peer) = fixture(); + let (options, mut created, released) = factory_options(); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize(&host.host_id, "owned", json!({"sessionId":"owned"})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + let original = receive(&mut created).await; + drop(host); + let client = if drop_order == "after-shutdown" { + Some(client) + } else { + drop(client); + if drop_order == "pending-cleanup" { + assert_eq!(peer.request().await["method"], "host.dispose"); + } + None + }; + (client, peer, original, released) + }); + assert!(released.try_recv().is_err()); + drop(runtime); + drop(client); + assert!(Arc::ptr_eq(&original, &released.try_recv().unwrap())); + assert!(matches!( + released.try_recv(), + Err(mpsc::error::TryRecvError::Disconnected) + )); + drop(peer); + } +} + +#[tokio::test] +async fn dropping_all_external_owners_reclaims_successful_handoff() { + let (client, mut peer) = fixture(); + let owner = Arc::downgrade(&client.inner); + let retention = client.inner.ahp_host_sessions.clone(); + let releases = Arc::new(AtomicUsize::new(0)); + let count = releases.clone(); + let (options, mut created, _) = factory_options(); + let host = start_factory( + &client, + &mut peer, + options.with_on_session_released(move |_| { + count.fetch_add(1, Ordering::SeqCst); + }), + ) + .await; + peer.materialize(&host.host_id, "owned", json!({"sessionId":"owned"})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + let original = receive(&mut created).await; + let session = Arc::downgrade(&original); + drop(original); + drop(created); + drop(host); + drop(client); + assert!( + retention.upgrade().is_none(), + "retention root must not be owned by its sessions" + ); + let cleanup = peer.request().await; + assert_eq!(cleanup["method"], "host.dispose"); + peer.respond(&cleanup, json!({})).await; + timeout(TIMEOUT, async { + while owner.upgrade().is_some() || session.upgrade().is_some() { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + assert_eq!(releases.load(Ordering::SeqCst), 1); + assert!( + timeout(TIMEOUT, peer.read.read_u8()) + .await + .unwrap() + .is_err() + ); +} + +#[tokio::test] +async fn app_retained_session_outlives_owner_dropped_on_plain_thread() { + let (client, mut peer) = fixture(); + let owner = Arc::downgrade(&client.inner); + let (options, mut created, mut released) = factory_options(); + let host = start_factory(&client, &mut peer, options).await; + peer.materialize(&host.host_id, "app-owned", json!({"sessionId":"app-owned"})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + let original = receive(&mut created).await; + drop(created); + drop(host); + std::thread::spawn(move || drop(client)).join().unwrap(); + assert!( + released.try_recv().is_err(), + "release must wait for host disposal" + ); + let cleanup = peer.request().await; + assert_eq!(cleanup["method"], "host.dispose"); + peer.respond(&cleanup, json!({})).await; + assert!(Arc::ptr_eq(&original, &receive(&mut released).await)); + assert!( + owner.upgrade().is_some(), + "app session must keep its connection alive" + ); + let session = original.clone(); + let events = tokio::spawn(async move { session.get_events().await }); + let request = peer.request().await; + assert_eq!(request["method"], "session.getMessages"); + peer.respond(&request, json!({"events":[]})).await; + events.await.unwrap().unwrap(); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); + drop(original); + timeout(TIMEOUT, async { + while owner.upgrade().is_some() { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn blocked_release_callback_cannot_lose_another_release_behind_notification_flood() { + let (client, mut peer) = fixture(); + let (created_tx, mut created) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let (blocked_tx, mut blocked) = mpsc::unbounded_channel(); + let (unblock, gate) = std::sync::mpsc::channel::<()>(); + let gate = std::sync::Mutex::new(gate); + let options = local_options() + .with_create_session(move |request: AhpSessionRequest, client: Client| { + let created_tx = created_tx.clone(); + async move { + let session = Arc::new(client.create_session(request.config).await?); + created_tx + .send((session.clone(), request.cancellation_token)) + .unwrap(); + Ok(session) + } + }) + .with_on_session_released(move |session| { + if session.id().as_str() == "blocked" { + blocked_tx.send(()).unwrap(); + gate.lock().unwrap().recv_timeout(TIMEOUT * 2).unwrap(); + } + released_tx.send(session).unwrap(); + }); + let host = start_factory(&client, &mut peer, options).await; + for id in ["blocked", "following"] { + peer.materialize(&host.host_id, id, json!({"sessionId":id})) + .await; + peer.created().await; + assert!(peer.request().await.get("result").is_some()); + } + let (first, first_cancelled) = receive(&mut created).await; + let (second, second_cancelled) = receive(&mut created).await; + peer.release(&host.host_id, "blocked").await; + receive(&mut blocked).await; + assert!(first_cancelled.is_cancelled()); + for n in 0..2048 { + peer.send(json!({"jsonrpc":"2.0", "method":"unrelated", "params":{"n":n}})) + .await; + } + peer.release(&host.host_id, "following").await; + timeout(TIMEOUT, second_cancelled.cancelled()) + .await + .unwrap(); + assert!(Arc::ptr_eq(&second, &receive(&mut released).await)); + unblock.send(()).unwrap(); + assert!(Arc::ptr_eq(&first, &receive(&mut released).await)); + peer.release(&host.host_id, "blocked").await; + peer.release(&host.host_id, "following").await; + client.force_stop(); + assert!(timeout(TIMEOUT, released.recv()).await.unwrap().is_none()); +} diff --git a/rust/src/ahp_host/tests.rs b/rust/src/ahp_host/tests.rs new file mode 100644 index 0000000000..90f7ed55df --- /dev/null +++ b/rust/src/ahp_host/tests.rs @@ -0,0 +1,730 @@ +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use serde_json::{Value, json}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream, duplex}; +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); + +#[path = "factory_tests.rs"] +mod factory_tests; + +// Same Content-Length duplex protocol fixture as tests/prepared_session_test.rs. +struct Peer { + read: DuplexStream, + write: DuplexStream, +} + +impl Peer { + async fn request(&mut self) -> Value { + timeout(TIMEOUT, async { + let mut header = Vec::new(); + while !header.ends_with(b"\r\n\r\n") { + header.push(self.read.read_u8().await.unwrap()); + } + let length: usize = std::str::from_utf8(&header) + .unwrap() + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut body = vec![0; length]; + self.read.read_exact(&mut body).await.unwrap(); + serde_json::from_slice(&body).unwrap() + }) + .await + .unwrap() + } + + async fn send(&mut self, value: Value) { + let body = serde_json::to_vec(&value).unwrap(); + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + self.write.write_all(header.as_bytes()).await.unwrap(); + self.write.write_all(&body).await.unwrap(); + self.write.flush().await.unwrap(); + } + + async fn respond(&mut self, request: &Value, result: Value) { + self.send(json!({"jsonrpc": "2.0", "id": request["id"], "result": result})) + .await; + } + + async fn started(&mut self, request: &Value, token: Option<&str>) { + let mut result = json!({ + "hostId": request["params"]["hostId"], + "url": "http://127.0.0.1:4321" + }); + if let Some(token) = token { + result["token"] = json!(token); + } + self.respond(request, result).await; + } + + async fn exited(&mut self, host_id: &str) { + self.send(json!({ + "jsonrpc": "2.0", "method": "host.exited", + "params": {"hostId": host_id, "reason": "exited", "exitCode": 17} + })) + .await; + } +} + +fn fixture() -> (Client, Peer) { + let (client_write, read) = duplex(1 << 20); + let (write, client_read) = duplex(1 << 20); + ( + Client::from_streams(client_read, client_write, PathBuf::from(".")).unwrap(), + Peer { read, write }, + ) +} + +fn start( + client: &Client, + options: AhpHostOptions, +) -> tokio::task::JoinHandle> { + let client = client.clone(); + tokio::spawn(async move { client.start_ahp_host(options).await }) +} + +fn callback_options() -> (AhpHostOptions, mpsc::UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded_channel(); + ( + 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_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"); + let host_id = request["params"]["hostId"].as_str().unwrap(); + assert_eq!(uuid::Uuid::parse_str(host_id).unwrap().get_version_num(), 4); + assert_eq!( + request["params"], + json!({ + "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.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, local_options()); + let request = peer.request().await; + peer.respond( + &request, + json!({ + "hostId": request["params"]["hostId"], + "url": "ws://127.0.0.1:4321", + "pid": 1234 + }), + ) + .await; + assert_eq!(pending.await.unwrap().unwrap().pid, Some(1234)); +} + +#[tokio::test] +async fn listener_task_exit_has_no_process_exit_code() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + peer.send(json!({ + "jsonrpc": "2.0", "method": "host.exited", + "params": {"hostId": host.host_id, "reason": "exited"} + })) + .await; + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_eq!(exit.reason, AhpHostExitReason::Exited); + assert_eq!(exit.exit_code, None); +} + +#[tokio::test] +async fn explicit_local_defaults_are_forwarded_and_token_can_be_absent() { + let (client, mut peer) = fixture(); + let pending = start(&client, local_options()); + let request = peer.request().await; + 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()); +} + +#[tokio::test] +async fn publication_forwards_existing_identity_to_the_owning_listener() { + let (client, mut peer) = fixture(); + let pending = start(&client, local_options()); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + let host_id = host.host_id.clone(); + let publication = tokio::spawn(async move { host.publish_session("resident").await }); + let request = peer.request().await; + assert_eq!(request["method"], "host.publishSession"); + assert_eq!( + request["params"], + json!({"hostId": host_id, "sessionId": "resident"}) + ); + peer.respond( + &request, + json!({"sessionId": "resident", "sessionUri": "copilot:/resident"}), + ) + .await; + let result = publication.await.unwrap().unwrap(); + assert_eq!(result.session_id, "resident"); + assert_eq!(result.session_uri, "copilot:/resident"); +} + +#[tokio::test] +async fn listener_validation_is_left_to_the_runtime() { + let (client, mut peer) = fixture(); + let pending = start( + &client, + 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"]["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"} + })) + .await; + let error = pending.await.unwrap().unwrap_err(); + assert!(matches!(error.kind(), ErrorKind::Rpc { code: -32602 })); + assert!(error.to_string().contains("invalid listener")); +} + +#[tokio::test] +async fn correlates_early_exit_and_delivers_at_most_once() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + let host_id = request["params"]["hostId"].as_str().unwrap(); + peer.exited("unrelated-host").await; + peer.exited(host_id).await; + peer.exited(host_id).await; + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_eq!(exit.host_id, host_id); + assert_eq!(exit.reason, AhpHostExitReason::Exited); + assert_eq!(exit.exit_code, Some(17)); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); + peer.started(&request, None).await; + assert_eq!(pending.await.unwrap().unwrap().host_id, host_id); +} + +#[tokio::test] +async fn concurrent_hosts_have_independent_callbacks_and_ids() { + let (client, mut peer) = fixture(); + let (first_options, mut first_exits) = callback_options(); + let first = start(&client, first_options); + let first_request = peer.request().await; + let (second_options, mut second_exits) = callback_options(); + let second = start(&client, second_options); + let second_request = peer.request().await; + let first_id = first_request["params"]["hostId"].as_str().unwrap(); + let second_id = second_request["params"]["hostId"].as_str().unwrap(); + assert_ne!(first_id, second_id); + peer.started(&first_request, None).await; + peer.started(&second_request, None).await; + first.await.unwrap().unwrap(); + second.await.unwrap().unwrap(); + peer.exited(second_id).await; + assert_eq!( + timeout(TIMEOUT, second_exits.recv()) + .await + .unwrap() + .unwrap() + .host_id, + second_id + ); + assert!(first_exits.try_recv().is_err()); + peer.exited(first_id).await; + assert_eq!( + timeout(TIMEOUT, first_exits.recv()) + .await + .unwrap() + .unwrap() + .host_id, + first_id + ); +} + +#[tokio::test] +async fn failed_start_releases_callback_without_synthetic_exit() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + assert_eq!(client.inner.ahp_host_callbacks.lock().len(), 1); + peer.send(json!({ + "jsonrpc": "2.0", "id": request["id"], + "error": {"code": -32000, "message": "could not start"} + })) + .await; + assert!(pending.await.unwrap().is_err()); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + peer.exited(request["params"]["hostId"].as_str().unwrap()) + .await; +} + +#[tokio::test] +async fn cancelled_start_disposes_after_late_success() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + pending.abort(); + assert!(pending.await.unwrap_err().is_cancelled()); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + assert!( + timeout(Duration::from_millis(50), peer.read.read_u8()) + .await + .is_err() + ); + peer.started(&request, None).await; + let disposal = peer.request().await; + assert_eq!(disposal["method"], "host.dispose"); + assert_eq!(disposal["params"]["hostId"], request["params"]["hostId"]); + peer.respond(&disposal, json!({})).await; +} + +#[tokio::test] +async fn cancelled_start_does_not_dispose_after_late_failure() { + let (client, mut peer) = fixture(); + let pending = start(&client, local_options()); + let request = peer.request().await; + pending.abort(); + assert!(pending.await.unwrap_err().is_cancelled()); + peer.send(json!({ + "jsonrpc": "2.0", "id": request["id"], + "error": {"code": -32000, "message": "could not start"} + })) + .await; + assert!( + timeout(Duration::from_millis(50), peer.read.read_u8()) + .await + .is_err() + ); +} + +#[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(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()); + peer.started(&request, None).await; + let disposal = peer.request().await; + assert_eq!(disposal["method"], "host.dispose"); + assert_eq!(disposal["params"]["hostId"], request["params"]["hostId"]); + peer.respond(&disposal, json!({})).await; +} + +#[tokio::test] +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, local_options()); + peer.request().await; + pending.abort(); + assert!(pending.await.unwrap_err().is_cancelled()); + drop(client); + assert!(owner.upgrade().is_none()); + assert!( + timeout(TIMEOUT, peer.read.read_u8()) + .await + .unwrap() + .is_err() + ); +} + +#[tokio::test] +async fn dispose_forwards_concurrent_repeated_calls_and_runtime_errors() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + let first = tokio::spawn({ + let host = host.clone(); + async move { host.dispose().await } + }); + let second = tokio::spawn({ + let host = host.clone(); + async move { host.dispose().await } + }); + let first_request = peer.request().await; + let second_request = peer.request().await; + assert_ne!(first_request["id"], second_request["id"]); + for request in [&first_request, &second_request] { + assert_eq!(request["method"], "host.dispose"); + assert_eq!(request["params"], json!({"hostId": host.host_id})); + } + peer.respond(&second_request, json!({})).await; + peer.respond(&first_request, json!({})).await; + first.await.unwrap().unwrap(); + second.await.unwrap().unwrap(); + assert_eq!(client.inner.ahp_host_callbacks.lock().len(), 1); + assert!( + exits.try_recv().is_err(), + "dispose must not synthesize exit" + ); + + let third = tokio::spawn({ + let host = host.clone(); + async move { host.dispose().await } + }); + let request = peer.request().await; + assert_eq!(request["method"], "host.dispose"); + peer.send(json!({ + "jsonrpc": "2.0", "id": request["id"], + "error": {"code": -32001, "message": "runtime disposal error"} + })) + .await; + assert!(matches!( + third.await.unwrap().unwrap_err().kind(), + ErrorKind::Rpc { code: -32001 } + )); + peer.exited(&host.host_id).await; + timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + let fourth = tokio::spawn(async move { host.dispose().await }); + let request = peer.request().await; + assert_eq!(request["method"], "host.dispose"); + peer.respond(&request, json!({})).await; + fourth.await.unwrap().unwrap(); +} + +#[tokio::test] +async fn disconnect_notifies_once_without_claiming_reaping() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + drop(peer); + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_owner_disconnected(&exit, &host.host_id); + client.force_stop(); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); + assert!(host.dispose().await.is_err()); +} + +#[tokio::test] +async fn force_stop_notifies_once_without_dispose_loop() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + client.force_stop(); + client.force_stop(); + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_owner_disconnected(&exit, &host.host_id); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); + assert_eq!( + timeout(TIMEOUT, peer.read.read_u8()) + .await + .unwrap() + .unwrap_err() + .kind(), + std::io::ErrorKind::UnexpectedEof + ); +} + +fn assert_owner_disconnected(exit: &AhpHostExit, host_id: &str) { + assert_eq!(exit.host_id, host_id); + assert_eq!(exit.reason, AhpHostExitReason::OwnerDisconnected); + assert_eq!(exit.exit_code, None); + assert!( + exit.error + .as_deref() + .unwrap() + .contains("runtime cleanup cannot be acknowledged") + ); +} + +#[tokio::test] +async fn queued_real_exit_precedes_disconnect_and_is_not_duplicated() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + let (options, mut remaining_exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let remaining = pending.await.unwrap().unwrap(); + + // The transport must enqueue these exits before publishing its EOF token. + for _ in 0..2 { + peer.exited(&host.host_id).await; + } + peer.write.shutdown().await.unwrap(); + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_eq!(exit.host_id, host.host_id); + assert_eq!(exit.reason, AhpHostExitReason::Exited); + assert_eq!(exit.exit_code, Some(17)); + assert_eq!(exit.error, None); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + let exit = timeout(TIMEOUT, remaining_exits.recv()) + .await + .unwrap() + .unwrap(); + assert_owner_disconnected(&exit, &remaining.host_id); + assert!( + timeout(TIMEOUT, remaining_exits.recv()) + .await + .unwrap() + .is_none() + ); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); +} + +#[tokio::test] +async fn wire_exit_immediately_before_eof_is_not_replaced_by_disconnect() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + peer.exited(&host.host_id).await; + drop(peer); + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_eq!(exit.reason, AhpHostExitReason::Exited); + assert_eq!(exit.exit_code, Some(17)); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); +} + +#[tokio::test] +async fn disconnect_notifies_all_callbacks_even_when_they_panic() { + let (client, mut peer) = fixture(); + let (tx, mut exits) = mpsc::unbounded_channel(); + let mut host_ids = Vec::new(); + for _ in 0..3 { + let tx = tx.clone(); + let pending = start( + &client, + local_options().with_on_exit(move |exit| { + tx.send(exit).unwrap(); + // 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; + peer.started(&request, None).await; + host_ids.push(pending.await.unwrap().unwrap().host_id); + } + drop(tx); + client.force_stop(); + for _ in 0..3 { + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + let index = host_ids.iter().position(|id| id == &exit.host_id).unwrap(); + assert_owner_disconnected(&exit, &host_ids.remove(index)); + } + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); +} + +#[tokio::test] +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, + local_options().with_on_exit({ + let calls = calls.clone(); + move |exit| { + gate.lock().unwrap().recv().unwrap(); + calls.fetch_add(1, Ordering::SeqCst); + first_tx.send(exit).unwrap(); + // Exercise unwinding without blocking channel closure on panic-hook output. + std::panic::resume_unwind(Box::new("test callback panic")); + } + }), + ); + let request = peer.request().await; + peer.started(&request, None).await; + let first = pending.await.unwrap().unwrap(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let second = pending.await.unwrap().unwrap(); + peer.exited(&first.host_id).await; + 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 }); + let request = peer.request().await; + peer.respond(&request, json!({})).await; + dispose.await.unwrap().unwrap(); +} + +#[tokio::test] +async fn handle_and_dispatcher_do_not_keep_client_alive() { + let (client, mut peer) = fixture(); + let client_weak = Arc::downgrade(&client.inner); + let callbacks_weak = Arc::downgrade(&client.inner.ahp_host_callbacks); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + let host = pending.await.unwrap().unwrap(); + drop(client); + assert!(client_weak.upgrade().is_none()); + let exit = timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_owner_disconnected(&exit, &host.host_id); + assert!(timeout(TIMEOUT, exits.recv()).await.unwrap().is_none()); + assert!(callbacks_weak.upgrade().is_none()); + assert!(host.dispose().await.is_err()); +} + +#[tokio::test] +async fn dropping_handle_does_not_dispose_or_unregister_callback() { + let (client, mut peer) = fixture(); + let (options, mut exits) = callback_options(); + let pending = start(&client, options); + let request = peer.request().await; + peer.started(&request, None).await; + drop(pending.await.unwrap().unwrap()); + assert_eq!(client.inner.ahp_host_callbacks.lock().len(), 1); + assert!( + timeout(Duration::from_millis(50), peer.read.read_u8()) + .await + .is_err() + ); + peer.exited(request["params"]["hostId"].as_str().unwrap()) + .await; + timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); +} 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.rs b/rust/src/copilot_request_handler.rs index efd5c03466..381acac0d2 100644 --- a/rust/src/copilot_request_handler.rs +++ b/rust/src/copilot_request_handler.rs @@ -1060,6 +1060,7 @@ pub(crate) struct CopilotRequestDispatcher { } impl CopilotRequestDispatcher { + #[cfg(any(feature = "runtime", test))] pub(crate) fn new(handler: Arc) -> Self { Self { handler, @@ -1068,6 +1069,7 @@ impl CopilotRequestDispatcher { } } + #[cfg(any(feature = "runtime", test))] pub(crate) fn set_client(&self, client: Weak) { let _ = self.client.set(client); } 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/extension_launch_provider.rs b/rust/src/extension_launch_provider.rs index 63e2284144..6db13dbfa7 100644 --- a/rust/src/extension_launch_provider.rs +++ b/rust/src/extension_launch_provider.rs @@ -58,6 +58,7 @@ impl ExtensionLaunchProviderDispatcher { let _ = self.client.set(client); } + #[cfg(feature = "runtime")] pub(crate) fn is_configured(&self) -> bool { self.handler.read().is_some() } 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 7a4d3bde2b..8069bbf04a 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -22,6 +22,28 @@ 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` + pub const HOST_CREATESESSION: &str = "host.createSession"; + /// `host.releaseSession` + pub const HOST_RELEASESESSION: &str = "host.releaseSession"; + /// `host.start` + 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` + pub const HOST_READY: &str = "host.ready"; /// `ping` pub const PING: &str = "ping"; /// `connect` @@ -34,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` @@ -66,12 +98,54 @@ pub mod rpc_methods { pub const MCP_DISCOVER: &str = "mcp.discover"; /// `mcp.planInstall` pub const MCP_PLANINSTALL: &str = "mcp.planInstall"; + /// `mcp.prepareInstall` + pub const MCP_PREPAREINSTALL: &str = "mcp.prepareInstall"; + /// `mcp.applyInstall` + pub const MCP_APPLYINSTALL: &str = "mcp.applyInstall"; + /// `mcp.planUninstall` + pub const MCP_PLANUNINSTALL: &str = "mcp.planUninstall"; + /// `mcp.applyUninstall` + pub const MCP_APPLYUNINSTALL: &str = "mcp.applyUninstall"; + /// `mcp.installations.list` + pub const MCP_INSTALLATIONS_LIST: &str = "mcp.installations.list"; + /// `mcp.installations.recover` + pub const MCP_INSTALLATIONS_RECOVER: &str = "mcp.installations.recover"; + /// `mcp.installations.status` + pub const MCP_INSTALLATIONS_STATUS: &str = "mcp.installations.status"; + /// `mcp.installations.cancel` + pub const MCP_INSTALLATIONS_CANCEL: &str = "mcp.installations.cancel"; /// `extensions.discover` pub const EXTENSIONS_DISCOVER: &str = "extensions.discover"; /// `extensions.enable` pub const EXTENSIONS_ENABLE: &str = "extensions.enable"; /// `extensions.disable` pub const EXTENSIONS_DISABLE: &str = "extensions.disable"; + /// `skills.planInstall` + pub const SKILLS_PLANINSTALL: &str = "skills.planInstall"; + /// `skills.applyInstall` + pub const SKILLS_APPLYINSTALL: &str = "skills.applyInstall"; + /// `skills.installations.list` + pub const SKILLS_INSTALLATIONS_LIST: &str = "skills.installations.list"; + /// `skills.installations.recover` + pub const SKILLS_INSTALLATIONS_RECOVER: &str = "skills.installations.recover"; + /// `skills.installations.status` + pub const SKILLS_INSTALLATIONS_STATUS: &str = "skills.installations.status"; + /// `skills.installations.cancel` + pub const SKILLS_INSTALLATIONS_CANCEL: &str = "skills.installations.cancel"; + /// `skills.installations.setEnabled` + pub const SKILLS_INSTALLATIONS_SETENABLED: &str = "skills.installations.setEnabled"; + /// `skills.planUninstall` + pub const SKILLS_PLANUNINSTALL: &str = "skills.planUninstall"; + /// `skills.applyUninstall` + pub const SKILLS_APPLYUNINSTALL: &str = "skills.applyUninstall"; + /// `skills.config.setDisabledSkills` + pub const SKILLS_CONFIG_SETDISABLEDSKILLS: &str = "skills.config.setDisabledSkills"; + /// `skills.config.setSkillDisabled` + pub const SKILLS_CONFIG_SETSKILLDISABLED: &str = "skills.config.setSkillDisabled"; + /// `skills.discover` + pub const SKILLS_DISCOVER: &str = "skills.discover"; + /// `skills.getDiscoveryPaths` + pub const SKILLS_GETDISCOVERYPATHS: &str = "skills.getDiscoveryPaths"; /// `registerExtensionLaunchProvider` pub const REGISTEREXTENSIONLAUNCHPROVIDER: &str = "registerExtensionLaunchProvider"; /// `catalog.search` @@ -104,34 +178,63 @@ pub mod rpc_methods { pub const PLUGINS_MARKETPLACES_BROWSE: &str = "plugins.marketplaces.browse"; /// `plugins.marketplaces.refresh` pub const PLUGINS_MARKETPLACES_REFRESH: &str = "plugins.marketplaces.refresh"; - /// `skills.config.setDisabledSkills` - pub const SKILLS_CONFIG_SETDISABLEDSKILLS: &str = "skills.config.setDisabledSkills"; - /// `skills.config.setSkillDisabled` - pub const SKILLS_CONFIG_SETSKILLDISABLED: &str = "skills.config.setSkillDisabled"; - /// `skills.discover` - pub const SKILLS_DISCOVER: &str = "skills.discover"; - /// `skills.getDiscoveryPaths` - pub const SKILLS_GETDISCOVERYPATHS: &str = "skills.getDiscoveryPaths"; /// `agents.discover` pub const AGENTS_DISCOVER: &str = "agents.discover"; /// `agents.getDiscoveryPaths` pub const AGENTS_GETDISCOVERYPATHS: &str = "agents.getDiscoveryPaths"; + /// `agents.getBuiltins` + pub const AGENTS_GETBUILTINS: &str = "agents.getBuiltins"; + /// `agents.getAvailableBuiltins` + pub const AGENTS_GETAVAILABLEBUILTINS: &str = "agents.getAvailableBuiltins"; + /// `agents.getBuiltinDefinition` + pub const AGENTS_GETBUILTINDEFINITION: &str = "agents.getBuiltinDefinition"; + /// `agents.getBuiltinListingDefinition` + pub const AGENTS_GETBUILTINLISTINGDEFINITION: &str = "agents.getBuiltinListingDefinition"; + /// `agents.customAgentInitialModelDecision` + pub const AGENTS_CUSTOMAGENTINITIALMODELDECISION: &str = + "agents.customAgentInitialModelDecision"; /// `instructions.discover` pub const INSTRUCTIONS_DISCOVER: &str = "instructions.discover"; /// `instructions.getDiscoveryPaths` pub const INSTRUCTIONS_GETDISCOVERYPATHS: &str = "instructions.getDiscoveryPaths"; + /// `globalState.load` + pub const GLOBALSTATE_LOAD: &str = "globalState.load"; + /// `globalState.loadForConfigDir` + pub const GLOBALSTATE_LOADFORCONFIGDIR: &str = "globalState.loadForConfigDir"; + /// `globalState.writeKey` + pub const GLOBALSTATE_WRITEKEY: &str = "globalState.writeKey"; /// `commands.list` pub const COMMANDS_LIST: &str = "commands.list"; - /// `user.settings.reload` - pub const USER_SETTINGS_RELOAD: &str = "user.settings.reload"; /// `user.settings.get` pub const USER_SETTINGS_GET: &str = "user.settings.get"; /// `user.settings.set` pub const USER_SETTINGS_SET: &str = "user.settings.set"; + /// `gitHubRepository.atPath` + pub const GITHUBREPOSITORY_ATPATH: &str = "gitHubRepository.atPath"; + /// `gitHubOwners.nextRequestId` + pub const GITHUBOWNERS_NEXTREQUESTID: &str = "gitHubOwners.nextRequestId"; + /// `gitHubOwners.list` + pub const GITHUBOWNERS_LIST: &str = "gitHubOwners.list"; + /// `gitHubOwners.cancel` + pub const GITHUBOWNERS_CANCEL: &str = "gitHubOwners.cancel"; + /// `git.currentBranchRemote` + pub const GIT_CURRENTBRANCHREMOTE: &str = "git.currentBranchRemote"; + /// `git.workingDirectoryContext` + pub const GIT_WORKINGDIRECTORYCONTEXT: &str = "git.workingDirectoryContext"; + /// `git.reposFromRemotes` + pub const GIT_REPOSFROMREMOTES: &str = "git.reposFromRemotes"; /// `managedSettings.read` pub const MANAGEDSETTINGS_READ: &str = "managedSettings.read"; /// `managedSettings.clearCache` 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` @@ -186,6 +289,12 @@ pub mod rpc_methods { pub const SESSIONS_RELEASELOCK: &str = "sessions.releaseLock"; /// `sessions.enrichMetadata` pub const SESSIONS_ENRICHMETADATA: &str = "sessions.enrichMetadata"; + /// `sessions.createWorkspace` + pub const SESSIONS_CREATEWORKSPACE: &str = "sessions.createWorkspace"; + /// `sessions.loadWorkspace` + pub const SESSIONS_LOADWORKSPACE: &str = "sessions.loadWorkspace"; + /// `sessions.updateWorkspaceFields` + pub const SESSIONS_UPDATEWORKSPACEFIELDS: &str = "sessions.updateWorkspaceFields"; /// `sessions.reloadPluginHooks` pub const SESSIONS_RELOADPLUGINHOOKS: &str = "sessions.reloadPluginHooks"; /// `sessions.loadDeferredRepoHooks` @@ -208,6 +317,25 @@ pub mod rpc_methods { pub const SESSIONS_CONFIGURESESSIONEXTENSIONS: &str = "sessions.configureSessionExtensions"; /// `agentRegistry.spawn` pub const AGENTREGISTRY_SPAWN: &str = "agentRegistry.spawn"; + /// `connectors.getCapabilities` + pub const CONNECTORS_GETCAPABILITIES: &str = "connectors.getCapabilities"; + /// `connectors.getAccounts` + pub const CONNECTORS_GETACCOUNTS: &str = "connectors.getAccounts"; + /// `connectors.list` + pub const CONNECTORS_LIST: &str = "connectors.list"; + /// `connectors.refresh` + pub const CONNECTORS_REFRESH: &str = "connectors.refresh"; + /// `session.providers.getCatalog` + pub const SESSION_PROVIDERS_GETCATALOG: &str = "session.providers.getCatalog"; + /// `session.providers.discover` + pub const SESSION_PROVIDERS_DISCOVER: &str = "session.providers.discover"; + /// `session.providers.getStatus` + pub const SESSION_PROVIDERS_GETSTATUS: &str = "session.providers.getStatus"; + /// `session.providers.models.list` + pub const SESSION_PROVIDERS_MODELS_LIST: &str = "session.providers.models.list"; + /// `session.providers.models.prepareConfiguration` + pub const SESSION_PROVIDERS_MODELS_PREPARECONFIGURATION: &str = + "session.providers.models.prepareConfiguration"; /// `session.suspend` pub const SESSION_SUSPEND: &str = "session.suspend"; /// `session.send` @@ -218,6 +346,8 @@ pub mod rpc_methods { pub const SESSION_SANDBOX_GETENFORCEMENTSTATUS: &str = "session.sandbox.getEnforcementStatus"; /// `session.sandbox.disableForSession` pub const SESSION_SANDBOX_DISABLEFORSESSION: &str = "session.sandbox.disableForSession"; + /// `session.sandbox.grantPathForRequest` + pub const SESSION_SANDBOX_GRANTPATHFORREQUEST: &str = "session.sandbox.grantPathForRequest"; /// `session.sendSystemNotification` pub const SESSION_SENDSYSTEMNOTIFICATION: &str = "session.sendSystemNotification"; /// `session.abort` @@ -249,6 +379,18 @@ pub mod rpc_methods { pub const SESSION_GITHUBAUTH_LOGOUTUSER: &str = "session.gitHubAuth.logoutUser"; /// `session.gitHubAuth.lastAuthErrors` pub const SESSION_GITHUBAUTH_LASTAUTHERRORS: &str = "session.gitHubAuth.lastAuthErrors"; + /// `session.accounts.enumerate` + pub const SESSION_ACCOUNTS_ENUMERATE: &str = "session.accounts.enumerate"; + /// `session.accounts.get` + pub const SESSION_ACCOUNTS_GET: &str = "session.accounts.get"; + /// `session.accounts.set` + pub const SESSION_ACCOUNTS_SET: &str = "session.accounts.set"; + /// `session.accounts.login.begin` + pub const SESSION_ACCOUNTS_LOGIN_BEGIN: &str = "session.accounts.login.begin"; + /// `session.accounts.login.advance` + pub const SESSION_ACCOUNTS_LOGIN_ADVANCE: &str = "session.accounts.login.advance"; + /// `session.accounts.login.cancel` + pub const SESSION_ACCOUNTS_LOGIN_CANCEL: &str = "session.accounts.login.cancel"; /// `session.debug.collectLogs` pub const SESSION_DEBUG_COLLECTLOGS: &str = "session.debug.collectLogs"; /// `session.canvas.list` @@ -265,36 +407,6 @@ pub mod rpc_methods { pub const SESSION_CANVAS_PROVIDER_REGISTER: &str = "session.canvas.provider.register"; /// `session.canvas.provider.unregister` pub const SESSION_CANVAS_PROVIDER_UNREGISTER: &str = "session.canvas.provider.unregister"; - /// `session.factory.run` - pub const SESSION_FACTORY_RUN: &str = "session.factory.run"; - /// `session.factory.resume` - pub const SESSION_FACTORY_RESUME: &str = "session.factory.resume"; - /// `session.factory.runFromTool` - pub const SESSION_FACTORY_RUNFROMTOOL: &str = "session.factory.runFromTool"; - /// `session.factory.resumeFromTool` - pub const SESSION_FACTORY_RESUMEFROMTOOL: &str = "session.factory.resumeFromTool"; - /// `session.factory.getRun` - pub const SESSION_FACTORY_GETRUN: &str = "session.factory.getRun"; - /// `session.factory.listRuns` - pub const SESSION_FACTORY_LISTRUNS: &str = "session.factory.listRuns"; - /// `session.factory.getRunDetail` - pub const SESSION_FACTORY_GETRUNDETAIL: &str = "session.factory.getRunDetail"; - /// `session.factory.getRunProgress` - pub const SESSION_FACTORY_GETRUNPROGRESS: &str = "session.factory.getRunProgress"; - /// `session.factory.cancel` - pub const SESSION_FACTORY_CANCEL: &str = "session.factory.cancel"; - /// `session.factory.pause` - pub const SESSION_FACTORY_PAUSE: &str = "session.factory.pause"; - /// `session.factory.pauseAtCheckpoint` - pub const SESSION_FACTORY_PAUSEATCHECKPOINT: &str = "session.factory.pauseAtCheckpoint"; - /// `session.factory.log` - pub const SESSION_FACTORY_LOG: &str = "session.factory.log"; - /// `session.factory.agent` - pub const SESSION_FACTORY_AGENT: &str = "session.factory.agent"; - /// `session.factory.journal.get` - pub const SESSION_FACTORY_JOURNAL_GET: &str = "session.factory.journal.get"; - /// `session.factory.journal.put` - pub const SESSION_FACTORY_JOURNAL_PUT: &str = "session.factory.journal.put"; /// `session.workflow.run` pub const SESSION_WORKFLOW_RUN: &str = "session.workflow.run"; /// `session.workflow.resume` @@ -470,8 +582,12 @@ pub mod rpc_methods { pub const SESSION_SKILLS_RELOAD: &str = "session.skills.reload"; /// `session.skills.ensureLoaded` pub const SESSION_SKILLS_ENSURELOADED: &str = "session.skills.ensureLoaded"; + /// `session.mcp.setConnectedIdeInfo` + pub const SESSION_MCP_SETCONNECTEDIDEINFO: &str = "session.mcp.setConnectedIdeInfo"; /// `session.mcp.list` pub const SESSION_MCP_LIST: &str = "session.mcp.list"; + /// `session.mcp.listConfigured` + pub const SESSION_MCP_LISTCONFIGURED: &str = "session.mcp.listConfigured"; /// `session.mcp.listTools` pub const SESSION_MCP_LISTTOOLS: &str = "session.mcp.listTools"; /// `session.mcp.enable` @@ -512,10 +628,16 @@ pub mod rpc_methods { /// `session.mcp.oauth.authenticationStateChanged` pub const SESSION_MCP_OAUTH_AUTHENTICATIONSTATECHANGED: &str = "session.mcp.oauth.authenticationStateChanged"; + /// `session.mcp.oauth.prepareLogin` + 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` + pub const SESSION_MCP_OAUTH_CANCELLOGIN: &str = "session.mcp.oauth.cancelLogin"; /// `session.mcp.oauth.respond` pub const SESSION_MCP_OAUTH_RESPOND: &str = "session.mcp.oauth.respond"; /// `session.mcp.headers.handlePendingHeadersRefreshRequest` @@ -539,12 +661,18 @@ pub mod rpc_methods { pub const SESSION_MCP_RESOURCES_LIST: &str = "session.mcp.resources.list"; /// `session.mcp.resources.listTemplates` pub const SESSION_MCP_RESOURCES_LISTTEMPLATES: &str = "session.mcp.resources.listTemplates"; + /// `session.mcp.prompts.list` + pub const SESSION_MCP_PROMPTS_LIST: &str = "session.mcp.prompts.list"; + /// `session.mcp.prompts.get` + pub const SESSION_MCP_PROMPTS_GET: &str = "session.mcp.prompts.get"; /// `session.diagnostics.configure` pub const SESSION_DIAGNOSTICS_CONFIGURE: &str = "session.diagnostics.configure"; /// `session.diagnostics.read` pub const SESSION_DIAGNOSTICS_READ: &str = "session.diagnostics.read"; /// `session.connectors.getCapabilities` pub const SESSION_CONNECTORS_GETCAPABILITIES: &str = "session.connectors.getCapabilities"; + /// `session.connectors.getAccount` + pub const SESSION_CONNECTORS_GETACCOUNT: &str = "session.connectors.getAccount"; /// `session.connectors.getStatus` pub const SESSION_CONNECTORS_GETSTATUS: &str = "session.connectors.getStatus"; /// `session.connectors.list` @@ -598,6 +726,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` @@ -655,8 +785,12 @@ pub mod rpc_methods { pub const SESSION_UI_ELICITATION: &str = "session.ui.elicitation"; /// `session.ui.handlePendingElicitation` pub const SESSION_UI_HANDLEPENDINGELICITATION: &str = "session.ui.handlePendingElicitation"; + /// `session.ui.handleHumanAskUser` + pub const SESSION_UI_HANDLEHUMANASKUSER: &str = "session.ui.handleHumanAskUser"; /// `session.ui.handlePendingUserInput` pub const SESSION_UI_HANDLEPENDINGUSERINPUT: &str = "session.ui.handlePendingUserInput"; + /// `session.ui.handleHumanUserInput` + pub const SESSION_UI_HANDLEHUMANUSERINPUT: &str = "session.ui.handleHumanUserInput"; /// `session.ui.handlePendingSampling` pub const SESSION_UI_HANDLEPENDINGSAMPLING: &str = "session.ui.handlePendingSampling"; /// `session.ui.handlePendingAutoModeSwitch` @@ -667,6 +801,8 @@ pub mod rpc_methods { "session.ui.handlePendingSessionLimitsExhausted"; /// `session.ui.handlePendingExitPlanMode` pub const SESSION_UI_HANDLEPENDINGEXITPLANMODE: &str = "session.ui.handlePendingExitPlanMode"; + /// `session.ui.handleHumanExitPlanMode` + pub const SESSION_UI_HANDLEHUMANEXITPLANMODE: &str = "session.ui.handleHumanExitPlanMode"; /// `session.ui.registerDirectAutoModeSwitchHandler` pub const SESSION_UI_REGISTERDIRECTAUTOMODESWITCHHANDLER: &str = "session.ui.registerDirectAutoModeSwitchHandler"; @@ -871,10 +1007,6 @@ pub mod rpc_methods { pub const SKILLPROVIDER_READ: &str = "skillProvider.read"; /// `providerToken.getToken` pub const PROVIDERTOKEN_GETTOKEN: &str = "providerToken.getToken"; - /// `factory.execute` - pub const FACTORY_EXECUTE: &str = "factory.execute"; - /// `factory.abort` - pub const FACTORY_ABORT: &str = "factory.abort"; /// `workflow.execute` pub const WORKFLOW_EXECUTE: &str = "workflow.execute"; /// `workflow.abort` @@ -883,8 +1015,12 @@ pub mod rpc_methods { pub const TASKS_CANCEL: &str = "tasks.cancel"; /// `sessionFs.readFile` pub const SESSIONFS_READFILE: &str = "sessionFs.readFile"; + /// `sessionFs.readFileBytes` + pub const SESSIONFS_READFILEBYTES: &str = "sessionFs.readFileBytes"; /// `sessionFs.writeFile` pub const SESSIONFS_WRITEFILE: &str = "sessionFs.writeFile"; + /// `sessionFs.writeFileBytes` + pub const SESSIONFS_WRITEFILEBYTES: &str = "sessionFs.writeFileBytes"; /// `sessionFs.appendFile` pub const SESSIONFS_APPENDFILE: &str = "sessionFs.appendFile"; /// `sessionFs.exists` @@ -1521,6 +1657,25 @@ pub struct UserAuthInfo { pub r#type: UserAuthInfoType, } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountAuthInfo { + /// Host coordinate owned by the account's model provider. + pub host: String, + /// Login identifying the provider-owned account. + pub login: String, + /// Provider-owned account authentication. + pub r#type: AccountAuthInfoType, +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// ///
    @@ -1740,6 +1895,147 @@ pub struct AccountLogoutResult { pub has_more_users: bool, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthEnumerateQueryAccounts { + /// Account-collection query variant discriminator. + pub kind: AuthEnumerateQueryAccountsKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthEnumerateQueryProviders { + /// Whether an interactive Entra broker is available on the host; gates Entra availability in the returned list. + #[serde(skip_serializing_if = "Option::is_none")] + pub broker_available: Option, + /// Account-collection query variant discriminator. + pub kind: AuthEnumerateQueryProvidersKind, +} + +/// Enumerate request carrying the typed collection query. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsEnumerateRequest { + /// Which typed accounts collection to enumerate. + pub query: AuthEnumerateQuery, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthReadQueryActiveAccount { + /// Account read-datum query variant discriminator. + pub kind: AuthReadQueryActiveAccountKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthReadQueryStatus { + /// Account read-datum query variant discriminator. + pub kind: AuthReadQueryStatusKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthReadQueryLastErrors { + /// Account read-datum query variant discriminator. + pub kind: AuthReadQueryLastErrorsKind, +} + +/// Read request carrying the typed datum query. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsGetRequest { + /// Which typed accounts datum to read. + pub query: AuthReadQuery, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthWriteSwitchActive { + /// Account mutation command variant discriminator. + pub kind: AuthWriteSwitchActiveKind, + /// Opaque selection id of the account to make active. + pub selection_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthWriteLogout { + /// Account mutation command variant discriminator. + pub kind: AuthWriteLogoutKind, + /// Opaque selection id of the account to log out; absent logs out the active account. + #[serde(skip_serializing_if = "Option::is_none")] + pub selection_id: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthWriteSetCredentials { + /// Authentication host URL. + pub host: String, + /// Account mutation command variant discriminator. + pub kind: AuthWriteSetCredentialsKind, + /// Login/username for the credential. + pub login: String, + /// GitHub authentication token to install. + pub token: String, +} + +/// Mutation request carrying the typed write command. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsSetRequest { + /// The non-interactive mutation command to apply. + pub command: AuthWrite, +} + +/// One signed-in account in the roster forest. +/// +///
    +/// +/// **Experimental.** This type 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 AccountStatus { + /// Whether this is the active account. + pub active: bool, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option, + /// Authentication host URL. + pub host: String, + /// The provider kind of this account. + pub kind: AccountKind, + /// Authenticated login/username. + pub login: String, + /// Opaque selection id used to switch to, or log out, this account. + pub selection_id: String, +} + /// Canonical directory where custom agents can be discovered or created, with scope, preference, and optional project path. /// ///
    @@ -2107,6 +2403,45 @@ pub struct AgentReloadResult { pub agents: Vec, } +/// The models a custom agent asks for, and the models actually available. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsCustomAgentInitialModelDecisionParams { + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + pub agent_models_json: String, + /// The models available to this session, serialized in the shape the model list carries. + pub available_models_json: String, +} + +/// The model to switch to, and the warning to show when the agent's preference could not be met. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsCustomAgentInitialModelDecisionResult { + /// The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + /// The first available model that matches the agent's preferences. Absent when none of the requested models is available. + #[serde(skip_serializing_if = "Option::is_none")] + pub target_model: Option, + /// What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option, +} + /// Optional project paths to include in agent discovery. /// ///
    @@ -2173,6 +2508,140 @@ pub struct AgentSetPromptRequest { pub prompt: String, } +/// The feature flags to evaluate shipped agents against. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetAvailableBuiltinsRequest { + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + #[serde(skip_serializing_if = "Option::is_none")] + pub overrides: Option>, +} + +/// A shipped agent, named and described. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct BuiltinAgentSummary { + /// One-line description of what the agent does. + pub description: String, + /// The agent name, as it appears in `getBuiltins`. + pub name: String, +} + +/// The shipped agents available under the requested flags. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetAvailableBuiltinsResult { + /// Available shipped agents, in the runtime's own order. + #[doc(hidden)] + pub(crate) agents: Vec, +} + +/// The shipped agent whose definition to load. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinDefinitionRequest { + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + pub name: String, +} + +/// One shipped agent's definition. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinDefinitionResult { + /// The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. + pub definition_json: String, +} + +/// The shipped agent whose listing entry to load. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinListingDefinitionRequest { + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. + pub name: String, +} + +/// One shipped agent, projected for a listing. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinListingDefinitionResult { + /// The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. + pub definition_json: String, +} + +/// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinsResult { + /// The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. + pub disableable_names: Vec, + /// Every agent name this runtime ships. + pub names: Vec, + /// The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. + pub yaml_based_names: Vec, +} + /// Optional project paths to include when enumerating agent discovery directories. /// ///
    @@ -2693,6 +3162,43 @@ pub struct AttachmentSelection { pub r#type: AttachmentSelectionType, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthEnumerateValueAccounts { + /// The signed-in account forest; empty when not logged in. + pub items: Vec, + /// Enumerated account-collection variant discriminator. + pub kind: AuthEnumerateValueAccountsKind, +} + +/// A provider offered for interactive 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 ProviderDescriptor { + /// Whether this provider is currently available to sign in with. + pub available: bool, + /// The neutral provider kind. + pub kind: LoginProviderKind, + /// Human-readable menu label, owned by the runtime so every consumer renders identical text. + pub label: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthEnumerateValueProviders { + /// The providers offered for interactive login. + pub items: Vec, + /// Enumerated account-collection variant discriminator. + pub kind: AuthEnumerateValueProvidersKind, +} + /// Credential-free authentication identity safe to expose to hosts and user interfaces. /// ///
    @@ -2722,6 +3228,289 @@ 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. +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginAdvanceRequest { + /// Opaque flow id from begin. + pub flow_id: String, + /// Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, +} + +/// Begin an interactive login flow for a provider kind. Dispatch is kind-only. +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginBeginRequest { + /// The provider kind to sign in with. + pub kind: LoginProviderKind, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginStepOpenUrl { + /// Login flow step variant discriminator. + pub kind: AuthLoginStepOpenUrlKind, + /// Authorize URL the consumer should open in a browser (consumer-driven browser-open). + pub url: String, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginStepInputRequired { + /// Login flow step variant discriminator. + pub kind: AuthLoginStepInputRequiredKind, + /// Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. a GitHub Enterprise Cloud host, *.ghe.com). + pub prompt: String, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginStepAwaiting { + /// Login flow step variant discriminator. + pub kind: AuthLoginStepAwaitingKind, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginStepNeedsInteraction { + /// Login flow step variant discriminator. + pub kind: AuthLoginStepNeedsInteractionKind, +} + +/// Result of an interactive login flow. Pending consent or account selection is not terminal. +/// +///
    +/// +/// **Experimental.** This type 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 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, + /// Current disposition of the login, including pending user decisions. + pub status: AuthLoginResultStatus, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginStepCompleted { + /// Login flow step variant discriminator. + pub kind: AuthLoginStepCompletedKind, + /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. + pub result: AuthLoginResultDto, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginStepError { + /// Login flow step variant discriminator. + pub kind: AuthLoginStepErrorKind, + /// Human-readable failure message. + pub message: String, +} + +/// A started login flow: its opaque id and first step. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthLoginBegun { + /// Opaque flow id used to advance or cancel this login. + pub flow_id: String, + /// The first step of the flow. + pub step: AuthLoginStep, +} + +/// Cancel an in-flight login flow. +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginCancelRequest { + /// Opaque flow id from begin. + pub flow_id: String, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthReadValueActiveAccount { + /// The active account, or absent when not logged in. + #[serde(skip_serializing_if = "Option::is_none")] + pub account: Option, + /// Credential-free identity metadata for the active account, including resolved Copilot user information when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_info: Option, + /// Account read-datum variant discriminator. + pub kind: AuthReadValueActiveAccountKind, +} + +/// Neutral authentication status summary. +/// +///
    +/// +/// **Experimental.** This type 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 AuthStatusDto { + /// Number of signed-in accounts in the roster. + pub account_count: i64, + /// Active account host, if authenticated. + #[serde(skip_serializing_if = "Option::is_none")] + pub active_host: Option, + /// Active account login, if authenticated. + #[serde(skip_serializing_if = "Option::is_none")] + pub active_login: Option, + /// Copilot plan tier of the active account, if known. + #[serde(skip_serializing_if = "Option::is_none")] + pub copilot_plan: Option, + /// Whether the session has resolved authentication. + pub is_authenticated: bool, +} + +/// +///
    +/// +/// **Experimental.** This type 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 AuthReadValueStatus { + /// Account read-datum variant discriminator. + pub kind: AuthReadValueStatusKind, + /// The neutral authentication status summary. + pub status: AuthStatusDto, +} + /// Validation error from an authentication attempt. /// ///
    @@ -2740,6 +3529,40 @@ pub struct AuthValidationError { pub message: String, } +/// +///
    +/// +/// **Experimental.** This type 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 AuthReadValueLastErrors { + /// Validation errors from the most recent authentication attempt. + pub errors: Vec, + /// Account read-datum variant discriminator. + pub kind: AuthReadValueLastErrorsKind, +} + +/// Result of a non-interactive accounts mutation. +/// +///
    +/// +/// **Experimental.** This type 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 AuthWriteResult { + /// For a logout, whether other signed-in accounts remain. + #[serde(skip_serializing_if = "Option::is_none")] + pub more_users: Option, + /// Whether the mutation was applied. + pub ok: bool, +} + /// Current per-window credit limit and consumption for an autopilot objective. /// ///
    @@ -3544,7 +4367,7 @@ pub struct CatalogCandidateSourceEmbedded { pub kind: CatalogCandidateSourceEmbeddedKind, } -/// An inert AI skill catalog result. AI skills are discovery-only and cannot be represented as installable through this surface. +/// An inert AI skill catalog result. Verified Skill candidates may be installable only when the runtime reports installability and the selected session is permitted to plan installation. /// ///
    /// @@ -3564,8 +4387,7 @@ pub struct CatalogAiSkillCandidate { pub handle: String, /// ISO 8601 timestamp after which the handle is stale and will be rejected. pub handle_expires_at: String, - /// AI skills are discovery-only and cannot be installed through this surface - #[serde(deserialize_with = "CatalogAiSkillCandidate::deserialize_installability")] + /// Whether this AI skill candidate can be planned for verified installation in the selected session. pub installability: CatalogAiSkillInstallability, /// Discriminator: this candidate describes an AI skill #[serde(deserialize_with = "CatalogAiSkillCandidate::deserialize_kind")] @@ -3586,24 +4408,6 @@ pub struct CatalogAiSkillCandidate { } impl CatalogAiSkillCandidate { - fn deserialize_installability<'de, D>( - deserializer: D, - ) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value != "not-installable-kind" { - return Err(serde::de::Error::unknown_variant( - &value, - &["not-installable-kind"], - )); - } - ::deserialize( - serde::de::value::StringDeserializer::::new(value), - ) - } - fn deserialize_kind<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -3931,7 +4735,7 @@ pub struct CatalogPolicyRejectedError { pub source: McpPlanPolicySource, } -/// An explicit numbered-page request. The SDK treats the token as opaque; only the runtime decodes it and changes its targetPage. Authority validation binds navigation to the original search. No snapshot stability or token TTL is promised. +/// An explicit numbered-page request. SDK consumers treat the token as opaque. For bound search, the runtime unwraps an expiring owner-bound reference to the private authority token; only the runtime changes the authority token's targetPage. Legacy unbound navigation keeps its authority-issued token semantics. No snapshot stability is promised. /// ///
    /// @@ -3944,7 +4748,7 @@ pub struct CatalogPolicyRejectedError { pub struct CatalogSearchPage { /// Requested one-based page. Must not exceed either the token's signed pageCount or the navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly available pages beyond that signed pageCount. pub number: i32, - /// Opaque authority-issued pagination token from an earlier response. Never decode, modify or log it in an SDK consumer. + /// Opaque pagination token from an earlier response, owner-bound when session-bound search was requested. Never decode, modify or log it in an SDK consumer. Expired or foreign bound references require a fresh bound search, not a legacy retry. pub token: String, } @@ -3969,7 +4773,7 @@ pub struct CatalogSearchPagination { pub page_count: i64, /// Page size bound to the search, equal to the effective request limit. pub page_size: i32, - /// Opaque authority-issued pagination token. Only the runtime decodes it or changes targetPage; SDK consumers must not decode, modify or log it. It has no runtime-created expiry or cache. + /// Opaque pagination token. Session-bound search returns an expiring runtime-owned reference retaining the exact private authority token, original search and authority. Legacy unbound search returns the authority token unchanged, without a runtime-created expiry. Only the runtime unwraps tokens or changes targetPage; SDK consumers must not decode, modify or log them. pub token: String, /// Backend-reported count for this response, not the number of returned candidates. Its relationship to the full query result set is unknown. pub total_count: i64, @@ -4003,6 +4807,67 @@ pub struct CatalogSearchRequest { pub query: String, } +/// Extensible [`CatalogSearchRequest`], including inputs added after it was published. +/// +/// Required inputs are [`CatalogSearchOptions::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 CatalogSearchOptions { + #[serde(flatten)] + legacy: CatalogSearchRequest, + #[serde(skip_serializing_if = "Option::is_none")] + policy_session_id: Option, +} + +impl CatalogSearchOptions { + /// Creates options with the required inputs. + pub fn new(contract: CatalogClientContract, query: impl Into) -> Self { + Self { + legacy: CatalogSearchRequest { + contract, + query: query.into(), + limit: None, + kinds: None, + page: None, + }, + policy_session_id: None, + } + } + + /// Maximum number of candidates to return. Defaults to 10 when omitted. + pub fn limit(mut self, value: i32) -> Self { + self.legacy.limit = Some(value); + self + } + + /// Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. + pub fn kinds(mut self, value: Vec) -> Self { + self.legacy.kinds = Some(value); + self + } + + /// Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh first-page search. + pub fn page(mut self, value: CatalogSearchPage) -> Self { + self.legacy.page = Some(value); + self + } + + /// Select an existing attached local session. Requires authenticated, session-bound search. + /// The runtime never creates, resumes or reconfigures a session to honour this selector. + pub fn policy_session_id(mut self, value: impl Into) -> Self { + self.policy_session_id = Some(value.into()); + self + } +} + /// A completed catalog search containing inert candidate summaries. MCP server and AI skill variants carry a single-use handle; the Agent Plugin variant is handleless. /// ///
    @@ -5083,6 +5948,12 @@ pub struct ConnectorCapabilities { pub max_poll_interval_ms: i64, /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. pub opaque_account_selection: bool, + /// Whether getAccount is supported. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_account_selection: Option, + /// Whether reconcile accepts forceConnectorName. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub targeted_reconcile: Option, } /// Credential-free Connector catalog entry. @@ -5101,12 +5972,21 @@ pub struct ConnectorCatalogEntry { pub description: Option, /// Untrusted display label from the service. pub display_name: String, + /// Optional catalog logo. + #[serde(skip_serializing_if = "Option::is_none")] + pub logo: Option, /// Canonical Connector name used by lifecycle methods. pub name: String, + /// Optional catalog release tag. + #[serde(skip_serializing_if = "Option::is_none")] + pub release_tag: Option, /// Opaque stable runtime IDs currently projected into the session for this Connector. pub runtime_server_ids: Vec, /// Current authoritative service connection state. pub status: ConnectorCatalogStatus, + /// Optional catalog tier. + #[serde(skip_serializing_if = "Option::is_none")] + pub tier: Option, } /// Validated Connector catalog snapshot cached by the session. @@ -5285,6 +6165,147 @@ pub struct ConnectorDisconnectResult { pub status: ConnectorStatus, } +/// Account metadata. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAuthInfo { + /// Host. + pub host: String, + /// Login. + pub login: String, + /// Authentication type. + pub r#type: AuthInfoType, +} + +/// Eligible account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccount { + /// Opaque account ID. + pub account_id: String, + /// Account metadata. + pub auth_info: ConnectorDiscoveryAuthInfo, +} + +/// Eligible accounts. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccountList { + /// Eligible accounts. + pub accounts: Vec, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, +} + +/// Selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccountRequest { + /// Opaque account ID. + pub account_id: String, +} + +/// Feature availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCapabilities { + /// API version. + pub api_version: i64, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, + /// Whether results are cached. + pub conditional_cache: bool, + /// Whether accounts are selected by opaque ID. + pub opaque_account_selection: bool, +} + +/// Entry. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCatalogEntry { + /// Description. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Display name. + pub display_name: String, + /// Logo. + #[serde(skip_serializing_if = "Option::is_none")] + pub logo: Option, + /// Name. + pub name: String, + /// Release tag. + #[serde(skip_serializing_if = "Option::is_none")] + pub release_tag: Option, + /// Status. + pub status: ConnectorCatalogStatus, + /// Tier. + #[serde(skip_serializing_if = "Option::is_none")] + pub tier: Option, +} + +/// Entries for the selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCatalogResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + /// Requests authoritative Connector-to-MCP reconciliation for the pinned account. /// ///
    @@ -5303,6 +6324,68 @@ pub struct ConnectorReconcileRequest { pub refresh_catalog: Option, } +/// Extensible [`ConnectorReconcileRequest`], including inputs added after it was published. +/// +/// Required inputs are [`ConnectorReconcileOptions::new`] arguments; optional inputs have fluent setters. +/// Input-only: it serialises to the flat wire request and is not deserialisable. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorReconcileOptions { + #[serde(flatten)] + legacy: ConnectorReconcileRequest, + #[serde(skip_serializing_if = "Option::is_none")] + force_connector_name: Option, +} + +impl ConnectorReconcileOptions { + /// Creates options with the required inputs. + pub fn new(account_id: impl Into) -> Self { + Self { + legacy: ConnectorReconcileRequest { + account_id: account_id.into(), + refresh_catalog: None, + }, + force_connector_name: None, + } + } + + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + pub fn refresh_catalog(mut self, value: bool) -> Self { + self.legacy.refresh_catalog = Some(value); + self + } + + /// Optional Connector name to reinitialize. Requires the targetedReconcile capability. + pub fn force_connector_name(mut self, value: impl Into) -> Self { + self.force_connector_name = Some(value.into()); + self + } +} + +/// Session account selection. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorSessionAccount { + /// Opaque session-scoped account selection ID. + pub account_id: String, + /// Credential-free identity metadata. + pub auth_info: AuthIdentityMetadata, +} + /// Remote session connection parameters. /// ///
    @@ -5462,6 +6545,9 @@ pub struct CurrentModel { /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. #[serde(skip_serializing_if = "Option::is_none")] pub pending_auto_tier: Option, + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_model_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -5499,6 +6585,45 @@ pub struct CurrentToolMetadata { pub namespaced_name: Option, } +/// Result of one customization reload component. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomizationReloadOutcome { + /// Reason for a skipped component or description of a failure, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// Whether the component reloaded, was skipped, or failed + pub status: CustomizationReloadStatus, + /// Component whose reload was attempted or skipped + pub subsystem: CustomizationReloadSubsystem, +} + +/// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomizationsReloadResult { + /// Errors from any component that could not be refreshed + pub errors: Vec, + /// Outcome of each component in reload order; a skipped component was not configured or loaded + pub outcomes: Vec, + /// Warnings from skill discovery + pub warnings: Vec, +} + /// A file included in the session debug bundle. /// ///
    @@ -6064,6 +7189,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. /// ///
    @@ -6082,6 +7450,160 @@ pub struct EnqueueCommandParams { pub display_text: Option, } +/// Hosting capabilities and session capacity advertised by an 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 EnvironmentCapabilities { + /// Advertised Agent Host Protocol version. + #[serde(skip_serializing_if = "Option::is_none")] + 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 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 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 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 EnvironmentsListResult { + /// Safe discovery records without relay bootstrap credentials. + pub environments: Vec, +} + /// Cursor, batch size, and optional long-poll/filter parameters for reading session events. /// ///
    @@ -6592,7 +8114,7 @@ pub struct ExternalToolTextResultForLlmContentText { pub r#type: ExternalToolTextResultForLlmContentTextType, } -/// Parameters for cooperatively aborting a factory body. +/// Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. /// ///
    /// @@ -6602,16 +8124,23 @@ pub struct ExternalToolTextResultForLlmContentText { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryAbortRequest { - /// Target session identifier - pub session_id: SessionId, - /// Factory run identifier. - pub run_id: String, - /// Opaque token identifying the execution attempt to abort. - pub execution_token: String, +pub struct FleetStartRequest { + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request + #[serde(skip_serializing_if = "Option::is_none")] + pub attachments: Option>, + /// If false, this request will not trigger a Premium Request Unit charge. User requests default to billable. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) billable: Option, + /// Optional user prompt to combine with fleet instructions + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt: Option, + /// If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. + #[serde(skip_serializing_if = "Option::is_none")] + pub wait: Option, } -/// Acknowledgement that a factory request was accepted. +/// Indicates whether fleet mode was successfully activated. /// ///
    /// @@ -6621,9 +8150,12 @@ pub struct FactoryAbortRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryAckResult {} +pub struct FleetStartResult { + /// Whether fleet mode was successfully activated + pub started: bool, +} -/// Options for one factory-scoped subagent call. +/// Folder path to add to trusted folders. /// ///
    /// @@ -6633,28 +8165,12 @@ pub struct FactoryAckResult {} ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryAgentOptions { - /// Optional built-in or custom agent name whose definition configures the subagent. - #[serde(skip_serializing_if = "Option::is_none")] - pub agent: Option, - /// Optional context tier override for the subagent. - #[serde(skip_serializing_if = "Option::is_none")] - pub context_tier: Option, - /// Optional label distinguishing otherwise identical memoized agent calls. - #[serde(skip_serializing_if = "Option::is_none")] - pub label: Option, - /// Optional model identifier for the subagent. - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, - /// Optional reasoning effort override for the subagent. - #[serde(skip_serializing_if = "Option::is_none")] - pub reasoning_effort: Option, - /// Optional JSON Schema for structured agent output. - #[serde(skip_serializing_if = "Option::is_none")] - pub schema: Option, +pub struct FolderTrustAddParams { + /// Folder path to mark as trusted + pub path: String, } -/// Parameters for one factory-scoped subagent call. +/// Folder path to check for trust. /// ///
    /// @@ -6664,18 +8180,12 @@ pub struct FactoryAgentOptions { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryAgentRequest { - /// Opaque token identifying the current factory execution attempt. - pub execution_token: String, - /// Factory run identifier that owns the subagent. - pub factory_run_id: String, - /// Subagent execution options. - pub opts: FactoryAgentOptions, - /// Prompt to send to the subagent. - pub prompt: String, +pub struct FolderTrustCheckParams { + /// Folder path to check + pub path: String, } -/// Result of one factory-scoped subagent call. +/// Folder trust check result. /// ///
    /// @@ -6685,13 +8195,12 @@ pub struct FactoryAgentRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryAgentResult { - /// Agent result, omitted when the agent produced no result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, +pub struct FolderTrustCheckResult { + /// Whether the folder is trusted + pub trusted: bool, } -/// Prompt-safe durable identity and live status for a direct factory agent. +/// The remote the checked-out branch tracks. /// ///
    /// @@ -6701,44 +8210,12 @@ pub struct FactoryAgentResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryAgentSummary { - /// Accumulated active agent time in milliseconds. - pub active_ms: i64, - /// Prompt-safe live activity text. - #[serde(skip_serializing_if = "Option::is_none")] - pub activity: Option, - /// Stable direct-agent identifier. - pub agent_id: String, - /// Registered agent type. - pub agent_type: String, - /// Epoch milliseconds when the agent completed. - #[serde(skip_serializing_if = "Option::is_none")] - pub completed_at: Option, - /// Friendly, non-unique name intended for display - #[serde(skip_serializing_if = "Option::is_none")] - pub display_name: Option, - /// Friendly, non-unique name intended for display - pub label: String, - /// Phase identifier active when the agent was launched, or null. - pub phase_id: Option, - /// Model requested when the agent was launched. - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_model: Option, - /// Concrete model resolved for the agent. - #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_model: Option, - /// Owning factory run identifier. - pub run_id: String, - /// Epoch milliseconds when the agent started. - #[serde(skip_serializing_if = "Option::is_none")] - pub started_at: Option, - /// Current durable or live agent status. - pub status: String, - /// Tool-call identifier that launched the agent. - pub tool_call_id: String, +pub(crate) struct GitCurrentBranchRemoteResult { + /// Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. + pub remote: String, } -/// Parameters for cancelling a factory run. +/// Working-tree path a git query applies to. /// ///
    /// @@ -6748,12 +8225,12 @@ pub struct FactoryAgentSummary { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryCancelRequest { - /// Factory run identifier. - pub run_id: String, +pub(crate) struct GitCwdRequest { + /// Absolute path to a directory inside the git working tree to query. + pub cwd: String, } -/// Current factory phase identity. +/// A GitHub login the authenticated user may act as: their own account, or an organization they belong to. /// ///
    /// @@ -6763,14 +8240,14 @@ pub struct FactoryCancelRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryCurrentPhase { - /// Current phase identifier. - pub id: String, - /// Zero-based declared phase ordinal, or null for an undeclared phase. - pub ordinal: Option, +pub(crate) struct GitHubOwnerOption { + /// The owner's GitHub login. + pub login: String, + /// Which kind of owner this is. The authenticated user's own account is always reported as `user`. + pub r#type: String, } -/// Declared or approved factory resource ceilings. +/// The owner listing to abandon. /// ///
    /// @@ -6780,22 +8257,12 @@ pub struct FactoryCurrentPhase { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryDeclaredLimits { - /// Maximum AI credits consumed by subagents and descendants. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_ai_credits: Option, - /// Maximum concurrently active subagents. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_concurrent_subagents: Option, - /// Maximum total subagents spawned by the run. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_total_subagents: Option, - /// Maximum accumulated active execution time in seconds. - #[serde(skip_serializing_if = "Option::is_none")] - pub timeout_seconds: Option, +pub(crate) struct GitHubOwnersCancelRequest { + /// Request id the listing was started with. + pub request_id: i64, } -/// Parameters sent to the owning extension to execute a factory closure. +/// Whether the id named a running owner listing. /// ///
    /// @@ -6805,20 +8272,12 @@ pub struct FactoryDeclaredLimits { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryExecuteRequest { - /// Target session identifier - pub session_id: SessionId, - /// Registered factory name. - pub name: String, - /// Factory run identifier. - pub run_id: String, - /// Opaque token identifying this factory execution attempt. - pub execution_token: String, - /// Factory input value. - pub args: serde_json::Value, +pub(crate) struct GitHubOwnersCancelResult { + /// True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. + pub canceled: bool, } -/// Result returned by an extension factory closure. +/// Credential to list owners under, and the request id that makes the listing cancellable. /// ///
    /// @@ -6828,13 +8287,14 @@ pub struct FactoryExecuteRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryExecuteResult { - /// Factory result value. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, +pub(crate) struct GitHubOwnersListRequest { + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + pub auth_info: serde_json::Value, + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + pub request_id: i64, } -/// Parameters for paging factory progress. +/// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. /// ///
    /// @@ -6844,24 +8304,23 @@ pub struct FactoryExecuteResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryGetRunProgressRequest { - /// Exclusive forward cursor. +pub(crate) struct GitHubOwnersListResult { + /// Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. #[serde(skip_serializing_if = "Option::is_none")] - pub after_seq: Option, - /// Exclusive backward cursor. + pub message: Option, + /// The owners, on success: the authenticated user first, then the organizations they belong to. + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub before_seq: Option, - /// Maximum records to return. Defaults to 200 and is capped at 500. + pub(crate) owners: Option>, + /// A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - /// Optional phase identifier used to scope records and cursors. + pub throw_error: Option, + /// A line the caller should log. Present only alongside `message`, and only for failures worth recording. #[serde(skip_serializing_if = "Option::is_none")] - pub phase_id: Option, - /// Factory run identifier. - pub run_id: String, + pub warning: Option, } -/// Parameters for retrieving a factory run. +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. /// ///
    /// @@ -6871,12 +8330,12 @@ pub struct FactoryGetRunProgressRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryGetRunRequest { - /// Factory run identifier. - pub run_id: String, +pub(crate) struct GitHubOwnersRequestIdResult { + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + pub request_id: i64, } -/// Parameters for reading a factory journal entry. +/// Working-tree path whose owning GitHub repository should be resolved. /// ///
    /// @@ -6886,16 +8345,12 @@ pub struct FactoryGetRunRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryJournalGetRequest { - /// Opaque token identifying the current factory execution attempt. - pub execution_token: String, - /// Namespaced journal key. - pub key: String, - /// Factory run identifier. - pub run_id: String, +pub(crate) struct GitHubRepositoryAtPathRequest { + /// Absolute path to a directory inside the git working tree to resolve. + pub path: String, } -/// Result of reading a factory journal entry. +/// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. /// ///
    /// @@ -6905,15 +8360,16 @@ pub struct FactoryJournalGetRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryJournalGetResult { - /// Whether the journal contained the requested key. - pub hit: bool, - /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. - #[serde(skip_serializing_if = "Option::is_none")] - pub result_json: Option, +pub struct GitHubRepositoryIdentity { + /// Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + pub host: String, + /// Repository name, without the owner prefix or the `.git` suffix. + pub name: String, + /// Repository owner login (user or organization). + pub owner: String, } -/// Parameters for storing a factory journal entry. +/// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. /// ///
    /// @@ -6923,18 +8379,13 @@ pub struct FactoryJournalGetResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryJournalPutRequest { - /// Opaque token identifying the current factory execution attempt. - pub execution_token: String, - /// Namespaced journal key. - pub key: String, - /// JSON result to memoize. - pub result_json: serde_json::Value, - /// Factory run identifier. - pub run_id: String, +pub(crate) struct GitHubRepositoryAtPathResult { + /// Resolved repository identity, or null when the selected remote resolves to no GitHub host. + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, } -/// Parameters for paging factory runs. +/// Client environment metadata describing the process that produced a telemetry event. /// ///
    /// @@ -6944,19 +8395,46 @@ pub struct FactoryJournalPutRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryListRunsRequest { - /// Exclusive forward cursor. - #[serde(skip_serializing_if = "Option::is_none")] - pub after_seq: Option, - /// Exclusive backward cursor. - #[serde(skip_serializing_if = "Option::is_none")] - pub before_seq: Option, - /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, +pub struct GitHubTelemetryClientInfo { + /// Copilot CLI version string. + #[serde(rename = "cli_version")] + pub cli_version: String, + /// Name of the client application. + #[serde(rename = "client_name", skip_serializing_if = "Option::is_none")] + pub client_name: Option, + /// Type of client. + #[serde(rename = "client_type", skip_serializing_if = "Option::is_none")] + pub client_type: Option, + /// Copilot subscription plan, when known. + #[serde(rename = "copilot_plan", skip_serializing_if = "Option::is_none")] + pub copilot_plan: Option, + /// Number of logical CPU cores on the host. + #[serde(rename = "cpu_count", skip_serializing_if = "Option::is_none")] + pub cpu_count: Option, + /// Distinct CPU model names for the host, comma-separated. + #[serde(rename = "cpu_model", skip_serializing_if = "Option::is_none")] + pub cpu_model: Option, + /// Stable machine identifier for the device. + #[serde(rename = "dev_device_id", skip_serializing_if = "Option::is_none")] + pub dev_device_id: Option, + /// Whether the user is a GitHub/Microsoft staff member. + #[serde(rename = "is_staff", skip_serializing_if = "Option::is_none")] + pub is_staff: Option, + /// Node.js runtime version string. + #[serde(rename = "node_version")] + pub node_version: String, + /// Operating system architecture (e.g. arm64, x64). + #[serde(rename = "os_arch")] + pub os_arch: String, + /// Operating system platform (e.g. darwin, linux, win32). + #[serde(rename = "os_platform")] + pub os_platform: String, + /// Operating system version string. + #[serde(rename = "os_version")] + pub os_version: String, } -/// Durable factory resource consumption. +/// 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. /// ///
    /// @@ -6966,16 +8444,43 @@ pub struct FactoryListRunsRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunConsumed { - /// Accumulated active execution time in milliseconds. - pub active_ms: i64, - /// AI usage consumed by the run in nano-AIU. - pub nano_aiu: i64, - /// Total subagents spawned by the run. - pub subagents: i64, +pub struct GitHubTelemetryEvent { + /// Client environment metadata. + #[serde(skip_serializing_if = "Option::is_none")] + pub client: Option, + /// Copilot tracking ID for user-level attribution. + #[serde( + rename = "copilot_tracking_id", + skip_serializing_if = "Option::is_none" + )] + pub copilot_tracking_id: Option, + /// Timestamp when the event was created (ISO 8601 format). + #[serde(rename = "created_at", skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// Experiment assignment context. + #[serde( + rename = "exp_assignment_context", + skip_serializing_if = "Option::is_none" + )] + pub exp_assignment_context: Option, + /// Feature flags enabled for this session, as a map from flag to value. + #[serde(skip_serializing_if = "Option::is_none")] + pub features: Option>, + /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). + pub kind: String, + /// Numeric metrics as a map from key to value. + pub metrics: HashMap, + /// Reference to the model call that produced this event. + #[serde(rename = "model_call_id", skip_serializing_if = "Option::is_none")] + pub model_call_id: Option, + /// String-valued properties as a map from key to value. + pub properties: HashMap, + /// Session identifier the event belongs to. + #[serde(rename = "session_id", skip_serializing_if = "Option::is_none")] + pub session_id: Option, } -/// Prompt-safe terminal factory outcome. +/// 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. /// ///
    /// @@ -6985,24 +8490,17 @@ pub struct FactoryRunConsumed { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunTerminal { - /// Human-readable terminal error. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Machine-readable terminal failure. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure: Option, - /// Pause initiator metadata, or null when the run did not pause. - pub pause_info: Option, - /// Human-readable terminal reason. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Prompt-safe preview of the completed result. +pub struct GitHubTelemetryNotification { + /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. + pub event: GitHubTelemetryEvent, + /// Whether this is a restricted telemetry event (cli.restricted_telemetry). Hosts must route restricted events to first-party Microsoft stores only. + pub restricted: bool, + /// 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. #[serde(skip_serializing_if = "Option::is_none")] - pub result_preview: Option, + pub session_id: Option, } -/// Durable factory run summary with read-time live overlays. +/// Asks the SDK client to acquire a GitHub access token from an opaque callback registration. /// ///
    /// @@ -7012,96 +8510,40 @@ pub struct FactoryRunTerminal { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunSummary { - /// Epoch milliseconds when the current active segment started, or null while inactive. - pub active_segment_started_at: Option, - /// Approved effective resource ceilings, or null until approved. - pub approved: Option, - /// Whether the durable run state currently passes runtime resume eligibility checks. - pub can_resume: bool, - /// Epoch milliseconds when the run completed, or null while nonterminal. - pub completed_at: Option, - /// Durable resource consumption. - pub consumed: FactoryRunConsumed, - /// Epoch milliseconds when the run was created. - pub created_at: i64, - /// Current phase identity, or null before any phase is entered. - pub current_phase: Option, - /// Resource ceilings declared by the factory. - pub declared_limits: FactoryDeclaredLimits, - /// Number of phases declared by the factory. - pub declared_phase_count: i64, - /// Human-readable factory description. - pub description: String, - /// Registered factory name. - pub factory_name: String, - /// Number of direct factory agents currently live. - pub live_agent_count: i64, - /// Epoch milliseconds when this live-overlay snapshot was observed. - pub observed_at: i64, - /// Monotonic durable run revision. - pub revision: i64, - /// Factory run identifier. - pub run_id: String, - /// Epoch milliseconds when execution first started, or null before start. - pub started_at: Option, - /// Current factory run status. - pub status: FactoryRunStatus, - /// Terminal run outcome, or null while nonterminal. - pub terminal: Option, - /// Total direct factory agents spawned across all attempts. - pub total_spawned_agent_count: i64, - /// Epoch milliseconds when the durable run was last updated. - pub updated_at: i64, +pub struct GitHubTokenAcquireRequest { + /// Effective GitHub host for which the callback must return a token. + pub host: String, + /// Why the runtime is requesting a GitHub credential. + pub reason: GitHubTokenAcquireReason, + /// Opaque identifier generated by the SDK for this callback registration. + pub registration_id: String, + /// Session receiving the token. Absent only before a cloud session has been assigned its id. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_id: Option, } -/// A page of factory runs in durable creation order. -/// -///
    -/// -/// **Experimental.** This type 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 FactoryListRunsResult { - /// Whether terminal runs newer than this page exist. - #[serde(skip_serializing_if = "Option::is_none")] - pub has_more_newer: Option, - /// Newest terminal-run cursor in this page, or null when the terminal window is empty. - #[serde(skip_serializing_if = "Option::is_none")] - pub newest_seq: Option, - /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. - #[serde(skip_serializing_if = "Option::is_none")] - pub oldest_seq: Option, - /// Number of terminal runs older than this page. +pub struct GitHubTokenAcquireResultToken { + /// GitHub access token acquired by the SDK host. + pub access_token: String, + /// Remaining token lifetime in seconds when callback execution completes. It must exceed the one-hour preflight refresh threshold. + pub expires_in: i64, + /// GitHub credential response variant discriminator. + pub kind: GitHubTokenAcquireResultTokenKind, + /// OAuth token type. Defaults to bearer when omitted. #[serde(skip_serializing_if = "Option::is_none")] - pub omitted_older: Option, - /// Factory run summaries in durable creation order. - pub runs: Vec, + pub token_type: Option, } -/// One ordered factory progress line. -/// -///
    -/// -/// **Experimental.** This type 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 FactoryLogLine { - /// Progress line kind. - pub kind: FactoryLogLineKind, - /// Monotonic sequence number within the factory run. - pub seq: i64, - /// Progress text. - pub text: String, +pub struct GitHubTokenAcquireResultCancelled { + /// GitHub credential response variant discriminator. + pub kind: GitHubTokenAcquireResultCancelledKind, } -/// Parameters for recording factory progress. +/// A GitHub repository one of a working tree's remotes points at. /// ///
    /// @@ -7111,16 +8553,18 @@ pub struct FactoryLogLine { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryLogRequest { - /// Opaque token identifying the current factory execution attempt. - pub execution_token: String, - /// Ordered progress lines to append. - pub lines: Vec, - /// Factory run identifier. - pub run_id: String, +pub(crate) struct GitRemoteRepository { + /// GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. + pub host: String, + /// Repository name, without the owner. + pub name: String, + /// Account or organization owning the repository. + pub owner: String, + /// Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. + pub remote_name: String, } -/// Parameters for an owned durable pause checkpoint. +/// Git working tree whose GitHub remotes should be listed. /// ///
    /// @@ -7130,15 +8574,12 @@ pub struct FactoryLogRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryPauseCheckpointRequest { - /// Opaque token identifying the execution attempt that reached the checkpoint. - pub execution_token: String, - /// Stable author-defined checkpoint key. - pub key: String, - /// Factory run identifier. - pub run_id: String, +pub(crate) struct GitReposFromRemotesRequest { + /// Absolute path to the root of the git working tree. + pub git_root: String, } +/// The GitHub repositories a working tree's remotes point at. /// ///
    /// @@ -7148,12 +8589,13 @@ pub struct FactoryPauseCheckpointRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryPauseCheckpointResult { - /// Whether this execution attempt must pause or may continue. - pub action: FactoryPauseCheckpointAction, +pub(crate) struct GitReposFromRemotesResult { + /// One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + #[doc(hidden)] + pub(crate) repositories: Vec, } -/// Parameters for pausing a running factory. +/// Selects the configuration directory whose machine-wide state to read. /// ///
    /// @@ -7163,12 +8605,13 @@ pub struct FactoryPauseCheckpointResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryPauseRequest { - /// Factory run identifier. - pub run_id: String, +pub(crate) struct GlobalStateLoadForConfigDirRequest { + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + #[serde(skip_serializing_if = "Option::is_none")] + pub config_dir: Option, } -/// Durable lifecycle and timing for one factory phase. +/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. /// ///
    /// @@ -7178,39 +8621,34 @@ pub struct FactoryPauseRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryPhaseObservation { - /// Completed active time accumulated by this phase in milliseconds. - pub accumulated_active_ms: i64, - /// Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip timestamp (equal to `startedAt`). - #[serde(skip_serializing_if = "Option::is_none")] - pub completed_at: Option, - /// Current live active time for this phase in milliseconds. - pub current_active_ms: i64, - /// Optional human-readable phase detail. +pub struct InstalledPlugin { + /// Path where the plugin is cached locally + #[serde(rename = "cache_path", skip_serializing_if = "Option::is_none")] + pub cache_path: Option, + /// Whether the plugin is currently enabled + pub enabled: bool, + /// Installation timestamp + #[serde(rename = "installed_at")] + pub installed_at: String, + /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. + #[serde(rename = "installed_from", skip_serializing_if = "Option::is_none")] + pub installed_from: Option, + /// Marketplace the plugin came from (empty string for direct repo installs) + pub marketplace: String, + /// Plugin name + pub name: String, + /// Source for direct repo installs (when marketplace is empty) #[serde(skip_serializing_if = "Option::is_none")] - pub detail: Option, - /// Number of times execution entered this phase. - pub entry_count: i64, - /// Phase identifier. - pub id: String, - /// Most recent run attempt that entered this phase, or `0` if the phase has never been entered. - pub last_entered_run_attempt: i64, - /// Direct agents in this phase that are currently live. - pub live_agent_count: i64, - /// Zero-based declared phase ordinal, or null for an undeclared phase. - pub ordinal: Option, - /// Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip timestamp (equal to `completedAt`). + pub source: Option, + /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. + #[serde(rename = "source_sha", skip_serializing_if = "Option::is_none")] + pub source_sha: Option, + /// Version installed (if available) #[serde(skip_serializing_if = "Option::is_none")] - pub started_at: Option, - /// Derived lifecycle state of the phase. - pub status: FactoryPhaseStatus, - /// Human-readable phase title. - pub title: String, - /// Total direct agents associated with this phase. - pub total_agent_count: i64, + pub version: Option, } -/// One durable factory progress record. +/// An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. /// ///
    /// @@ -7220,22 +8658,20 @@ pub struct FactoryPhaseObservation { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryProgressLine { - /// Resume attempt that emitted this record. - pub attempt: i64, - /// Progress record kind. - pub kind: FactoryLogLineKind, - /// Phase active when the record was emitted, or null before any phase. - pub phase_id: Option, - /// Epoch milliseconds when the record was persisted. - pub recorded_at: i64, - /// Global monotonic sequence number within the run. - pub seq: i64, - /// Prompt-safe progress text. - pub text: String, +pub(crate) struct LoggedInUser { + /// Source account this account was derived from, when one was recorded. + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option, + /// Host the account belongs to, such as `github.com` or an Enterprise server. + pub host: String, + /// Account kind, when the host recorded one. Consumers must tolerate new strings. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// Account login on that host. + pub login: String, } -/// A bidirectional page of factory progress. +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
    /// @@ -7245,22 +8681,72 @@ pub struct FactoryProgressLine { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryProgressPage { - /// Whether progress records newer than this page exist. - pub has_more_newer: bool, - /// Whether progress records older than this page exist. - pub has_more_older: bool, - /// Newest sequence number in this page, or null when empty. - pub newest_seq: Option, - /// Oldest sequence number in this page, or null when empty. - pub oldest_seq: Option, - /// Progress records in sequence order. - pub records: Vec, - /// Run revision reflected by this page. - pub revision: i64, +pub(crate) struct GlobalStateLoadResult { + /// Whether the user has answered the prompt suggesting they install the desktop app. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_install_nudge_responded: Option, + /// Whether the app tip has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_tip_shown: Option, + /// Terminals the user has already been asked to set up, so the host does not ask twice. + #[serde(skip_serializing_if = "Option::is_none")] + pub asked_setup_terminals: Option>, + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_feedback_last_prompted_at: Option, + /// When the host first ran on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub first_launch_at: Option, + /// Plugins installed on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed_plugins: Option>, + /// Account used for the most recent sign-in. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) last_logged_in_user: Option, + /// Every account the host has signed in to on this machine. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) logged_in_users: Option>, + /// Whether the one-off cleanup of stored reasoning summaries has run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summaries_cleanup_done: Option, + /// Models the user selected recently, most recent first. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_model_ids: Option>, + /// Whether the user declined to trust the sandbox credential proxy CA. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_credential_proxy_ca_declined: Option, + /// Whether the sandbox onboarding has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_onboarding_shown: Option, + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff: Option, + /// Whether the user was recognized as GitHub staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_github: Option, + /// When the staff-only log level migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_log_level_migration_at: Option, + /// Whether the user was recognized as Microsoft staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_microsoft: Option, + /// When the staff-only model reset last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_model_reset_at: Option, + /// When the staff-only update channel migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_update_channel_migration_at: Option, + /// Folders where the user declined the init prompt, so it stays hidden there. + #[serde(skip_serializing_if = "Option::is_none")] + pub suppress_init_folders: Option>, + /// Folders the user has marked as trusted. + #[serde(skip_serializing_if = "Option::is_none")] + pub trusted_folders: Option>, } -/// Wire-only per-invocation factory resource ceiling overrides. +/// A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. /// ///
    /// @@ -7270,22 +8756,18 @@ pub struct FactoryProgressPage { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunLimits { - /// Maximum AI credits consumed by factory subagents and their descendants. The post-paid ceiling is soft: parallel turns can settle beyond it before the run stops. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_ai_credits: Option, - /// Maximum number of factory subagents that may run concurrently. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_concurrent_subagents: Option, - /// Maximum total number of factory subagents that may be admitted. +pub(crate) struct GlobalStateWriteKeyRequest { + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. #[serde(skip_serializing_if = "Option::is_none")] - pub max_total_subagents: Option, - /// 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. + pub config_dir: Option, + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + pub key: String, + /// Value to store for the key. Omit it, or pass null, to remove the key instead. #[serde(skip_serializing_if = "Option::is_none")] - pub timeout_seconds: Option, + pub value: Option, } -/// Parameters for resuming a factory run from its persisted identity. +/// Pending external tool call request ID, with the tool result or an error describing why it failed. /// ///
    /// @@ -7295,21 +8777,18 @@ pub struct FactoryRunLimits { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryResumeRequest { - /// Optional per-invocation resource ceiling overrides. - #[serde(skip_serializing_if = "Option::is_none")] - pub limits: Option, - /// Whether to emit factory phase names to the session transcript. +pub struct HandlePendingToolCallRequest { + /// Error message if the tool call failed #[serde(skip_serializing_if = "Option::is_none")] - pub log_phase_names: Option, - /// Whether to notify the originating session when the factory completes. + pub error: Option, + /// Request ID of the pending tool call + pub request_id: RequestId, + /// Tool call result (string or expanded result object) #[serde(skip_serializing_if = "Option::is_none")] - pub notify_on_complete: Option, - /// Factory run identifier. - pub run_id: String, + pub result: Option, } -/// Complete current or terminal factory run envelope. +/// Indicates whether the external tool call result was handled successfully. /// ///
    /// @@ -7319,35 +8798,12 @@ pub struct FactoryResumeRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunResult { - /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. - #[serde(skip_serializing_if = "Option::is_none")] - pub attempt: Option, - /// Error message for an errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Machine-readable failure details for a halted or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure: Option, - /// Structured pause initiator metadata for a paused attempt. - #[serde(skip_serializing_if = "Option::is_none")] - pub pause_info: Option, - /// Reason for a halted or cancelled run. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Completed factory result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Factory run identifier. - pub run_id: String, - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, +pub struct HandlePendingToolCallResult { + /// Whether the tool call result was handled successfully + pub success: bool, } -/// Resolved persisted factory identity and resumed run envelope. +/// Indicates whether an in-progress manual compaction was aborted. /// ///
    /// @@ -7357,14 +8813,12 @@ pub struct FactoryRunResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryResumeResult { - /// Persisted factory name resolved for the resumed run. - pub factory_name: String, - /// Terminal resumed run envelope. - pub run: FactoryRunResult, +pub struct HistoryAbortManualCompactionResult { + /// Whether an in-progress manual compaction was aborted. False when no manual compaction was running, when its abort controller was already aborted, or when the session is remote. + pub aborted: bool, } -/// Full factory run observability detail. +/// Indicates whether an in-progress background compaction was cancelled. /// ///
    /// @@ -7374,56 +8828,12 @@ pub struct FactoryResumeResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunDetail { - /// Epoch milliseconds when the current active segment started, or null while inactive. - pub active_segment_started_at: Option, - /// Durable identities and live statuses for direct factory agents. - pub agents: Vec, - /// Approved effective resource ceilings, or null until approved. - pub approved: Option, - /// Whether the durable run state currently passes runtime resume eligibility checks. - pub can_resume: bool, - /// Epoch milliseconds when the run completed, or null while nonterminal. - pub completed_at: Option, - /// Durable resource consumption. - pub consumed: FactoryRunConsumed, - /// Epoch milliseconds when the run was created. - pub created_at: i64, - /// Current phase identity, or null before any phase is entered. - pub current_phase: Option, - /// Resource ceilings declared by the factory. - pub declared_limits: FactoryDeclaredLimits, - /// Number of phases declared by the factory. - pub declared_phase_count: i64, - /// Human-readable factory description. - pub description: String, - /// Registered factory name. - pub factory_name: String, - /// Number of direct factory agents currently live. - pub live_agent_count: i64, - /// Epoch milliseconds when this live-overlay snapshot was observed. - pub observed_at: i64, - /// Lifecycle and timing observations for each factory phase. - pub phases: Vec, - /// Bidirectional page of durable factory progress. - pub progress: FactoryProgressPage, - /// Monotonic durable run revision. - pub revision: i64, - /// Factory run identifier. - pub run_id: String, - /// Epoch milliseconds when execution first started, or null before start. - pub started_at: Option, - /// Current factory run status. - pub status: FactoryRunStatus, - /// Terminal run outcome, or null while nonterminal. - pub terminal: Option, - /// Total direct factory agents spawned across all attempts. - pub total_spawned_agent_count: i64, - /// Epoch milliseconds when the durable run was last updated. - pub updated_at: i64, +pub struct HistoryCancelBackgroundCompactionResult { + /// Whether an in-progress background compaction was cancelled. False when no compaction was running, when the session is remote, or when the underlying processor was unavailable. + pub cancelled: bool, } -/// Options controlling factory invocation. +/// Parameters for clearing the conversation and seeding the window that replaces it. /// ///
    /// @@ -7433,22 +8843,12 @@ pub struct FactoryRunDetail { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RunOptions { - /// Per-invocation resource ceiling overrides. - #[serde(skip_serializing_if = "Option::is_none")] - pub limits: Option, - /// Whether to emit factory phase names to the session transcript. - #[serde(skip_serializing_if = "Option::is_none")] - pub log_phase_names: Option, - /// Whether to notify the originating session when the factory completes. - #[serde(skip_serializing_if = "Option::is_none")] - pub notify_on_complete: Option, - /// Run identifier whose journal and progress should seed this resumed run. - #[serde(skip_serializing_if = "Option::is_none")] - pub resume_from_run_id: Option, +pub struct 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. + pub prompt: String, } -/// Parameters for invoking a registered factory. +/// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. /// ///
    /// @@ -7458,17 +8858,12 @@ pub struct RunOptions { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunRequest { - /// Factory input value. - pub args: serde_json::Value, - /// Registered factory name. - pub name: String, - /// Factory invocation options. - #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option, +pub struct HistoryClearContextResult { + /// Number of non-system, non-developer messages that were removed from the conversation. Zero only when the window already held no conversation. + pub messages_cleared: i64, } -/// Internal parameters for resuming a factory run from a tool. +/// Post-compaction context window usage breakdown /// ///
    /// @@ -7478,18 +8873,25 @@ pub struct FactoryRunRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct FactoryToolResumeRequest { - /// Optional per-invocation resource ceiling overrides. +pub struct HistoryCompactContextWindow { + /// Token count from non-system messages (user, assistant, tool) #[serde(skip_serializing_if = "Option::is_none")] - pub limits: Option, - /// Factory run identifier. - pub run_id: String, - /// Opaque identifier of the originating tool call. + pub conversation_tokens: Option, + /// Current total tokens in the context window (system + conversation + tool definitions) + pub current_tokens: i64, + /// Current number of messages in the conversation + pub messages_length: i64, + /// Token count from system message(s) #[serde(skip_serializing_if = "Option::is_none")] - pub tool_call_id: Option, + pub system_tokens: Option, + /// Maximum token count for the model's context window + pub token_limit: i64, + /// Token count from tool definitions + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_definitions_tokens: Option, } -/// Options for an internal tool-originated factory invocation. +/// Optional compaction parameters. /// ///
    /// @@ -7499,16 +8901,19 @@ pub(crate) struct FactoryToolResumeRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct FactoryToolRunOptions { - /// Per-invocation resource ceiling overrides. +pub struct HistoryCompactRequest { + /// Optional user-provided instructions to focus the compaction summary #[serde(skip_serializing_if = "Option::is_none")] - pub limits: Option, - /// Run identifier whose journal and progress should seed this resumed run. + pub custom_instructions: Option, + /// 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. #[serde(skip_serializing_if = "Option::is_none")] - pub resume_from_run_id: Option, + pub token_limit: Option, + /// 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). + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger: Option, } -/// Internal parameters for invoking a registered factory from a tool. +/// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. /// ///
    /// @@ -7518,21 +8923,22 @@ pub(crate) struct FactoryToolRunOptions { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct FactoryToolRunRequest { - /// Factory input value. - pub args: serde_json::Value, - /// Registered factory name. - pub name: String, - /// Tool-originated factory invocation options. - #[doc(hidden)] +pub struct HistoryCompactResult { + /// Post-compaction context window usage breakdown #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) options: Option, - /// Opaque identifier of the originating tool call. + pub context_window: Option, + /// Number of messages removed during compaction + pub messages_removed: i64, + /// Whether compaction completed successfully + pub success: bool, + /// Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). #[serde(skip_serializing_if = "Option::is_none")] - pub tool_call_id: Option, + pub summary_content: Option, + /// Number of tokens freed by compaction + pub tokens_removed: i64, } -/// Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. +/// A root user turn that the session can rewind to. /// ///
    /// @@ -7542,23 +8948,48 @@ pub(crate) struct FactoryToolRunRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FleetStartRequest { - /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request - #[serde(skip_serializing_if = "Option::is_none")] - pub attachments: Option>, - /// If false, this request will not trigger a Premium Request Unit charge. User requests default to billable. - #[doc(hidden)] - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) billable: Option, - /// Optional user prompt to combine with fleet instructions - #[serde(skip_serializing_if = "Option::is_none")] - pub prompt: Option, - /// If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. +pub struct HistoryRewindPoint { + /// Whether at least one file in this turn or a later turn can be restored. + pub can_restore_files: bool, + /// ID of the user.message event that begins the discarded suffix. + pub event_id: String, + /// Number of unique files in this turn and all later turns that have captured changes. + pub file_count: i64, + /// Whether this turn was an automatically injected autopilot continuation. + pub is_autopilot_continuation: bool, + /// Lines added by this turn's captured file changes. + pub lines_added: i64, + /// Lines removed by this turn's captured file changes. + pub lines_removed: i64, + /// ISO timestamp of the user turn. + pub timestamp: String, + /// Whether this turn itself captured any file changes. + pub turn_changed_files: bool, + /// User-visible message text for the turn. + pub user_message: String, +} + +/// Rewind points and file-change-tracking availability for the session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HistoryListRewindPointsResult { + /// Whether this session captured file changes from its first turn. + pub file_change_tracking_enabled: bool, + /// Root user turns in chronological order. Empty when `unavailableReason` is set. + pub points: Vec, + /// 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`. #[serde(skip_serializing_if = "Option::is_none")] - pub wait: Option, + pub unavailable_reason: Option, } -/// Indicates whether fleet mode was successfully activated. +/// Event boundary to preview for conversation-and-files rewind. /// ///
    /// @@ -7568,12 +8999,12 @@ pub struct FleetStartRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FleetStartResult { - /// Whether fleet mode was successfully activated - pub started: bool, +pub struct HistoryPreviewRewindRequest { + /// ID of the user.message event that begins the discarded suffix. + pub event_id: String, } -/// Folder path to add to trusted folders. +/// A file that a conversation-and-files rewind would restore. /// ///
    /// @@ -7583,12 +9014,18 @@ pub struct FleetStartResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FolderTrustAddParams { - /// Folder path to mark as trusted +pub struct HistoryRewindFilePreview { + /// Aggregate change made across the discarded turns. + pub change_type: HistoryRewindChangeType, + /// Lines added across the discarded turns. + pub lines_added: i64, + /// Lines removed across the discarded turns. + pub lines_removed: i64, + /// Absolute path of the captured file. pub path: String, } -/// Folder path to check for trust. +/// Files and aggregate changes for a prospective rewind. /// ///
    /// @@ -7598,12 +9035,53 @@ pub struct FolderTrustAddParams { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FolderTrustCheckParams { - /// Folder path to check +pub struct HistoryPreviewRewindResult { + /// Whether file restore is available for this session. This is authoritative: switch on it and read `reason` only when it is false. + pub available: bool, + /// Number of unique files in the preview. + pub file_count: i64, + /// Files ordered by path. + pub files: Vec, + /// Why file restore is unavailable, when applicable. Populated only when `available` is false and never set when `available` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +/// Boundary and mode for rewinding session history. +/// +///
    +/// +/// **Experimental.** This type 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 HistoryRewindRequest { + /// ID of the user.message event that begins the discarded suffix. + pub event_id: String, + /// Whether to rewind only conversation history or also restore captured files. + pub mode: HistoryRewindMode, +} + +/// A captured file that rewind intentionally left unchanged. +/// +///
    +/// +/// **Experimental.** This type 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 HistorySkippedFileRestore { + /// Absolute path of the skipped file. pub path: String, + /// Reason the file was not restored. + pub reason: HistoryFileRestoreSkipReason, } -/// Folder trust check result. +/// Structured outcome of a rewind request. /// ///
    /// @@ -7613,12 +9091,22 @@ pub struct FolderTrustCheckParams { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FolderTrustCheckResult { - /// Whether the folder is trusted - pub trusted: bool, +pub struct HistoryRewindResult { + /// Failure detail. Set only for the failure and partial-failure outcomes (`files-rolled-back`, `rollback-incomplete`, `truncation-failed`, `checkpoint-cleanup-failed`, `snapshot-prune-failed`); omitted for `success` and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`). + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// 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`. + #[serde(skip_serializing_if = "Option::is_none")] + pub events_removed: Option, + /// 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. + pub outcome: HistoryRewindOutcome, + /// 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. + pub restored_files: Vec, + /// 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. + pub skipped_files: Vec, } -/// Client environment metadata describing the process that produced a telemetry event. +/// Markdown summary of the conversation context (empty when not available). /// ///
    /// @@ -7628,46 +9116,12 @@ pub struct FolderTrustCheckResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GitHubTelemetryClientInfo { - /// Copilot CLI version string. - #[serde(rename = "cli_version")] - pub cli_version: String, - /// Name of the client application. - #[serde(rename = "client_name", skip_serializing_if = "Option::is_none")] - pub client_name: Option, - /// Type of client. - #[serde(rename = "client_type", skip_serializing_if = "Option::is_none")] - pub client_type: Option, - /// Copilot subscription plan, when known. - #[serde(rename = "copilot_plan", skip_serializing_if = "Option::is_none")] - pub copilot_plan: Option, - /// Number of logical CPU cores on the host. - #[serde(rename = "cpu_count", skip_serializing_if = "Option::is_none")] - pub cpu_count: Option, - /// Distinct CPU model names for the host, comma-separated. - #[serde(rename = "cpu_model", skip_serializing_if = "Option::is_none")] - pub cpu_model: Option, - /// Stable machine identifier for the device. - #[serde(rename = "dev_device_id", skip_serializing_if = "Option::is_none")] - pub dev_device_id: Option, - /// Whether the user is a GitHub/Microsoft staff member. - #[serde(rename = "is_staff", skip_serializing_if = "Option::is_none")] - pub is_staff: Option, - /// Node.js runtime version string. - #[serde(rename = "node_version")] - pub node_version: String, - /// Operating system architecture (e.g. arm64, x64). - #[serde(rename = "os_arch")] - pub os_arch: String, - /// Operating system platform (e.g. darwin, linux, win32). - #[serde(rename = "os_platform")] - pub os_platform: String, - /// Operating system version string. - #[serde(rename = "os_version")] - pub os_version: String, +pub struct HistorySummarizeForHandoffResult { + /// 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. + pub summary: String, } -/// 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. +/// Identifier of the event to truncate to; this event and all later events are removed. /// ///
    /// @@ -7677,43 +9131,50 @@ pub struct GitHubTelemetryClientInfo { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GitHubTelemetryEvent { - /// Client environment metadata. +pub struct HistoryTruncateRequest { + /// Event ID to truncate to. This event and all events after it are removed from the session. + pub event_id: String, +} + +/// Number of events that were removed by the truncation. +/// +///
    +/// +/// **Experimental.** This type 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 HistoryTruncateResult { + /// Failure detail when checkpointCleanupFailed is true. #[serde(skip_serializing_if = "Option::is_none")] - pub client: Option, - /// Copilot tracking ID for user-level attribution. - #[serde( - rename = "copilot_tracking_id", - skip_serializing_if = "Option::is_none" - )] - pub copilot_tracking_id: Option, - /// Timestamp when the event was created (ISO 8601 format). - #[serde(rename = "created_at", skip_serializing_if = "Option::is_none")] - pub created_at: Option, - /// Experiment assignment context. - #[serde( - rename = "exp_assignment_context", - skip_serializing_if = "Option::is_none" - )] - pub exp_assignment_context: Option, - /// Feature flags enabled for this session, as a map from flag to value. + pub checkpoint_cleanup_error: Option, + /// 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. #[serde(skip_serializing_if = "Option::is_none")] - pub features: Option>, - /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). - pub kind: String, - /// Numeric metrics as a map from key to value. - pub metrics: HashMap, - /// Reference to the model call that produced this event. - #[serde(rename = "model_call_id", skip_serializing_if = "Option::is_none")] - pub model_call_id: Option, - /// String-valued properties as a map from key to value. - pub properties: HashMap, - /// Session identifier the event belongs to. - #[serde(rename = "session_id", skip_serializing_if = "Option::is_none")] - pub session_id: Option, + pub checkpoint_cleanup_failed: Option, + /// Number of events that were removed + pub events_removed: i64, } -/// 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. +/// Runtime-owned wire payload for a server-to-client hook callback invocation. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct HookInvokeRequest { + pub hook_type: HookType, + pub input: serde_json::Value, + pub session_id: SessionId, +} + +/// Optional output returned by an SDK callback hook. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct HookInvokeResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub output: Option, +} + +/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. /// ///
    /// @@ -7723,17 +9184,16 @@ pub struct GitHubTelemetryEvent { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GitHubTelemetryNotification { - /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. - pub event: GitHubTelemetryEvent, - /// Whether this is a restricted telemetry event (cli.restricted_telemetry). Hosts must route restricted events to first-party Microsoft stores only. - pub restricted: bool, - /// 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. +pub struct HooksDiscoverRequest { + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. #[serde(skip_serializing_if = "Option::is_none")] - pub session_id: Option, + pub exclude_host_hooks: Option, + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + #[serde(skip_serializing_if = "Option::is_none")] + pub project_paths: Option>, } -/// Asks the SDK client to acquire a GitHub access token from an opaque callback registration. +/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. /// ///
    /// @@ -7743,40 +9203,93 @@ pub struct GitHubTelemetryNotification { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GitHubTokenAcquireRequest { - /// Effective GitHub host for which the callback must return a token. - pub host: String, - /// Why the runtime is requesting a GitHub credential. - pub reason: GitHubTokenAcquireReason, - /// Opaque identifier generated by the SDK for this callback registration. - pub registration_id: String, - /// Session receiving the token. Absent only before a cloud session has been assigned its id. +pub struct HooksDiscoverResult { + /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + pub errors: Vec, + /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + pub hooks: Vec, + /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + pub warnings: Vec, +} + +/// GitHub Mission Control registration options. The compute ID is application-owned and stable. +/// +///
    +/// +/// **Experimental.** This type 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 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, + /// Secret connection token, absent when authentication is disabled. #[serde(skip_serializing_if = "Option::is_none")] - pub session_id: Option, + 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 GitHubTokenAcquireResultToken { - /// GitHub access token acquired by the SDK host. - pub access_token: String, - /// Remaining token lifetime in seconds when callback execution completes. It must exceed the one-hour preflight refresh threshold. - pub expires_in: i64, - /// GitHub credential response variant discriminator. - pub kind: GitHubTokenAcquireResultTokenKind, - /// OAuth token type. Defaults to bearer when omitted. +pub struct HostConfiguration { + /// Requested GitHub Mission Control registration. #[serde(skip_serializing_if = "Option::is_none")] - pub token_type: Option, + 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. +/// +///
    +/// +/// **Experimental.** This type 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 GitHubTokenAcquireResultCancelled { - /// GitHub credential response variant discriminator. - pub kind: GitHubTokenAcquireResultCancelledKind, +pub struct HostDisposeRequest { + /// Listener UUID. Unknown or successfully stopped IDs are harmless. + pub host_id: String, } -/// Pending external tool call request ID, with the tool result or an error describing why it failed. +/// Empty acknowledgement for a completed host lifecycle operation. /// ///
    /// @@ -7786,18 +9299,46 @@ pub struct GitHubTokenAcquireResultCancelled { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HandlePendingToolCallRequest { - /// Error message if the tool call failed +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")] +pub struct HostExitedNotification { + /// Explicit startup or teardown failure, when present. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Request ID of the pending tool call - pub request_id: RequestId, - /// Tool call result (string or expanded result object) + /// Process exit status when available; absent for in-process listener tasks. #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, + pub exit_code: Option, + /// Listener UUID. + pub host_id: String, + /// Cause of termination. + pub reason: HostExitReason, } -/// Indicates whether the external tool call result was handled successfully. +/// Local WebSocket transport options. /// ///
    /// @@ -7807,12 +9348,22 @@ pub struct HandlePendingToolCallRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HandlePendingToolCallResult { - /// Whether the tool call result was handled successfully - pub success: bool, +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, } -/// Indicates whether an in-progress manual compaction was aborted. +/// Publishes a resident session attached to the listener's owning connection. /// ///
    /// @@ -7822,12 +9373,14 @@ pub struct HandlePendingToolCallResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryAbortManualCompactionResult { - /// Whether an in-progress manual compaction was aborted. False when no manual compaction was running, when its abort controller was already aborted, or when the session is remote. - pub aborted: bool, +pub struct HostPublishSessionRequest { + /// Listener UUID returned by host.start. + pub host_id: String, + /// Canonical runtime session ID attached to the listener's owning connection. + pub session_id: SessionId, } -/// Indicates whether an in-progress background compaction was cancelled. +/// The existing runtime identity and its resource on the listener. /// ///
    /// @@ -7837,12 +9390,14 @@ pub struct HistoryAbortManualCompactionResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryCancelBackgroundCompactionResult { - /// Whether an in-progress background compaction was cancelled. False when no compaction was running, when the session is remote, or when the underlying processor was unavailable. - pub cancelled: bool, +pub struct HostPublishSessionResult { + /// Canonical runtime ID of the published session. + pub session_id: SessionId, + /// AHP resource URI for the session on this listener. + pub session_uri: String, } -/// Parameters for clearing the conversation and seeding the window that replaces it. +/// Readiness reported by the supervised hosting participant on its own SDK connection. /// ///
    /// @@ -7852,12 +9407,56 @@ pub struct HistoryCancelBackgroundCompactionResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct 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. - pub prompt: String, +pub struct HostReadyRequest { + /// Actual bound WebSocket URL. + #[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, } -/// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. +/// Listener-scoped registration, not a copy or durable adoption of a session. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HostRegisterSessionRequest { + /// Additional directories already granted to the resident session. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_directories: Option>, + /// Session creation time in milliseconds since the Unix epoch, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at_unix_ms: Option, + /// Last session modification time in milliseconds since the Unix epoch, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub modified_at_unix_ms: Option, + /// Canonical ID of the existing resident runtime session. + pub session_id: SessionId, + /// Current display title of the resident session, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Absolute working directory of the resident session. + pub working_directory: String, +} + +/// Application callback routed over its existing SDK connection. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HostSessionCreateCallback { + /// Host-selected SDK creation or resume settings, without executable callbacks or tools. + pub config: HashMap, + /// Unique identity of the session participation being requested. + pub handoff_id: String, + /// Listener UUID identifying the owning application's host. + pub host_id: String, + /// Resume an app-owned durable session instead of creating a new session. + #[serde(skip_serializing_if = "Option::is_none")] + pub resume: Option, +} + +/// One application-owned session handoff, requested by the supervised hosting participant. /// ///
    /// @@ -7867,12 +9466,17 @@ pub struct HistoryClearContextRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryClearContextResult { - /// Number of non-system, non-developer messages that were removed from the conversation. Zero only when the window already held no conversation. - pub messages_cleared: i64, +pub struct HostSessionCreateRequest { + /// Host-selected SDK creation or resume settings, without executable callbacks or tools. + pub config: HashMap, + /// Unique identity for this participation, independent of the session lifetime. + pub handoff_id: String, + /// Resume an app-owned durable session instead of creating a new session. + #[serde(skip_serializing_if = "Option::is_none")] + pub resume: Option, } -/// Post-compaction context window usage breakdown +/// The resident session the application has materialized on its own connection. /// ///
    /// @@ -7882,25 +9486,64 @@ pub struct HistoryClearContextResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryCompactContextWindow { - /// Token count from non-system messages (user, assistant, tool) +pub struct HostSessionCreateResult { + /// Runtime session UUID materialized on the application's SDK connection. + pub session_id: SessionId, +} + +/// Releases the original application session object retained for one handoff. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HostSessionReleasedNotification { + /// Identity of the handoff retaining the original application session object. + pub handoff_id: String, + /// Listener UUID whose application session participation ended. + pub host_id: String, +} + +/// Ends one participation, not the application's session lifetime. +/// +///
    +/// +/// **Experimental.** This type 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 HostSessionReleaseRequest { + /// Identity of the participation to release without destroying the session. + pub handoff_id: String, +} + +/// Starts a supervised AHP host with at least one explicitly selected transport. +/// +///
    +/// +/// **Experimental.** This type 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 HostStartRequest { + /// Registers a GitHub Mission Control environment and enables its relay transport. #[serde(skip_serializing_if = "Option::is_none")] - pub conversation_tokens: Option, - /// Current total tokens in the context window (system + conversation + tool definitions) - pub current_tokens: i64, - /// Current number of messages in the conversation - pub messages_length: i64, - /// Token count from system message(s) + pub github_environment: Option, + /// Caller-generated UUID identifying this connection-owned listener. + pub host_id: String, + /// Enables a local WebSocket listener. #[serde(skip_serializing_if = "Option::is_none")] - pub system_tokens: Option, - /// Maximum token count for the model's context window - pub token_limit: i64, - /// Token count from tool definitions + pub local_server: Option, + /// Ask the owning application to resume its durable AHP sessions. #[serde(skip_serializing_if = "Option::is_none")] - pub tool_definitions_tokens: Option, + pub resume_factory: Option, + /// Ask the owning SDK application to materialize AHP sessions. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_factory: Option, } -/// Optional compaction parameters. +/// Listener readiness, returned only after binding and the supervised participant's SDK handshake. /// ///
    /// @@ -7910,19 +9553,24 @@ pub struct HistoryCompactContextWindow { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryCompactRequest { - /// Optional user-provided instructions to focus the compaction summary +pub struct HostStartResult { + /// GitHub Mission Control environment ID, present when its relay transport is ready. #[serde(skip_serializing_if = "Option::is_none")] - pub custom_instructions: Option, - /// 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. + 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. #[serde(skip_serializing_if = "Option::is_none")] - pub token_limit: Option, - /// 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). + pub pid: Option, + /// Secret connection token, absent when authentication is disabled. #[serde(skip_serializing_if = "Option::is_none")] - pub trigger: Option, + pub token: Option, + /// Actual bound WebSocket URL, including the allocated port. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, } -/// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. +/// Catalogue identity retained from a bound candidate or plan at installation time. /// ///
    /// @@ -7932,22 +9580,32 @@ pub struct HistoryCompactRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryCompactResult { - /// Post-compaction context window usage breakdown +pub struct InstallationCatalogueIdentity { + /// Catalogue description retained at install planning time. #[serde(skip_serializing_if = "Option::is_none")] - pub context_window: Option, - /// Number of messages removed during compaction - pub messages_removed: i64, - /// Whether compaction completed successfully - pub success: bool, - /// Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). + pub description: Option, + /// Human display name retained from the catalogue candidate. + pub display_name: String, + /// Catalogue item URL when supplied by the authority. #[serde(skip_serializing_if = "Option::is_none")] - pub summary_content: Option, - /// Number of tokens freed by compaction - pub tokens_removed: i64, + pub item_url: Option, + /// Catalogue publisher retained at install planning time. + #[serde(skip_serializing_if = "Option::is_none")] + pub publisher: Option, + /// Authority resource identifier when supplied by the catalogue. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_id: Option, + /// Catalogue authority/source string that supplied the candidate. + pub source: String, + /// Catalogue trust observation retained at install planning time. + #[serde(skip_serializing_if = "Option::is_none")] + pub trust_at_install: Option, + /// Catalogue version retained at install planning time. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, } -/// A root user turn that the session can rewind to. +/// The configuration-change alternative for the transportChoices entry at the same index. Only the selected alternative is applied; entries are not cumulative. The payload stays behind the runtime boundary. /// ///
    /// @@ -7957,28 +9615,38 @@ pub struct HistoryCompactResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryRewindPoint { - /// Whether at least one file in this turn or a later turn can be restored. - pub can_restore_files: bool, - /// ID of the user.message event that begins the discarded suffix. - pub event_id: String, - /// Number of unique files in this turn and all later turns that have captured changes. - pub file_count: i64, - /// Whether this turn was an automatically injected autopilot continuation. - pub is_autopilot_continuation: bool, - /// Lines added by this turn's captured file changes. - pub lines_added: i64, - /// Lines removed by this turn's captured file changes. - pub lines_removed: i64, - /// ISO timestamp of the user turn. - pub timestamp: String, - /// Whether this turn itself captured any file changes. - pub turn_changed_files: bool, - /// User-visible message text for the turn. - pub user_message: String, +pub struct McpPlanConfigurationChange { + /// Names of the configuration fields the change would set, without their values. + pub changed_fields: Vec, + /// Configuration key the change applies to. + pub config_key: String, + /// Whether the change would create a new entry or modify an existing one. + pub operation: McpPlanConfigurationOperation, + /// Scope the change would be written to. + pub scope: McpPlanScope, + /// Secret placeholders the written configuration would reference. The constrained placeholder type cannot carry a literal secret value. + pub secret_references: Vec, } -/// Rewind points and file-change-tracking availability for the session. +/// Final remote configuration, not a template. The producer refuses external-value +/// expansion before presenting this review. Receipt-owned secrets appear only as +/// `${installation-secret:}` references whose `` matches a reviewed +/// `${secret:}` placeholder; values are never included. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationRemoteConfiguration { + /// Configured headers, excluding separately authorised OAuth tokens. Values may + /// contain owned secret references, never secret values. + pub headers: HashMap, + /// Configured tool selection, not permission to invoke those tools. + pub tools: Vec, + /// Transport in the effective persisted remote configuration. + pub transport: McpPlanRemoteTransport, + /// Exact resolved endpoint, without templates or secret placeholders. + pub url: String, +} + +/// Normalised identity of the MCP server a plan targets, independent of how the card spelled it. /// ///
    /// @@ -7988,17 +9656,20 @@ pub struct HistoryRewindPoint { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryListRewindPointsResult { - /// Whether this session captured file changes from its first turn. - pub file_change_tracking_enabled: bool, - /// Root user turns in chronological order. Empty when `unavailableReason` is set. - pub points: Vec, - /// 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`. +pub struct McpPlanResourceIdentity { + /// Canonical, normalised name of the server, for example `io.github.owner/server`. + pub canonical_name: String, + /// Registry identifier of the server, when it came from a registry. #[serde(skip_serializing_if = "Option::is_none")] - pub unavailable_reason: Option, + pub registry_id: Option, + /// Local configuration key the server would be recorded under. + pub server_name: String, + /// Version advertised by the card, when it declares one. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, } -/// Event boundary to preview for conversation-and-files rewind. +/// One Registry string-valued configuration entry for the selected transport. /// ///
    /// @@ -8008,12 +9679,16 @@ pub struct HistoryListRewindPointsResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryPreviewRewindRequest { - /// ID of the user.message event that begins the discarded suffix. - pub event_id: String, +pub struct McpInstallationInput { + /// Exact category declared by the selected choice. + pub category: McpPlanValueCategory, + /// Exact key declared by the selected choice. + pub key: String, + /// Explicit non-secret value. Secret placeholders use a separate input channel. + pub value: String, } -/// A file that a conversation-and-files rewind would restore. +/// Outcome of evaluating the planned server against registry and enterprise policy. Evaluation is read-only. /// ///
    /// @@ -8023,18 +9698,17 @@ pub struct HistoryPreviewRewindRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryRewindFilePreview { - /// Aggregate change made across the discarded turns. - pub change_type: HistoryRewindChangeType, - /// Lines added across the discarded turns. - pub lines_added: i64, - /// Lines removed across the discarded turns. - pub lines_removed: i64, - /// Absolute path of the captured file. - pub path: String, +pub struct McpPlanPolicyResult { + /// What policy decided for this server. + pub decision: McpPlanPolicyDecision, + /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Which authority produced the decision. + pub source: McpPlanPolicySource, } -/// Files and aggregate changes for a prospective rewind. +/// Provenance of the exact validated JSON MCP card content bound privately to a completed plan and its opaque handle. /// ///
    /// @@ -8044,19 +9718,18 @@ pub struct HistoryRewindFilePreview { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryPreviewRewindResult { - /// Whether file restore is available for this session. This is authoritative: switch on it and read `reason` only when it is false. - pub available: bool, - /// Number of unique files in the preview. - pub file_count: i64, - /// Files ordered by path. - pub files: Vec, - /// Why file restore is unavailable, when applicable. Populated only when `available` is false and never set when `available` is true. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, +pub struct McpPlanProvenance { + /// Authority associated with the validated card, without path, query, or credentials. Inert untrusted data. + pub authority: String, + /// Semantic digest of the exact validated JSON content bound to the plan handle. + pub card_digest: CardDigest, + /// JSON MCP media type the validated card was interpreted as. + pub media_type: McpServerCardMediaType, + /// ISO 8601 timestamp at which the runtime completed strict parsing and schema validation of the card content. + pub validated_at: String, } -/// Boundary and mode for rewinding session history. +/// One non-secret scalar value a transport choice needs before it can be applied. /// ///
    /// @@ -8066,14 +9739,47 @@ pub struct HistoryPreviewRewindResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryRewindRequest { - /// ID of the user.message event that begins the discarded suffix. - pub event_id: String, - /// Whether to rewind only conversation history or also restore captured files. - pub mode: HistoryRewindMode, +pub struct McpPlanRequiredValueScalar { + /// Where the value is applied when the server is launched. + pub category: McpPlanValueCategory, + /// Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. + #[serde(skip_serializing_if = "Option::is_none")] + pub default_value: Option, + /// Human-readable explanation from the card. Inert untrusted text. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Whether the value may be supplied more than once. + pub is_repeated: bool, + /// Key the value is supplied under. Inert untrusted data. + pub key: String, + /// Discriminator: this required value uses a scalar type. + #[serde(deserialize_with = "McpPlanRequiredValueScalar::deserialize_kind")] + pub kind: McpPlanRequiredValueScalarKind, + /// Whether the value must be present for the plan to be applicable. + pub required: bool, + /// Human-readable label from the card. Inert untrusted text. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Scalar type the value must conform to. + pub value_type: McpPlanScalarValueType, } -/// A captured file that rewind intentionally left unchanged. +impl McpPlanRequiredValueScalar { + fn deserialize_kind<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value != "scalar" { + return Err(serde::de::Error::unknown_variant(&value, &["scalar"])); + } + ::deserialize(serde::de::value::StringDeserializer::< + D::Error, + >::new(value)) + } +} + +/// One enumerated non-secret value a transport choice needs before it can be applied. The permitted values are structurally required. /// ///
    /// @@ -8083,14 +9789,63 @@ pub struct HistoryRewindRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistorySkippedFileRestore { - /// Absolute path of the skipped file. - pub path: String, - /// Reason the file was not restored. - pub reason: HistoryFileRestoreSkipReason, +pub struct McpPlanRequiredValueEnum { + /// Where the value is applied when the server is launched. + pub category: McpPlanValueCategory, + /// Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. + #[serde(skip_serializing_if = "Option::is_none")] + pub default_value: Option, + /// Human-readable explanation from the card. Inert untrusted text. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Non-empty permitted value set. Inert untrusted data. + pub enum_values: Vec, + /// Whether the value may be supplied more than once. + pub is_repeated: bool, + /// Key the value is supplied under. Inert untrusted data. + pub key: String, + /// Discriminator: this required value uses a fixed enumeration. + #[serde(deserialize_with = "McpPlanRequiredValueEnum::deserialize_kind")] + pub kind: McpPlanRequiredValueEnumKind, + /// Whether the value must be present for the plan to be applicable. + pub required: bool, + /// Human-readable label from the card. Inert untrusted text. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Discriminator: the value must be one of `enumValues`. + #[serde(deserialize_with = "McpPlanRequiredValueEnum::deserialize_value_type")] + pub value_type: McpPlanEnumValueType, } -/// Structured outcome of a rewind request. +impl McpPlanRequiredValueEnum { + fn deserialize_kind<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value != "enum" { + return Err(serde::de::Error::unknown_variant(&value, &["enum"])); + } + ::deserialize( + serde::de::value::StringDeserializer::::new(value), + ) + } + + fn deserialize_value_type<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value != "enum" { + return Err(serde::de::Error::unknown_variant(&value, &["enum"])); + } + ::deserialize(serde::de::value::StringDeserializer::::new( + value, + )) + } +} + +/// A secret a transport choice needs, referenced by placeholder. No secret value ever appears in a plan, and the placeholder resolves against the keychain only when a plan is applied. /// ///
    /// @@ -8100,22 +9855,17 @@ pub struct HistorySkippedFileRestore { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryRewindResult { - /// Failure detail. Set only for the failure and partial-failure outcomes (`files-rolled-back`, `rollback-incomplete`, `truncation-failed`, `checkpoint-cleanup-failed`, `snapshot-prune-failed`); omitted for `success` and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`). - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// 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`. +pub struct McpPlanSecretPlaceholder { + /// Key the secret is supplied under. Inert untrusted data. + pub key: String, + /// The runtime-assigned `${secret:}` placeholder written into configuration in place of the value. + pub placeholder: String, + /// Human-readable label from the card. Inert untrusted text. #[serde(skip_serializing_if = "Option::is_none")] - pub events_removed: Option, - /// 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. - pub outcome: HistoryRewindOutcome, - /// 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. - pub restored_files: Vec, - /// 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. - pub skipped_files: Vec, + pub title: Option, } -/// Markdown summary of the conversation context (empty when not available). +/// An eligible local-package transport choice. Package identity is required and a remote endpoint cannot be represented. /// ///
    /// @@ -8125,12 +9875,42 @@ pub struct HistoryRewindResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistorySummarizeForHandoffResult { - /// 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. - pub summary: String, +pub struct McpPlanTransportChoicePackage { + /// Stable identifier for this choice within the plan, used to select it when the plan is applied. + pub choice_id: String, + /// Discriminator: this choice runs a local package + #[serde(deserialize_with = "McpPlanTransportChoicePackage::deserialize_install_method")] + pub install_method: McpPlanPackageInstallMethod, + /// Package identifier. Inert untrusted data. + pub package_identifier: String, + /// Packaging ecosystem, for example `oci` or `npm`. + pub package_type: String, + /// Typed values this choice requires, excluding secrets. + pub required_values: Vec, + /// Secrets this choice requires, referenced by placeholder only. + pub secret_placeholders: Vec, + /// Local process transport this package choice would use. + pub transport: McpPlanPackageTransport, } -/// Identifier of the event to truncate to; this event and all later events are removed. +impl McpPlanTransportChoicePackage { + fn deserialize_install_method<'de, D>( + deserializer: D, + ) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value != "package" { + return Err(serde::de::Error::unknown_variant(&value, &["package"])); + } + ::deserialize( + serde::de::value::StringDeserializer::::new(value), + ) + } +} + +/// An eligible remote-endpoint transport choice. The endpoint is required and package identity cannot be represented. /// ///
    /// @@ -8140,12 +9920,40 @@ pub struct HistorySummarizeForHandoffResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryTruncateRequest { - /// Event ID to truncate to. This event and all events after it are removed from the session. - pub event_id: String, +pub struct McpPlanTransportChoiceRemote { + /// Stable identifier for this choice within the plan, used to select it when the plan is applied. + pub choice_id: String, + /// Endpoint URL. Inert untrusted data. + pub endpoint: String, + /// Discriminator: this choice connects to a remote endpoint + #[serde(deserialize_with = "McpPlanTransportChoiceRemote::deserialize_install_method")] + pub install_method: McpPlanRemoteInstallMethod, + /// Typed values this choice requires, excluding secrets. + pub required_values: Vec, + /// Secrets this choice requires, referenced by placeholder only. + pub secret_placeholders: Vec, + /// Endpoint transport this remote choice would use. + pub transport: McpPlanRemoteTransport, } -/// Number of events that were removed by the truncation. +impl McpPlanTransportChoiceRemote { + fn deserialize_install_method<'de, D>( + deserializer: D, + ) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value != "remote" { + return Err(serde::de::Error::unknown_variant(&value, &["remote"])); + } + ::deserialize( + serde::de::value::StringDeserializer::::new(value), + ) + } +} + +/// Where a plan would be written. /// ///
    /// @@ -8155,35 +9963,105 @@ pub struct HistoryTruncateRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HistoryTruncateResult { - /// Failure detail when checkpointCleanupFailed is true. +pub struct McpPlanTarget { + /// Configuration key the server would be recorded under within that scope. + pub config_key: String, + /// Configuration scope the plan targets. + pub scope: McpPlanScope, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationReviewInstall { + /// Exact reviewed installation action. + pub action: McpInstallationReviewInstallAction, + /// Catalogue identity retained from the bound candidate when available. #[serde(skip_serializing_if = "Option::is_none")] - pub checkpoint_cleanup_error: Option, - /// 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. + pub catalogue: Option, + /// Original catalogue trust metadata, not a verification claim. #[serde(skip_serializing_if = "Option::is_none")] - pub checkpoint_cleanup_failed: Option, - /// Number of events that were removed - pub events_removed: i64, + pub catalogue_trust: Option, + /// The configuration change for the selected alternative only. + pub configuration_change: McpPlanConfigurationChange, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_configuration: Option, + /// Identity from the retained plan, not caller display text. + pub identity: McpPlanResourceIdentity, + /// Non-secret values supplied for this selected alternative. + pub inputs: Vec, + /// Policy decision bound to this plan. + pub policy: McpPlanPolicyResult, + /// Original source identity and content commitment. + pub provenance: McpPlanProvenance, + /// Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. + pub secret_storage: McpInstallationSecretStorage, + /// Only the selected alternative is applied. + pub selected_choice: McpPlanTransportChoice, + /// Exact reviewed placeholders supplied separately. Never secret values. + pub supplied_secrets: Vec, + /// Exact reviewed user-scope destination. + pub target: McpPlanTarget, } -/// Runtime-owned wire payload for a server-to-client hook callback invocation. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct HookInvokeRequest { - pub hook_type: HookType, - pub input: serde_json::Value, - pub session_id: SessionId, +pub struct McpInstallationReviewUninstall { + /// Exact reviewed installation action. + pub action: McpInstallationReviewUninstallAction, + /// Identity from the installed receipt. + pub identity: McpPlanResourceIdentity, + /// Receipt-owned installation being removed. + pub installation_id: String, + /// Exact planner-owned secret slots to remove, excluding shared OAuth grants. + pub owned_secret_count: i64, + /// Current removal policy, independent of permission to activate the server. + pub policy: McpPlanPolicyResult, + /// Shared profile authentication is deliberately retained, not pending cleanup. + pub preserves_shared_authentication: bool, + /// Source identity and content commitment retained by the installed receipt. + pub provenance: McpPlanProvenance, + /// Whether uninstall restores a protected pre-install configuration. + pub restores_previous_configuration: bool, + /// Exact destination, checked for intervening changes before mutation. + pub target: McpPlanTarget, } -/// Optional output returned by an SDK callback hook. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallationReviewMcp { + /// Reviewed resource discriminator. + pub resource: InstallationReviewMcpResource, + /// The exact MCP action and its reviewed changes. + pub review: McpInstallationReview, +} + +/// One reviewed Skill file. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct HookInvokeResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub output: Option, +pub struct SkillInstallationFileReview { + /// SHA-256 digest of the exact file bytes. + pub digest: String, + /// Whether the file is installed with executable permissions. + pub executable: bool, + /// Declared media type for the file. + pub media_type: String, + /// Relative file path within the Skill root. + pub path: String, + /// Exact reviewed file size in bytes. + pub size_bytes: i64, } -/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +/// Source identity retained from Agent Finder and the pinned GitHub descriptor. /// ///
    /// @@ -8193,16 +10071,26 @@ pub(crate) struct HookInvokeResponse { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HooksDiscoverRequest { - /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude_host_hooks: Option, - /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. - #[serde(skip_serializing_if = "Option::is_none")] - pub project_paths: Option>, +pub struct SkillInstallationSource { + /// Digest of the descriptor's bundle manifest. + pub bundle_digest: String, + /// Agent Finder materialisation revision identifier. + pub catalog_revision_id: String, + /// Digest of the canonical materialisation descriptor. + pub descriptor_digest: String, + /// Repository full name, for example owner/name. + pub repository: String, + /// GitHub repository database identifier. + pub repository_id: String, + /// Agent Finder resource identifier. + pub resource_id: String, + /// Pinned Git commit revision. + pub revision: String, + /// Root path within the pinned repository. + pub root: String, } -/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. +/// A user-facing personal Skill installation location without absolute host paths. /// ///
    /// @@ -8212,16 +10100,49 @@ pub struct HooksDiscoverRequest { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HooksDiscoverResult { - /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. - pub errors: Vec, - /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. - pub hooks: Vec, - /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. - pub warnings: Vec, +pub struct SkillInstallationLocation { + /// Diagnostics-only absolute host path. Hosts must not display it by default. + #[serde(skip_serializing_if = "Option::is_none")] + pub diagnostics_absolute_path: Option, + /// Safe display label, for example ~/.copilot/skills/run-checks. + pub display_label: String, + /// Path relative to the Copilot home. + pub relative_path: String, + /// Installation scope. Agent Finder Skills are installed in the user's personal Copilot home. + pub scope: SkillInstallationScope, } -/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. +/// Review for installing a verified Skill. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationReviewInstall { + /// Exact reviewed Skill installation action. + pub action: SkillInstallationReviewInstallAction, + /// Catalogue identity retained from the bound candidate before consent. + pub catalogue: InstallationCatalogueIdentity, + /// Skill description from SKILL.md when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Complete verified SKILL.md content. Planning refuses with review-too-large + /// when this exceeds 262144 UTF-8 bytes; it is never truncated. + pub entrypoint_content: String, + /// Relative path of the verified Skill entrypoint. + pub entrypoint_path: String, + /// Reviewed files and digests. + pub files: Vec, + /// Installing never grants immediate use; the Skill is written disabled. + pub installs_disabled: bool, + /// Skill invocation name from SKILL.md. + pub name: String, + /// Exact verified source identity. + pub source: SkillInstallationSource, + /// Exact user-scope target location without an absolute host path. + pub target: SkillInstallationLocation, + /// Total reviewed payload size in bytes. + pub total_bytes: i64, +} + +/// Durable verified Skill ownership summary. /// ///
    /// @@ -8231,31 +10152,86 @@ pub struct HooksDiscoverResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct InstalledPlugin { - /// Path where the plugin is cached locally - #[serde(rename = "cache_path", skip_serializing_if = "Option::is_none")] - pub cache_path: Option, - /// Whether the plugin is currently enabled - pub enabled: bool, - /// Installation timestamp - #[serde(rename = "installed_at")] +pub struct SkillInstallationSummary { + /// Catalogue identity retained at install time. + pub catalogue: InstallationCatalogueIdentity, + /// Persisted enablement requested for this installation. + pub configured_enabled: bool, + /// Exact durable installation receipt identity. + pub installation_id: String, + /// ISO 8601 wall-clock installation time. pub installed_at: String, - /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. - #[serde(rename = "installed_from", skip_serializing_if = "Option::is_none")] - pub installed_from: Option, - /// Marketplace the plugin came from (empty string for direct repo installs) - pub marketplace: String, - /// Plugin name + /// Skill invocation name. pub name: String, - /// Source for direct repo installs (when marketplace is empty) - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. - #[serde(rename = "source_sha", skip_serializing_if = "Option::is_none")] - pub source_sha: Option, - /// Version installed (if available) + /// Operation that installed this Skill. + pub operation_id: String, + /// Ownership state observed from files and receipts. + pub ownership_state: SkillInstallationOwnershipState, + /// Bound-session load observation. + pub session_state: SkillInstallationSessionState, + /// Exact retained verified source identity. + pub source: SkillInstallationSource, + /// User-facing installation location without an absolute host path. + pub target: SkillInstallationLocation, +} + +/// Review for uninstalling an owned verified Skill. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationReviewUninstall { + /// Exact reviewed Skill installation action. + pub action: SkillInstallationReviewUninstallAction, + /// Catalogue identity retained at install time. + pub catalogue: InstallationCatalogueIdentity, + /// Files recorded by the installation receipt. + pub files: Vec, + /// Whether current files differ from the receipt. Apply refuses drift. + pub files_modified: bool, + /// Owned installation being removed. + pub installation: SkillInstallationSummary, + /// Total receipt-owned payload size in bytes. + pub total_bytes: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallationReviewSkill { + /// Reviewed resource discriminator. + pub resource: InstallationReviewSkillResource, + /// The exact verified Skill action and its reviewed files. + pub review: SkillInstallationReview, +} + +/// One connection-owned, expiring request for a trusted host's explicit user decision. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallationConfirmationRequest { + /// Opaque one-use challenge. Return unchanged; never log or persist. + pub confirmation_id: String, + /// Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. + pub expires_at: String, + /// Random identifier of this installation operation, not a plan handle. + pub operation_id: String, + /// 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. #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, + pub policy_session_id: Option, + /// Resource-specific review to present before collecting the user's decision. + pub review: InstallationReview, + /// Opaque commitment to the exact review and inputs. Return unchanged; never log. + pub review_fingerprint: String, +} + +/// A response is meaningful only on the connection and request that issued its challenge. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallationConfirmationResponse { + /// Exact challenge from the request. + pub confirmation_id: String, + /// Fresh explicit user decision. There is no default. + pub decision: InstallationDecision, + /// Exact review commitment from the request. + pub review_fingerprint: String, } /// Information about an installed plugin tracked in global state. @@ -8887,7 +10863,7 @@ pub struct ManagedMcpServerConfig { pub url: String, } -/// Validated device-managed settings discovered before a session exists. +/// Lock state and provenance of one managed setting. /// ///
    /// @@ -8897,16 +10873,103 @@ pub struct ManagedMcpServerConfig { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ManagedSettingsReadResult { - /// Discovery or validation error text when managed settings could not be read safely. +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 error_message: Option, - /// Validated, canonical managed-settings JSON. Omitted when no managed settings were discovered or when discovered settings failed validation. + 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_json: Option, + 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, } -/// 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. +/// 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. /// ///
    /// @@ -8946,6 +11009,174 @@ pub struct ManagedSettingsResolvedData { 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. +/// +///
    +/// +/// **Experimental.** This type 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 ManagedSettingsReadResult { + /// Discovery or validation error text when managed settings could not be read safely. + #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, + /// Validated, canonical managed-settings JSON. Omitted when no managed settings were discovered or when discovered settings failed validation. + #[serde(skip_serializing_if = "Option::is_none")] + pub settings_json: Option, +} + +/// Optional opaque account selection or GitHub token whose managed settings are resolved. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub 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_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 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 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 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, + /// 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. /// ///
    @@ -9106,6 +11337,44 @@ pub struct McpAllowedServer { pub redacted_note: Option, } +/// Applies exactly one previously prepared operation on its original connection. +/// +///
    +/// +/// **Experimental.** This type 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 McpApplyInstallRequest { + /// Capabilities required by the original prepared operation. + pub contract: CatalogClientContract, + /// Runtime-issued ID already returned by prepareInstall, never reused or rebound. + pub operation_id: String, + /// Same existing attached or privately borrowed session as preparation. + pub policy_session_id: String, +} + +/// One-use application of the exact retained removal plan. +/// +///
    +/// +/// **Experimental.** This type 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 McpApplyUninstallRequest { + /// Required authenticated bound installation capabilities. + pub contract: CatalogClientContract, + /// Opaque original removal plan, consumed once. + pub plan_handle: String, + /// Same existing selected session as removal preparation. + pub policy_session_id: String, +} + /// MCP server, tool name, and arguments to invoke from an MCP App view. /// ///
    @@ -9516,6 +11785,71 @@ pub struct McpConfigUpdateRequest { pub name: String, } +/// Observational state for a matching already materialized MCP server. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerState { + /// Observed connection error, when the materialized server failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Observed connection status. This is not a configuration or readiness guarantee. + pub status: McpServerStatus, +} + +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServer { + /// Human-readable display name supplied by configuration. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Whether this configured server is enabled after session configuration and policy filtering. + pub enabled: bool, + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + #[serde(skip_serializing_if = "Option::is_none")] + pub live: Option, + /// Server name (config key) + pub name: String, + /// Configuration provenance: user, workspace, plugin, builtin, or managed. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// Plugin name that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin: Option, + /// Plugin version that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin_version: Option, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerList { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Credential-free authentication identity used to configure GitHub MCP. /// ///
    @@ -9562,6 +11896,44 @@ pub struct McpDisableRequest { pub server_name: String, } +/// Extensible [`McpDisableRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpDisableOptions::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 McpDisableOptions { + #[serde(flatten)] + legacy: McpDisableRequest, + #[serde(skip_serializing_if = "Option::is_none")] + expected_installation_id: Option, +} + +impl McpDisableOptions { + /// Creates options with the required inputs. + pub fn new(server_name: impl Into) -> Self { + Self { + legacy: McpDisableRequest { + server_name: server_name.into(), + }, + expected_installation_id: None, + } + } + + /// Required for an owned installation; omission preserves only manual-server behaviour. + pub fn expected_installation_id(mut self, value: impl Into) -> Self { + self.expected_installation_id = Some(value.into()); + self + } +} + /// Optional working directory used as context for MCP server discovery. /// ///
    @@ -9611,6 +11983,44 @@ pub struct McpEnableRequest { pub server_name: String, } +/// Extensible [`McpEnableRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpEnableOptions::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 McpEnableOptions { + #[serde(flatten)] + legacy: McpEnableRequest, + #[serde(skip_serializing_if = "Option::is_none")] + expected_installation_id: Option, +} + +impl McpEnableOptions { + /// Creates options with the required inputs. + pub fn new(server_name: impl Into) -> Self { + Self { + legacy: McpEnableRequest { + server_name: server_name.into(), + }, + expected_installation_id: None, + } + } + + /// Exact receipt identity for explicit owned activation in this session. + pub fn expected_installation_id(mut self, value: impl Into) -> Self { + self.expected_installation_id = Some(value.into()); + self + } +} + /// 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. /// ///
    @@ -9806,7 +12216,15 @@ pub struct McpHostState { pub pending_connections: Vec, } -/// The configuration-change alternative for the transportChoices entry at the same index. Only the selected alternative is applied; entries are not cumulative. The payload stays behind the runtime boundary. +/// Already-confirmed durable work must be reconciled before new mutations or inventory. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementOutcomeRecoveryRequired { + /// Installation management outcome discriminator. + pub kind: McpInstallationManagementOutcomeRecoveryRequiredKind, +} + +/// Inert, runtime-owned admission. The operation ID is known before confirmation or effects. /// ///
    /// @@ -9816,20 +12234,23 @@ pub struct McpHostState { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanConfigurationChange { - /// Names of the configuration fields the change would set, without their values. - pub changed_fields: Vec, - /// Configuration key the change applies to. - pub config_key: String, - /// Whether the change would create a new entry or modify an existing one. - pub operation: McpPlanConfigurationOperation, - /// Scope the change would be written to. - pub scope: McpPlanScope, - /// Secret placeholders the written configuration would reference. The constrained placeholder type cannot carry a literal secret value. - pub secret_references: Vec, +pub struct McpPreparedInstall { + /// Original plan expiry in Unix epoch milliseconds; preparation does not extend it. + pub expires_at_epoch_ms: i64, + /// Original connection-owned operation, known before the first confirmation callback. + pub operation_id: String, } -/// Normalised identity of the MCP server a plan targets, independent of how the card spelled it. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementOutcomeInstallPrepared { + /// Installation management outcome discriminator. + pub kind: McpInstallationManagementOutcomeInstallPreparedKind, + /// Known original operation identity, returned before callback or effects. + pub operation: McpPreparedInstall, +} + +/// Durable configuration ownership is distinct from session-specific usability. /// ///
    /// @@ -9839,20 +12260,44 @@ pub struct McpPlanConfigurationChange { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanResourceIdentity { - /// Canonical, normalised name of the server, for example `io.github.owner/server`. - pub canonical_name: String, - /// Registry identifier of the server, when it came from a registry. +pub struct McpInstallationSummary { + /// Catalogue identity retained from the installed plan when available. #[serde(skip_serializing_if = "Option::is_none")] - pub registry_id: Option, - /// Local configuration key the server would be recorded under. - pub server_name: String, - /// Version advertised by the card, when it declares one. + pub catalogue: Option, + /// Exact alternative retained in the installing receipt. + pub choice_id: String, + /// Identity retained from the validated original plan. + pub identity: McpPlanResourceIdentity, + /// Exact durable installation receipt identity. + pub installation_id: String, + /// ISO 8601 wall-clock installation time when available. #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, + pub installed_at: Option, + /// Original installing operation, not a fresh management operation. + pub operation_id: String, + /// Ownership or setup state, never inferred proof of tool usability. + pub state: McpInstallationState, } -/// Outcome of evaluating the planned server against registry and enterprise policy. Evaluation is read-only. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementOutcomeListed { + /// Owned receipts visible to the selected account and host. + pub installations: Vec, + /// Installation management outcome discriminator. + pub kind: McpInstallationManagementOutcomeListedKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementOutcomeRecovered { + /// Freshly inspected receipts after successful durable reconciliation. + pub installations: Vec, + /// Installation management outcome discriminator. + pub kind: McpInstallationManagementOutcomeRecoveredKind, +} + +/// Exact inert removal plan. No configuration or credentials have changed. /// ///
    /// @@ -9862,17 +12307,212 @@ pub struct McpPlanResourceIdentity { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanPolicyResult { - /// What policy decided for this server. - pub decision: McpPlanPolicyDecision, - /// Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. +pub struct McpUninstallPlan { + /// Original wall-clock expiry in milliseconds. Applying never renews it. + pub expires_at_epoch_ms: i64, + /// Original owned receipt being removed. + pub installation: McpInstallationSummary, + /// The original operation, inspectable and cancellable on this same connection. + pub operation_id: String, + /// Exact configured input slots owned by this installation, never shared OAuth tokens. + pub owned_secret_count: i64, + /// One-use original connection and authority-bound plan handle. + pub plan_handle: String, + /// Shared authentication is deliberately retained; revocation is a separate action. + pub preserves_shared_authentication: bool, + /// Whether removal restores a protected earlier configuration. + pub restores_previous_configuration: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementOutcomeUninstallPlanned { + /// Installation management outcome discriminator. + pub kind: McpInstallationManagementOutcomeUninstallPlannedKind, + /// Original owned removal plan and operation. + pub plan: McpUninstallPlan, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOperationStatusPreparing { + /// Whether cancellation has been requested, not proof that a write was undone. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + /// Original operation progress discriminator. + pub phase: McpInstallationOperationStatusPreparingPhase, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOperationStatusPrepared { + /// Whether the inert prepared operation was asked to cancel. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + /// Original operation progress discriminator. + pub phase: McpInstallationOperationStatusPreparedPhase, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOperationStatusAwaitingConfirmation { + /// Whether the pending human callback was asked to cancel. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + /// Original operation progress discriminator. + pub phase: McpInstallationOperationStatusAwaitingConfirmationPhase, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOperationStatusRevalidating { + /// Whether source or authority revalidation was asked to cancel. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + /// Original operation progress discriminator. + pub phase: McpInstallationOperationStatusRevalidatingPhase, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOperationStatusApplying { + /// Whether applying was asked to cancel; already-started effects retain their lease. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + /// Original operation progress discriminator. + pub phase: McpInstallationOperationStatusApplyingPhase, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOutcomeInstalled { + /// Durable installation succeeded but final transaction cleanup remains. + pub cleanup_pending: bool, + /// Receipt identity produced by the confirmed transaction. + pub installation: McpInstallationSummary, + /// Terminal installation outcome discriminator. + pub kind: McpInstallationOutcomeInstalledKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOutcomeUninstalled { + /// Durable removal succeeded but final cleanup remains. + pub cleanup_pending: bool, + /// Exact removed receipt identity. + pub installation_id: String, + /// Terminal installation outcome discriminator. + pub kind: McpInstallationOutcomeUninstalledKind, + /// Original removal operation. + pub operation_id: String, + /// Any grants in the incumbent shared OAuth store remain unowned and retained. + pub preserved_shared_authentication: bool, + /// Exact owned input slots removed, excluding shared OAuth credentials. + pub removed_owned_secrets: i64, + /// Whether protected pre-install configuration was restored. + pub restored_previous_configuration: bool, +} + +/// The durable transaction was aborted or fully compensated. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOutcomeRolledBack { + /// Terminal installation outcome discriminator. + pub kind: McpInstallationOutcomeRolledBackKind, + /// Original connection-owned operation. + pub operation_id: String, + /// Cause of the fully aborted or compensated operation. + pub reason: McpInstallationFailureReason, +} + +/// A write may have completed. Recover and inspect durable state before retrying. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOutcomeRecoveryRequired { + /// Terminal installation outcome discriminator. + pub kind: McpInstallationOutcomeRecoveryRequiredKind, + /// Operation whose durable result must be recovered and inspected. + pub operation_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOutcomeDeclined { + /// Terminal installation outcome discriminator. + pub kind: McpInstallationOutcomeDeclinedKind, + /// Original operation explicitly declined by the user. + pub operation_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOutcomeCancelled { + /// Terminal installation outcome discriminator. + pub kind: McpInstallationOutcomeCancelledKind, + /// Original operation cancelled before a terminal application result. + pub operation_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOutcomeRefused { + /// Terminal installation outcome discriminator. + pub kind: McpInstallationOutcomeRefusedKind, + /// Present once an operation has been allocated; never a plan handle. #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Which authority produced the decision. - pub source: McpPlanPolicySource, + pub operation_id: Option, + /// Specific bounded refusal, never a success-shaped fallback. + pub reason: McpInstallationFailureReason, } -/// Provenance of the exact validated JSON MCP card content bound privately to a completed plan and its opaque handle. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationOperationStatusCompleted { + /// Whether cancellation was requested before the terminal result. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + /// Immutable terminal receipt. + pub outcome: McpInstallationOutcome, + /// Original operation progress discriminator. + pub phase: McpInstallationOperationStatusCompletedPhase, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementOutcomeOperation { + /// Installation management outcome discriminator. + pub kind: McpInstallationManagementOutcomeOperationKind, + /// Original-connection operation snapshot. + pub operation: McpInstallationOperationStatus, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementOutcomeRefused { + /// Installation management outcome discriminator. + pub kind: McpInstallationManagementOutcomeRefusedKind, + /// Specific bounded refusal. + pub reason: McpInstallationFailureReason, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationManagementResultOutcome { + /// A handled management outcome with an honoured contract. + pub kind: McpInstallationManagementResultOutcomeKind, + /// Capabilities actually honoured for this request. + pub negotiated: CatalogNegotiatedContract, + /// Observed management outcome. + pub outcome: McpInstallationManagementOutcome, +} + +/// Existing-operation control. A new session selector is deliberately not accepted. /// ///
    /// @@ -9882,18 +12522,25 @@ pub struct McpPlanPolicyResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanProvenance { - /// Authority associated with the validated card, without path, query, or credentials. Inert untrusted data. - pub authority: String, - /// Semantic digest of the exact validated JSON content bound to the plan handle. - pub card_digest: CardDigest, - /// JSON MCP media type the validated card was interpreted as. - pub media_type: McpServerCardMediaType, - /// ISO 8601 timestamp at which the runtime completed strict parsing and schema validation of the card content. - pub validated_at: String, +pub struct McpInstallationOperationRequest { + /// Original installation wire capability; new-work authentication is not reacquired. + pub contract: CatalogClientContract, + /// Exact runtime-issued operation ID on the original connection. + pub operation_id: String, } -/// Where a plan would be written. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpInstallationResultOutcome { + /// A handled operation outcome with an honoured contract. + pub kind: McpInstallationResultOutcomeKind, + /// Capabilities actually honoured for this request. + pub negotiated: CatalogNegotiatedContract, + /// Terminal result of the original operation. + pub outcome: McpInstallationOutcome, +} + +/// A request-local value for one exact reviewed placeholder. Never logged or persisted in a plan. /// ///
    /// @@ -9903,11 +12550,28 @@ pub struct McpPlanProvenance { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanTarget { - /// Configuration key the server would be recorded under within that scope. - pub config_key: String, - /// Configuration scope the plan targets. - pub scope: McpPlanScope, +pub struct McpInstallationSecret { + /// Exact placeholder from the selected choice, not a caller-chosen backend identifier. + pub placeholder: String, + /// Fresh explicit secret value. It is omitted from confirmation reviews and telemetry. + pub value: String, +} + +/// New-work inventory or recovery request under an explicitly selected existing 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 McpInstallationsRequest { + /// Required authenticated bound installation contract. + pub contract: CatalogClientContract, + /// Existing selected local session on this connection. + pub policy_session_id: String, } /// A normalised, inert description of what installing an MCP server would involve. Carries no raw card, no install specification, and no secret value. @@ -9943,7 +12607,7 @@ pub struct McpInstallPlan { /// Configuration scope and key the plan would write to. pub target: McpPlanTarget, /// Every eligible transport, so a host can present an explicit choice. A completed plan always has at least one; when none is eligible, planning returns `CatalogUnavailableTransportError` instead. - pub transport_choices: Vec, + pub transport_choices: Vec, } /// Server name to check running status for. @@ -10065,6 +12729,55 @@ pub struct McpOauthAuthenticationStateChangedRequest { pub server_name: Option, } +/// Targets only the original prepared/applying owned login on this exact session requester. +/// +///
    +/// +/// **Experimental.** This type 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 McpOauthCancelLoginRequest { + /// The same authoritative installation identity supplied during preparation. + pub expected_installation_id: String, + /// Runtime-issued login handle known before the effectful login request begins. + pub login_id: String, +} + +/// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. +/// +///
    +/// +/// **Experimental.** This type 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 McpOauthCancelLoginResult { + /// True after cancellation settles, false when the original login already connected successfully. + 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 { @@ -10119,7 +12832,7 @@ pub struct McpOauthHandlePendingResult { pub success: bool, } -/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. +/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. /// ///
    /// @@ -10155,6 +12868,112 @@ pub struct McpOauthLoginRequest { pub server_name: String, } +/// Extensible [`McpOauthLoginRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpOauthLoginOptions::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 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, +} + +impl McpOauthLoginOptions { + /// Creates options with the required inputs. + pub fn new(server_name: impl Into) -> Self { + Self { + legacy: McpOauthLoginRequest { + server_name: server_name.into(), + force_reauth: None, + client_name: None, + callback_success_message: None, + client_id: None, + client_secret: None, + public_client: None, + grant_type: None, + }, + redirect_uri: None, + login_id: None, + expected_installation_id: None, + } + } + + /// 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. + pub fn force_reauth(mut self, value: bool) -> Self { + self.legacy.force_reauth = Some(value); + self + } + + /// 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. + pub fn client_name(mut self, value: impl Into) -> Self { + self.legacy.client_name = Some(value.into()); + self + } + + /// 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. + pub fn callback_success_message(mut self, value: impl Into) -> Self { + self.legacy.callback_success_message = Some(value.into()); + self + } + + /// Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. + pub fn client_id(mut self, value: impl Into) -> Self { + self.legacy.client_id = Some(value.into()); + self + } + + /// 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. + pub fn client_secret(mut self, value: impl Into) -> Self { + self.legacy.client_secret = Some(value.into()); + self + } + + /// 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. + pub fn public_client(mut self, value: bool) -> Self { + self.legacy.public_client = Some(value); + self + } + + /// Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. + pub fn grant_type(mut self, value: McpOauthLoginGrantType) -> Self { + self.legacy.grant_type = Some(value); + 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 { + self.login_id = Some(value.into()); + self + } + + /// Exact owned receipt identity. Owned login never uses an implicit helper session. + pub fn expected_installation_id(mut self, value: impl Into) -> Self { + self.expected_installation_id = Some(value.into()); + self + } +} + /// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. /// ///
    @@ -10166,9 +12985,61 @@ pub struct McpOauthLoginRequest { #[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. + #[serde(skip_serializing_if = "Option::is_none")] + pub login_id: Option, + /// Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Effect-free preparation bound to the existing local session, requester and installation, with frozen 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 McpOauthPrepareLoginRequest { + /// Text shown on the loopback callback page after successful authorisation. + #[serde(skip_serializing_if = "Option::is_none")] + pub callback_success_message: Option, + /// Display name used by the incumbent OAuth client-registration flow. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + /// Exact installation identity from owned inventory, never a server-name alias. + pub expected_installation_id: String, + /// Request a new authorisation rather than accepting a usable cached grant. + #[serde(skip_serializing_if = "Option::is_none")] + pub force_reauth: Option, + /// Name recorded by the authoritative owned installation receipt. + pub server_name: String, +} + +/// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. +/// +///
    +/// +/// **Experimental.** This type 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 McpOauthPrepareLoginResult { + /// Original expiry, not extended by consumption, retries or cancellation. + pub expires_at: String, + /// Retain with the original requester and use for one login or cancellation. + pub login_id: String, } /// Remote MCP server name for a passive OAuth status probe. @@ -10186,6 +13057,44 @@ pub struct McpOauthProbeRequest { pub server_name: String, } +/// Extensible [`McpOauthProbeRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpOauthProbeOptions::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 McpOauthProbeOptions { + #[serde(flatten)] + legacy: McpOauthProbeRequest, + #[serde(skip_serializing_if = "Option::is_none")] + expected_installation_id: Option, +} + +impl McpOauthProbeOptions { + /// Creates options with the required inputs. + pub fn new(server_name: impl Into) -> Self { + Self { + legacy: McpOauthProbeRequest { + server_name: server_name.into(), + }, + expected_installation_id: None, + } + } + + /// Exact owned receipt identity; probing never activates a dormant installation. + pub fn expected_installation_id(mut self, value: impl Into) -> Self { + self.expected_installation_id = Some(value.into()); + self + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpOauthProbeResultNoAuthRequired { @@ -10439,7 +13348,10 @@ pub struct McpPlanInstallRequest { pub source: McpPlanInstallSource, } -/// One non-secret scalar value a transport choice needs before it can be applied. +/// Extensible [`McpPlanInstallRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpPlanInstallOptions::new`] arguments; optional inputs have fluent setters. +/// Input-only: it serialises to the flat wire request and is not deserialisable. /// ///
    /// @@ -10447,49 +13359,42 @@ pub struct McpPlanInstallRequest { /// and may change or be removed in future SDK or CLI releases. /// ///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanRequiredValueScalar { - /// Where the value is applied when the server is launched. - pub category: McpPlanValueCategory, - /// Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. +pub struct McpPlanInstallOptions { + #[serde(flatten)] + legacy: McpPlanInstallRequest, #[serde(skip_serializing_if = "Option::is_none")] - pub default_value: Option, - /// Human-readable explanation from the card. Inert untrusted text. - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// Whether the value may be supplied more than once. - pub is_repeated: bool, - /// Key the value is supplied under. Inert untrusted data. - pub key: String, - /// Discriminator: this required value uses a scalar type. - #[serde(deserialize_with = "McpPlanRequiredValueScalar::deserialize_kind")] - pub kind: McpPlanRequiredValueScalarKind, - /// Whether the value must be present for the plan to be applicable. - pub required: bool, - /// Human-readable label from the card. Inert untrusted text. - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - /// Scalar type the value must conform to. - pub value_type: McpPlanScalarValueType, + policy_session_id: Option, } -impl McpPlanRequiredValueScalar { - fn deserialize_kind<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value != "scalar" { - return Err(serde::de::Error::unknown_variant(&value, &["scalar"])); +impl McpPlanInstallOptions { + /// Creates options with the required inputs. + pub fn new(contract: CatalogClientContract, source: McpPlanInstallSource) -> Self { + Self { + legacy: McpPlanInstallRequest { + contract, + source, + scope: None, + }, + policy_session_id: None, } - ::deserialize(serde::de::value::StringDeserializer::< - D::Error, - >::new(value)) + } + + /// Configuration scope the plan targets. Defaults to user scope when omitted. + pub fn scope(mut self, value: McpPlanScope) -> Self { + self.legacy.scope = Some(value); + self + } + + /// The same existing attached session that owns the original catalogue candidate. + pub fn policy_session_id(mut self, value: impl Into) -> Self { + self.policy_session_id = Some(value.into()); + self } } -/// One enumerated non-secret value a transport choice needs before it can be applied. The permitted values are structurally required. +/// Read-only preparation of one owned removal under fresh selected-session authority. /// ///
    /// @@ -10499,63 +13404,101 @@ impl McpPlanRequiredValueScalar { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanRequiredValueEnum { - /// Where the value is applied when the server is launched. - pub category: McpPlanValueCategory, - /// Default supplied by the card, when the value can be resolved without input. Presence is the authoritative indication that a default exists. Inert untrusted data. +pub struct McpPlanUninstallRequest { + /// Required authenticated bound installation capabilities. + pub contract: CatalogClientContract, + /// Exact receipt to inspect, not a server-name guess. + pub installation_id: String, + /// Existing selected session on the original connection. + pub policy_session_id: String, +} + +/// Side-effect-free preparation of one original bound remote MCP choice. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPrepareInstallRequest { + /// Exact selected alternative from that plan. + pub choice_id: String, + /// Required bound catalogue and confirmed remote installation capabilities. + pub contract: CatalogClientContract, + /// Declared non-secret values. Non-empty only when the caller requires + /// `mcp-configured-remote-installation`; omitted values use the card default. + pub inputs: Vec, + /// Original single-use bound plan, never a client-authored configuration. + pub plan_handle: String, + /// An existing local session attached to this connection, not permission to attach one. + pub policy_session_id: String, + /// One entry per declared secret placeholder of the selected choice. Non-empty + /// only when the caller requires `mcp-configured-remote-installation`. + pub secrets: Vec, + /// The trusted host presents this choice alongside the exact secret placeholders. + pub secret_storage: McpInstallationSecretStorage, + /// The exact original source, used transiently only after confirmation. + 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 default_value: Option, - /// Human-readable explanation from the card. Inert untrusted text. + pub additional_properties: Option>, + /// Description of the argument #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - /// Non-empty permitted value set. Inert untrusted data. - pub enum_values: Vec, - /// Whether the value may be supplied more than once. - pub is_repeated: bool, - /// Key the value is supplied under. Inert untrusted data. - pub key: String, - /// Discriminator: this required value uses a fixed enumeration. - #[serde(deserialize_with = "McpPlanRequiredValueEnum::deserialize_kind")] - pub kind: McpPlanRequiredValueEnumKind, - /// Whether the value must be present for the plan to be applicable. - pub required: bool, - /// Human-readable label from the card. Inert untrusted text. + /// 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 title: Option, - /// Discriminator: the value must be one of `enumValues`. - #[serde(deserialize_with = "McpPlanRequiredValueEnum::deserialize_value_type")] - pub value_type: McpPlanEnumValueType, + pub required: Option, } -impl McpPlanRequiredValueEnum { - fn deserialize_kind<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value != "enum" { - return Err(serde::de::Error::unknown_variant(&value, &["enum"])); - } - ::deserialize( - serde::de::value::StringDeserializer::::new(value), - ) - } - - fn deserialize_value_type<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value != "enum" { - return Err(serde::de::Error::unknown_variant(&value, &["enum"])); - } - ::deserialize(serde::de::value::StringDeserializer::::new( - value, - )) - } +/// 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, } -/// A secret a transport choice needs, referenced by placeholder. No secret value ever appears in a plan, and the placeholder resolves against the keychain only when a plan is applied. +/// An MCP prompt descriptor. Server-provided non-standard fields are exposed under `additionalProperties`. /// ///
    /// @@ -10565,17 +13508,30 @@ impl McpPlanRequiredValueEnum { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanSecretPlaceholder { - /// Key the secret is supplied under. Inert untrusted data. - pub key: String, - /// The runtime-assigned `${secret:}` placeholder written into configuration in place of the value. - pub placeholder: String, - /// Human-readable label from the card. Inert untrusted text. +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 eligible local-package transport choice. Package identity is required and a remote endpoint cannot be represented. +/// An MCP prompt message with opaque JSON content preserved without flattening or content-type filtering. /// ///
    /// @@ -10585,42 +13541,40 @@ pub struct McpPlanSecretPlaceholder { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanTransportChoicePackage { - /// Stable identifier for this choice within the plan, used to select it when the plan is applied. - pub choice_id: String, - /// Discriminator: this choice runs a local package - #[serde(deserialize_with = "McpPlanTransportChoicePackage::deserialize_install_method")] - pub install_method: McpPlanPackageInstallMethod, - /// Package identifier. Inert untrusted data. - pub package_identifier: String, - /// Packaging ecosystem, for example `oci` or `npm`. - pub package_type: String, - /// Typed values this choice requires, excluding secrets. - pub required_values: Vec, - /// Secrets this choice requires, referenced by placeholder only. - pub secret_placeholders: Vec, - /// Local process transport this package choice would use. - pub transport: McpPlanPackageTransport, +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, } -impl McpPlanTransportChoicePackage { - fn deserialize_install_method<'de, D>( - deserializer: D, - ) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value != "package" { - return Err(serde::de::Error::unknown_variant(&value, &["package"])); - } - ::deserialize( - serde::de::value::StringDeserializer::::new(value), - ) - } +/// 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, } -/// An eligible remote-endpoint transport choice. The endpoint is required and package identity cannot be represented. +/// Prompt messages returned by the MCP server without sending them to the model. /// ///
    /// @@ -10630,37 +13584,60 @@ impl McpPlanTransportChoicePackage { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct McpPlanTransportChoiceRemote { - /// Stable identifier for this choice within the plan, used to select it when the plan is applied. - pub choice_id: String, - /// Endpoint URL. Inert untrusted data. - pub endpoint: String, - /// Discriminator: this choice connects to a remote endpoint - #[serde(deserialize_with = "McpPlanTransportChoiceRemote::deserialize_install_method")] - pub install_method: McpPlanRemoteInstallMethod, - /// Typed values this choice requires, excluding secrets. - pub required_values: Vec, - /// Secrets this choice requires, referenced by placeholder only. - pub secret_placeholders: Vec, - /// Endpoint transport this remote choice would use. - pub transport: McpPlanRemoteTransport, +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, } -impl McpPlanTransportChoiceRemote { - fn deserialize_install_method<'de, D>( - deserializer: D, - ) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value != "remote" { - return Err(serde::de::Error::unknown_variant(&value, &["remote"])); - } - ::deserialize( - serde::de::value::StringDeserializer::::new(value), - ) - } +/// 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. @@ -11037,6 +14014,51 @@ pub struct McpRestartServerRequest { pub server_name: String, } +/// Extensible [`McpRestartServerRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpRestartServerOptions::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 McpRestartServerOptions { + #[serde(flatten)] + legacy: McpRestartServerRequest, + #[serde(skip_serializing_if = "Option::is_none")] + expected_installation_id: Option, +} + +impl McpRestartServerOptions { + /// Creates options with the required inputs. + pub fn new(server_name: impl Into) -> Self { + Self { + legacy: McpRestartServerRequest { + server_name: server_name.into(), + config: None, + }, + expected_installation_id: None, + } + } + + /// 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). + pub fn config(mut self, value: serde_json::Value) -> Self { + self.legacy.config = Some(value); + self + } + + /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. + pub fn expected_installation_id(mut self, value: impl Into) -> Self { + self.expected_installation_id = Some(value.into()); + self + } +} + /// Per-field MCP telemetry-obfuscation policy. /// ///
    @@ -11075,6 +14097,21 @@ pub struct McpSamplingExecutionResult { pub result: Option, } +/// Owned installation that a listed MCP server's live configuration came from. +/// +///
    +/// +/// **Experimental.** This type 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 McpServerOwnership { + /// Stable installation identifier from the owned installation receipt. + pub installation_id: String, +} + /// MCP server status entry, including config source/plugin source and any connection error. /// ///
    @@ -11094,6 +14131,9 @@ pub struct McpServer { pub error: Option, /// Server name (config key) pub name: String, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub owned: Option, /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub server_metadata: Option, @@ -11108,6 +14148,9 @@ pub struct McpServer { pub source_plugin_version: Option, /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured pub status: McpServerStatus, + /// Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, } /// Authentication settings with optional redirect port configuration. @@ -11459,6 +14502,51 @@ pub struct McpStartServerRequest { pub server_name: String, } +/// Extensible [`McpStartServerRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpStartServerOptions::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 McpStartServerOptions { + #[serde(flatten)] + legacy: McpStartServerRequest, + #[serde(skip_serializing_if = "Option::is_none")] + expected_installation_id: Option, +} + +impl McpStartServerOptions { + /// Creates options with the required inputs. + pub fn new(server_name: impl Into) -> Self { + Self { + legacy: McpStartServerRequest { + server_name: server_name.into(), + config: None, + }, + expected_installation_id: None, + } + } + + /// MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). + pub fn config(mut self, value: serde_json::Value) -> Self { + self.legacy.config = Some(value); + self + } + + /// Exact receipt identity for explicit owned activation in this session. + pub fn expected_installation_id(mut self, value: impl Into) -> Self { + self.expected_installation_id = Some(value.into()); + self + } +} + /// MCP server startup filtering result. /// ///
    @@ -11495,6 +14583,44 @@ pub struct McpStopServerRequest { pub server_name: String, } +/// Extensible [`McpStopServerRequest`], including inputs added after it was published. +/// +/// Required inputs are [`McpStopServerOptions::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 McpStopServerOptions { + #[serde(flatten)] + legacy: McpStopServerRequest, + #[serde(skip_serializing_if = "Option::is_none")] + expected_installation_id: Option, +} + +impl McpStopServerOptions { + /// Creates options with the required inputs. + pub fn new(server_name: impl Into) -> Self { + Self { + legacy: McpStopServerRequest { + server_name: server_name.into(), + }, + expected_installation_id: None, + } + } + + /// Exact owned receipt identity. Stop also forgets this session's durable activation. + pub fn expected_installation_id(mut self, value: impl Into) -> Self { + self.expected_installation_id = Some(value.into()); + self + } +} + /// Metadata controlling an MCP task's lifetime. /// ///
    @@ -11545,7 +14671,7 @@ pub struct MemoryConfiguration { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextAttributionResultContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -11592,7 +14718,7 @@ pub struct MetadataContextAttributionResultContextAttributionEntriesItem { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextAttributionResultContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: MetadataContextAttributionResultContextAttributionCategories, @@ -11602,13 +14728,13 @@ pub struct MetadataContextAttributionResultContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -11673,9 +14799,9 @@ pub struct MetadataContextHeaviestMessagesResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextInfoRequest { - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. pub output_token_limit: i64, - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. pub prompt_token_limit: i64, /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. #[serde(skip_serializing_if = "Option::is_none")] @@ -11686,19 +14812,19 @@ pub struct MetadataContextInfoRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextInfoResultContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, /// Tokens consumed by the system prompt pub system_tokens: i64, @@ -12071,100 +15197,6 @@ pub struct ModelBilling { pub token_prices: Option, } -/// Vision-specific limits -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesLimitsVision { - /// Maximum image size in bytes - #[serde(rename = "max_prompt_image_size")] - pub max_prompt_image_size: i64, - /// Maximum number of images per prompt - #[serde(rename = "max_prompt_images")] - pub max_prompt_images: i64, - /// MIME types the model accepts - #[serde(rename = "supported_media_types")] - pub supported_media_types: Vec, -} - -/// Token limits for prompts, outputs, and context window -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesLimits { - /// Maximum total context window size in tokens - #[serde( - rename = "max_context_window_tokens", - skip_serializing_if = "Option::is_none" - )] - pub max_context_window_tokens: Option, - /// Maximum number of output/completion tokens - #[serde(rename = "max_output_tokens", skip_serializing_if = "Option::is_none")] - pub max_output_tokens: Option, - /// Maximum number of prompt/input tokens - #[serde(rename = "max_prompt_tokens", skip_serializing_if = "Option::is_none")] - pub max_prompt_tokens: Option, - /// Vision-specific limits - #[serde(skip_serializing_if = "Option::is_none")] - pub vision: Option, -} - -/// Feature flags indicating what the model supports -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesSupports { - /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). - #[serde(rename = "adaptive_thinking", skip_serializing_if = "Option::is_none")] - pub adaptive_thinking: Option, - /// Whether this model supports reasoning effort configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub reasoning_effort: Option, - /// Whether this model supports canonical tool calling - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_calls: Option, - /// Whether this model supports vision/image input - #[serde(skip_serializing_if = "Option::is_none")] - pub vision: Option, -} - -/// Model capabilities and limits -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilities { - /// Token limits for prompts, outputs, and context window - #[serde(skip_serializing_if = "Option::is_none")] - pub limits: Option, - /// Feature flags indicating what the model supports - #[serde(skip_serializing_if = "Option::is_none")] - pub supports: Option, -} - /// A service-published message about a model, carrying a stable machine-readable code alongside human-readable text. /// ///
    @@ -12200,6 +15232,25 @@ pub struct ModelPolicy { pub terms: Option, } +/// A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. +/// +///
    +/// +/// **Experimental.** This type 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 ModelProviderRef { + /// Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. + pub id: String, + /// The provider kind. + pub kind: ModelProviderKind, + /// Human-readable provider label, owned by the runtime so every consumer renders identical text. + pub label: String, +} + /// Service-published warning text that hosts should display when presenting a model. /// ///
    @@ -12254,12 +15305,18 @@ pub struct Model { /// Policy state (if applicable) #[serde(skip_serializing_if = "Option::is_none")] pub policy: Option, + /// The model provider that produced this model, as a neutral reference (opaque id, human-readable label, and provider kind). Present on models returned by `session.model.list`, which resolves provider attribution from the session's account roster; absent on the flat `server.models.list`, which does not resolve a session roster. The model picker groups by this reference. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, /// Context-window tiers this model offers, when the provider advertises them independently of tiered token pricing. Copilot models carry their tiers in `billing.tokenPrices`; a provider that has no pricing to publish (an agent host reached over AHP, for example) declares them here instead, so the model picker can still offer the tier toggle. #[serde(skip_serializing_if = "Option::is_none")] pub supported_context_tiers: Option>, /// Supported reasoning effort levels (only present if model supports reasoning effort) #[serde(skip_serializing_if = "Option::is_none")] pub supported_reasoning_efforts: Option>, + /// Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. + #[serde(skip_serializing_if = "Option::is_none")] + pub vendor: Option, /// Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. #[serde(skip_serializing_if = "Option::is_none")] pub warning_messages: Option>, @@ -12291,6 +15348,12 @@ pub struct ModelApplyStartupOverlayRequest { /// Model required by device-managed policy, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub device_managed_model: Option, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub managed_context_tier: Option, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub managed_reasoning_effort: Option, /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub policy_helper_model: Option, @@ -12482,6 +15545,412 @@ 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. +/// +///
    +/// +/// **Experimental.** This type 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 ModelProviderDescriptor { + /// Opaque, stable provider id, stamped onto every model this provider returns. + pub id: String, + /// The neutral provider kind. + pub kind: ModelProviderKind, + /// Human-readable menu label, owned by the runtime so every consumer renders identical text. + pub label: String, + /// Stable ordering key for presenting providers in a deterministic sequence. + 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. /// ///
    @@ -12836,63 +16305,6 @@ pub struct MoveMcpLoadingToBackgroundResult { pub moved_to_background: bool, } -/// Azure-specific provider options. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProviderConfigAzure { - /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - #[serde(skip_serializing_if = "Option::is_none")] - pub api_version: Option, -} - -/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NamedProviderConfig { - /// Static API key used to authenticate provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub api_key: Option, - /// Azure authentication configuration for the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub azure: Option, - /// Base URL for provider API requests. - pub base_url: String, - /// Static bearer token used to authenticate provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub bearer_token: Option, - /// Whether the host supplies bearer tokens dynamically. - #[serde(skip_serializing_if = "Option::is_none")] - pub has_bearer_token_provider: Option, - /// Additional HTTP headers included with provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, - /// Unique provider name used to qualify model selection IDs. - pub name: String, - /// Transport used to communicate with the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub transport: Option, - /// Provider protocol family. - #[serde(skip_serializing_if = "Option::is_none")] - pub r#type: Option, - /// Wire API used to communicate with the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub wire_api: Option, -} - /// The session's friendly name, or null when not yet set. /// ///
    @@ -13199,7 +16611,7 @@ pub struct PermissionDecisionApproveForSessionApprovalExtensionManagement { pub operation: Option, } -/// Session-scoped factory approval, optionally narrowed by approval key. +/// Session-scoped workflow approval, optionally narrowed by approval key. /// ///
    /// @@ -13209,12 +16621,12 @@ pub struct PermissionDecisionApproveForSessionApprovalExtensionManagement { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalFactory { - /// Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. +pub struct PermissionDecisionApproveForSessionApprovalWorkflow { + /// Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. #[serde(skip_serializing_if = "Option::is_none")] pub approval_key: Option, - /// Approval covering factory operations. - pub kind: PermissionDecisionApproveForSessionApprovalFactoryKind, + /// Approval covering workflow operations. + pub kind: PermissionDecisionApproveForSessionApprovalWorkflowKind, } /// Session-scoped approval details for an extension's permission-gated capability access, keyed by extension name. @@ -13274,6 +16686,23 @@ pub struct PermissionDecisionApproveForSession { pub kind: PermissionDecisionApproveForSessionKind, } +/// Approve file-tool read access to specific directories for this logical session, including continuation or resume. +/// +///
    +/// +/// **Experimental.** This type 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 PermissionDecisionApproveReadOnlyForSession { + /// Canonical directories covered by the read-only grant + pub directories: Vec, + /// Approve read-only file-tool directory access for this session + pub kind: PermissionDecisionApproveReadOnlyForSessionKind, +} + /// Location-scoped approval details for specific command identifiers. /// ///
    @@ -13407,7 +16836,7 @@ pub struct PermissionDecisionApproveForLocationApprovalExtensionManagement { pub operation: Option, } -/// Location-scoped factory approval, optionally narrowed by approval key. +/// Location-scoped workflow approval, optionally narrowed by approval key. /// ///
    /// @@ -13417,12 +16846,12 @@ pub struct PermissionDecisionApproveForLocationApprovalExtensionManagement { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalFactory { - /// Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. +pub struct PermissionDecisionApproveForLocationApprovalWorkflow { + /// Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. #[serde(skip_serializing_if = "Option::is_none")] pub approval_key: Option, - /// Approval covering factory operations. - pub kind: PermissionDecisionApproveForLocationApprovalFactoryKind, + /// Approval covering workflow operations. + pub kind: PermissionDecisionApproveForLocationApprovalWorkflowKind, } /// Location-scoped approval details for an extension's permission-gated capability access, keyed by extension name. @@ -13867,7 +17296,7 @@ pub struct PermissionsLocationsAddToolApprovalDetailsExtensionManagement { pub operation: Option, } -/// Location-persisted factory approval, optionally narrowed by approval key. +/// Location-persisted workflow approval, optionally narrowed by approval key. /// ///
    /// @@ -13877,12 +17306,12 @@ pub struct PermissionsLocationsAddToolApprovalDetailsExtensionManagement { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionsLocationsAddToolApprovalDetailsFactory { - /// Optional factory operation name or canonical approval key; when omitted, the approval covers all factory operations. +pub struct PermissionsLocationsAddToolApprovalDetailsWorkflow { + /// Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. #[serde(skip_serializing_if = "Option::is_none")] pub approval_key: Option, - /// Approval covering factory operations. - pub kind: PermissionsLocationsAddToolApprovalDetailsFactoryKind, + /// Approval covering workflow operations. + pub kind: PermissionsLocationsAddToolApprovalDetailsWorkflowKind, } /// Location-persisted tool approval details for an extension's permission-gated capability access, keyed by extension name. @@ -14563,7 +17992,7 @@ pub struct PermissionsSetModeRequest { pub assisted_approval_model: Option, /// Permission mode to apply pub mode: PermissionMode, - /// Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. + /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, } @@ -15283,50 +18712,6 @@ pub struct ProtocolSystemMessageReplaceConfig { pub mode: ProtocolReplaceMode, } -/// A BYOK model definition referencing a named provider. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProviderModelConfig { - /// Optional capability overrides (vision, tool_calls, reasoning, etc.). - #[serde(skip_serializing_if = "Option::is_none")] - pub capabilities: Option, - /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. - pub id: String, - /// Maximum context window tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_context_window_tokens: Option, - /// Maximum output tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_output_tokens: Option, - /// Maximum prompt/input tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_prompt_tokens: Option, - /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option>, - /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - #[serde(skip_serializing_if = "Option::is_none")] - pub model_id: Option, - /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - /// Name of the configured provider that serves this model. - pub provider: String, - /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. - #[serde(skip_serializing_if = "Option::is_none")] - pub system_message: Option, - /// The model name sent to the provider API for inference. Defaults to `id`. - #[serde(skip_serializing_if = "Option::is_none")] - pub wire_model: Option, -} - /// BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. /// ///
    @@ -15404,6 +18789,9 @@ pub struct ProviderConfig { /// Well-known model ID used for capability lookup. When set, agent behavior config and token limits are inferred from this model. #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// The product serving the model, reported in telemetry as `model_provider`. Set it when `type` alone cannot identify the product, such as Ollama or LM Studio behind an OpenAI-compatible endpoint. Only affects telemetry. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// Provider name used for model and telemetry attribution. #[serde(skip_serializing_if = "Option::is_none")] pub provider_name: Option, @@ -15562,6 +18950,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 /// ///
    @@ -16984,14 +20407,44 @@ pub struct ResponseFormat { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SandboxConfigAuth { - /// Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the OS keyring the sandbox blocks. Default: false (opt-in). + /// 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). #[serde(skip_serializing_if = "Option::is_none")] pub gh: Option, - /// Whether to inject git credentials as an `http..extraheader` so authenticated HTTPS git works inside the sandbox without the shell-based credential helper the sandbox blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's own helper before the sandbox is applied. Default: false (opt-in). + /// Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http..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). #[serde(skip_serializing_if = "Option::is_none")] pub git: Option, } +/// Destinations authorized to receive one masked environment 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 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. + pub inject_hosts: Vec, +} + +/// Whole-value environment credential masking for sandboxed children. +/// +///
    +/// +/// **Experimental.** This type 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 SandboxCredentialsConfig { + /// Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. + pub env_vars: HashMap, +} + /// macOS seatbelt experimental options. /// ///
    @@ -17162,6 +20615,9 @@ pub struct SandboxConfig { /// Credential-injection capability flags. #[serde(skip_serializing_if = "Option::is_none")] pub auth: Option, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub credentials: Option, /// Whether sandboxing is enabled for the session. pub enabled: bool, /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. @@ -17238,7 +20694,40 @@ pub struct SandboxEnforcementStatus { pub required: 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`. +/// Request to accept the sandbox path grant offered on an active sandbox escalation permission 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 SandboxGrantPathForRequestRequest { + /// Optional attribution for the permission decision. + #[serde(skip_serializing_if = "Option::is_none")] + pub decision_context: Option, + /// Identifier of the exact pending sandbox escalation permission request whose sandboxPathGrant to accept. + pub request_id: RequestId, +} + +/// Result of accepting a sandbox path grant. +/// +///
    +/// +/// **Experimental.** This type 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 SandboxGrantPathForRequestResult { + /// Whether this call resolved the pending request and added the path to the session's sandbox policy. + 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`, except `filesystem_enumeration`, whose absence degrades sandboxed PowerShell instead of failing it. /// ///
    /// @@ -17249,7 +20738,7 @@ pub struct SandboxEnforcementStatus { #[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")] @@ -17278,6 +20767,57 @@ pub struct SandboxHostSupport { pub supported: bool, } +/// Result of creating the persistent certificate authority of the sandbox credential proxy. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaCreateResult { + /// Absolute path of the public certificate of the certificate authority, in PEM format. + pub certificate_path: String, +} + +/// Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRequest { + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_config: Option, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaStatus { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + /// Register an absolute-time scheduled prompt. /// ///
    @@ -17847,35 +21387,6 @@ pub struct SessionActivity { pub has_active_work: bool, } -/// Current authentication information, or null when no authentication is active. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionAuthInfoResult { - /// Snapshot of the authenticated user's Copilot subscription info, if known - #[serde(skip_serializing_if = "Option::is_none")] - pub copilot_user: Option, - /// Name of the environment variable that supplied the credential, when applicable - #[serde(skip_serializing_if = "Option::is_none")] - pub env_var: Option, - /// Authentication host - pub host: String, - /// Authenticated login, when available - #[serde(skip_serializing_if = "Option::is_none")] - pub login: Option, - /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. - #[serde(skip_serializing_if = "Option::is_none")] - pub registration_id: Option, - /// Authentication type - pub r#type: AuthInfoType, -} - /// Internal GitHub login parameters. /// ///
    @@ -17976,11 +21487,28 @@ pub struct SessionBulkDeleteResult { pub freed_bytes: HashMap, } +/// The IDE a host is connected to, as reported to the session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectedIdeInfo { + /// Display name of the connected IDE, for example `VS Code`. + pub ide_name: String, + /// Absolute path of the workspace folder the IDE has open. + pub workspace_folder: String, +} + /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -18034,7 +21562,7 @@ pub struct SessionContextAttributionEntriesItem { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: SessionContextAttributionCategories, @@ -18044,13 +21572,13 @@ pub struct SessionContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -18067,19 +21595,19 @@ pub struct SessionContextAttribution { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, /// Tokens consumed by the system prompt pub system_tokens: i64, @@ -18142,6 +21670,9 @@ pub struct SessionFsError { /// Free-form detail about the error, for logging/diagnostics #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + #[serde(skip_serializing_if = "Option::is_none")] + pub write_changed: Option, } /// Path to test for existence in the client-provided session filesystem. @@ -18286,6 +21817,41 @@ pub struct SessionFsReaddirWithTypesResult { pub error: Option, } +/// Path of the binary file to read from the client-provided session filesystem. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsReadFileBytesRequest { + /// Target session identifier + pub session_id: SessionId, + /// Path using SessionFs conventions + pub path: String, +} + +/// File bytes as standard base64, or a filesystem error if the read failed. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsReadFileBytesResult { + /// Exact file bytes encoded as standard base64 + pub content: String, + /// Describes a filesystem error. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + /// Path of the file to read from the client-provided session filesystem. /// ///
    @@ -18374,6 +21940,9 @@ pub struct SessionFsRmRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFsSetProviderCapabilities { + /// Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes + #[serde(skip_serializing_if = "Option::is_none")] + pub binary: Option, /// Whether the provider supports SQLite query/exists operations #[serde(skip_serializing_if = "Option::is_none")] pub sqlite: Option, @@ -18593,6 +22162,28 @@ pub struct SessionFsStatResult { pub size: i64, } +/// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsWriteFileBytesRequest { + /// Target session identifier + pub session_id: SessionId, + /// Path using SessionFs conventions + pub path: String, + /// Exact file bytes encoded as standard base64 + pub content: String, + /// Optional POSIX-style mode for newly created files + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + /// File path, content to write, and optional mode for the client-provided session filesystem. /// ///
    @@ -18904,6 +22495,9 @@ pub struct SessionManagedPermissions { /// When set to `disable`, prevents bypass/allow-all permission modes. Advisory auto-approval remains available because normal prompt paths stay active. Any other value is accepted rather than failing the session, but is enforced as `disable`: the key is only present to restrict something, so a mode this runtime cannot interpret fails closed to the most restrictive one it knows. Omit the key entirely to impose no restriction. #[serde(skip_serializing_if = "Option::is_none")] pub disable_bypass_permissions_mode: Option, + /// Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected because every network request must be enforceable at host-level egress. Multiple managed sources intersect their lists; an empty list denies all hosts. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit_to: Option>, } /// Managed settings an SDK host may inject at session startup. Only permissions are accepted in this initial contract. @@ -18922,6 +22516,22 @@ pub struct SessionManagedSettings { pub permissions: Option, } +/// Records which IDE the host is connected to, or clears it. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpSetConnectedIdeInfoParams { + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + #[serde(skip_serializing_if = "Option::is_none")] + pub ide: Option, +} + /// Public-facing projection of workspace metadata for SDK / TUI consumers #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -19041,6 +22651,9 @@ pub struct SessionModelList { /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub model_price_categories: Option>, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub providers: Option>, /// Per-quota snapshots returned alongside the model list, keyed by quota type. #[serde(skip_serializing_if = "Option::is_none")] pub quota_snapshots: Option>, @@ -19447,10 +23060,16 @@ pub struct SessionOpenOptions { /// Resolved sandbox configuration. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_config: Option, - /// Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. - #[doc(hidden)] + /// 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.** 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(crate) sandbox_config_source: Option, + pub sandbox_config_source: Option, /// Capabilities enabled for this session. #[serde(skip_serializing_if = "Option::is_none")] pub session_capabilities: Option>, @@ -19855,6 +23474,85 @@ pub struct SessionsCloseRequest { #[serde(rename_all = "camelCase")] pub struct SessionsCloseResult {} +/// A working-directory context together with the client that produced it. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionWorkingDirectoryContextWithClient { + /// Merge-base commit SHA + #[serde(skip_serializing_if = "Option::is_none")] + pub base_commit: Option, + /// Current git branch name + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, + /// Name of the client that created the session + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + /// Current working directory path + pub cwd: String, + /// Root directory of the git repository + #[serde(skip_serializing_if = "Option::is_none")] + pub git_root: Option, + /// Head commit of the current git branch + #[serde(skip_serializing_if = "Option::is_none")] + pub head_commit: Option, + /// Hosting platform type of the repository + #[serde(skip_serializing_if = "Option::is_none")] + pub host_type: Option, + /// Repository identifier derived from the git remote URL + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Raw host string from the git remote URL + #[serde(skip_serializing_if = "Option::is_none")] + pub repository_host: Option, +} + +/// Identity, state location and starting context for a workspace record. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsCreateWorkspaceRequest { + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + pub convention: String, + /// User-supplied display name for the workspace + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Session ID the workspace record belongs to + pub session_id: SessionId, + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + pub session_state_path: String, +} + +/// The workspace record that was written. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsCreateWorkspaceResult { + /// The created workspace record, as JSON + pub workspace_json: String, +} + /// Session ID to delete from disk. /// ///
    @@ -20549,6 +24247,39 @@ pub struct SessionsLoadDeferredRepoHooksRequest { pub session_id: SessionId, } +/// Where the session's state lives, as a root directory and the session ID under it. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsLoadWorkspaceRequest { + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + pub session_id: SessionId, + /// Root directory every session's state directory sits under + pub sessions_home: String, +} + +/// The workspace record on disk, omitted when the session has none. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsLoadWorkspaceResult { + /// The workspace record, as JSON. Omitted when the record does not exist. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_json: Option, +} + /// Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). /// ///
    @@ -20777,6 +24508,37 @@ pub struct SessionsTransferRemoteControlRequest { pub to_session_id: String, } +/// Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsUpdateWorkspaceFieldsRequest { + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + pub fields_json: String, + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + pub session_id: SessionId, + /// Root directory every session's state directory sits under + pub sessions_home: String, +} + +/// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsUpdateWorkspaceFieldsResult {} + /// Telemetry engagement ID for the session, when available. /// ///
    @@ -20950,10 +24712,16 @@ pub struct SessionUpdateOptionsParams { /// Resolved sandbox configuration. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_config: Option, - /// Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently. - #[doc(hidden)] + /// 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.** 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(crate) sandbox_config_source: Option, + pub sandbox_config_source: Option, /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub session_capabilities: Option>, @@ -21030,7 +24798,7 @@ pub struct ShellCancelUserRequestedRequest { pub request_id: RequestId, } -/// Shell command to run, with optional working directory and timeout in milliseconds. +/// Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. /// ///
    /// @@ -21051,7 +24819,7 @@ pub struct ShellExecRequest { pub timeout: Option, } -/// Identifier of the spawned process, used to correlate streamed output and exit notifications. +/// Identifier of the spawned shell process, usable with shell.kill while the process is running. /// ///
    /// @@ -21062,7 +24830,7 @@ pub struct ShellExecRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShellExecResult { - /// Unique identifier for tracking streamed output + /// Identifier usable with shell.kill while the process is running pub process_id: String, } @@ -21173,6 +24941,44 @@ pub struct Skill { pub user_invocable: bool, } +/// Applies exactly one retained verified Skill installation plan. +/// +///
    +/// +/// **Experimental.** This type 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 SkillApplyInstallRequest { + /// Required authenticated bound catalogue and Skill installation capabilities. + pub contract: CatalogClientContract, + /// Opaque original plan, consumed once. + pub plan_handle: String, + /// Same existing selected session as planning. + pub policy_session_id: String, +} + +/// One-use application of the exact retained Skill removal plan. +/// +///
    +/// +/// **Experimental.** This type 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 SkillApplyUninstallRequest { + /// Required authenticated bound installation contract. + pub contract: CatalogClientContract, + /// Opaque original removal plan, consumed once. + pub plan_handle: String, + /// Same existing selected session as removal preparation. + pub policy_session_id: String, +} + /// Canonical directory where skills can be discovered or created, with scope, preference, and optional project path. /// ///
    @@ -21210,6 +25016,344 @@ pub struct SkillDiscoveryPathList { pub paths: Vec, } +/// Already-confirmed durable work must be reconciled before new mutations or inventory. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeRecoveryRequired { + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeRecoveryRequiredKind, +} + +/// A computed Skill install plan. Nothing has been applied. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallPlan { + /// Original wall-clock expiry as an ISO 8601 timestamp. + pub expires_at: String, + /// Original operation identifier returned before confirmation. + pub operation_id: String, + /// One-use plan handle, bound to the original candidate authority. + pub plan_handle: String, + /// Safe review to present before applying the plan. + pub review: SkillInstallationReview, +} + +/// A verified Skill installation plan was prepared. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeInstallPlanned { + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeInstallPlannedKind, + /// Prepared install plan. + pub plan: SkillInstallPlan, +} + +/// Owned Skill installations were listed. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeListed { + /// Owned Skill installation summaries. + pub installations: Vec, + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeListedKind, +} + +/// Recovery completed and inventory was inspected. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeRecovered { + /// Owned Skill installation summaries after recovery. + pub installations: Vec, + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeRecoveredKind, +} + +/// Interrupted work was safely compensated and the pending marker was cleared. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeRolledBack { + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeRolledBackKind, + /// Original operation identity. + #[serde(rename = "operation_id")] + pub operation_id: String, + /// Cause of the compensation. + pub reason: SkillInstallationFailureReason, +} + +/// A computed Skill uninstall plan. Nothing has been removed. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillUninstallPlan { + /// Original wall-clock expiry as an ISO 8601 timestamp. + pub expires_at: String, + /// Owned Skill installation being removed. + pub installation: SkillInstallationSummary, + /// Original removal operation identifier returned before confirmation. + pub operation_id: String, + /// One-use uninstall plan handle. + pub plan_handle: String, + /// Safe review to present before applying removal. + pub review: SkillInstallationReview, +} + +/// An owned Skill uninstall plan was prepared. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeUninstallPlanned { + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeUninstallPlannedKind, + /// Prepared uninstall plan. + pub plan: SkillUninstallPlan, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOperationStatusPreparing { + /// Whether cancellation has been requested. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + pub phase: SkillInstallationOperationStatusPreparingPhase, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOperationStatusPrepared { + /// Whether cancellation has been requested. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + pub phase: SkillInstallationOperationStatusPreparedPhase, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOperationStatusAwaitingConfirmation { + /// Whether cancellation has been requested. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + pub phase: SkillInstallationOperationStatusAwaitingConfirmationPhase, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOperationStatusRevalidating { + /// Whether cancellation has been requested. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + pub phase: SkillInstallationOperationStatusRevalidatingPhase, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOperationStatusApplying { + /// Whether cancellation has been requested; already-started effects require recovery. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + pub phase: SkillInstallationOperationStatusApplyingPhase, +} + +/// The verified Skill was installed disabled. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOutcomeInstalled { + /// Durable installed Skill summary. + pub installation: SkillInstallationSummary, + /// Terminal Skill installation outcome discriminator. + pub kind: SkillInstallationOutcomeInstalledKind, +} + +/// The owned Skill was removed. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOutcomeUninstalled { + /// Removed installation identity. + pub installation_id: String, + /// Terminal Skill installation outcome discriminator. + pub kind: SkillInstallationOutcomeUninstalledKind, + /// Original removal operation identity. + pub operation_id: String, +} + +/// The durable transaction was aborted or fully compensated. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOutcomeRolledBack { + /// Terminal Skill installation outcome discriminator. + pub kind: SkillInstallationOutcomeRolledBackKind, + /// Original operation identity. + pub operation_id: String, + /// Cause of the fully aborted or compensated operation. + pub reason: SkillInstallationFailureReason, +} + +/// A write may have completed. Recover and inspect durable state before retrying. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOutcomeRecoveryRequired { + /// Terminal Skill installation outcome discriminator. + pub kind: SkillInstallationOutcomeRecoveryRequiredKind, + /// Operation whose durable result must be recovered and inspected. + pub operation_id: String, +} + +/// The user declined the confirmation request. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOutcomeDeclined { + /// Terminal Skill installation outcome discriminator. + pub kind: SkillInstallationOutcomeDeclinedKind, + /// Original operation identity. + pub operation_id: String, +} + +/// The operation was cancelled before a terminal mutation. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOutcomeCancelled { + /// Terminal Skill installation outcome discriminator. + pub kind: SkillInstallationOutcomeCancelledKind, + /// Original operation identity. + pub operation_id: String, +} + +/// The operation was refused without applying changes. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOutcomeRefused { + /// Terminal Skill installation outcome discriminator. + pub kind: SkillInstallationOutcomeRefusedKind, + /// Present once an operation has been allocated. + #[serde(skip_serializing_if = "Option::is_none")] + pub operation_id: Option, + /// Bounded refusal reason. + pub reason: SkillInstallationFailureReason, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationOperationStatusCompleted { + /// Whether cancellation was requested before the terminal result. + pub cancellation_requested: bool, + /// Original runtime-issued operation identity. + pub operation_id: String, + /// Immutable terminal receipt. + pub outcome: SkillInstallationOutcome, + pub phase: SkillInstallationOperationStatusCompletedPhase, +} + +/// Original-connection operation snapshot. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeOperation { + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeOperationKind, + /// Operation status. + pub operation: SkillInstallationOperationStatus, +} + +/// Enablement changed and the selected session was reconciled. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeEnabledChanged { + /// Safe reload or reconciliation diagnostics. + pub diagnostics: Vec, + /// Updated installation summary. + pub installation: SkillInstallationSummary, + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeEnabledChangedKind, +} + +/// The management request was refused. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementOutcomeRefused { + /// Skill installation management outcome discriminator. + pub kind: SkillInstallationManagementOutcomeRefusedKind, + /// Bounded refusal reason. + pub reason: SkillInstallationFailureReason, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationManagementResultOutcome { + /// Discriminator: handled management outcome. + pub kind: SkillInstallationManagementResultOutcomeKind, + /// Capabilities honoured for this request. + pub negotiated: CatalogNegotiatedContract, + /// Observed management outcome. + pub outcome: SkillInstallationManagementOutcome, +} + +/// Existing-operation control. A new session selector is deliberately not accepted. +/// +///
    +/// +/// **Experimental.** This type 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 SkillInstallationOperationRequest { + /// Required authenticated bound Skill installation capability. + pub contract: CatalogClientContract, + /// Exact runtime-issued operation ID on the original connection. + pub operation_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInstallationResultOutcome { + /// Discriminator: handled operation outcome. + pub kind: SkillInstallationResultOutcomeKind, + /// Capabilities honoured for this request. + pub negotiated: CatalogNegotiatedContract, + /// Terminal operation outcome. + pub outcome: SkillInstallationOutcome, +} + +/// Inventory request under an explicitly selected existing 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 SkillInstallationsRequest { + /// Required authenticated bound installation contract. + pub contract: CatalogClientContract, + /// Existing selected local session on this connection. + pub policy_session_id: String, +} + /// Skills available to the session, with their enabled state. /// ///
    @@ -21225,7 +25369,45 @@ pub struct SkillList { pub skills: Vec, } -/// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. +/// Side-effect-free planning of one verified Agent Finder Skill candidate. +/// +///
    +/// +/// **Experimental.** This type 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 SkillPlanInstallRequest { + /// Fresh single-use AI skill candidate handle returned by a bound catalogue search. + pub candidate_handle: String, + /// Required authenticated bound catalogue and Skill installation capabilities. + pub contract: CatalogClientContract, + /// Existing local session attached to this connection. + pub policy_session_id: String, +} + +/// Read-only preparation of one owned Skill removal under fresh selected-session authority. +/// +///
    +/// +/// **Experimental.** This type 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 SkillPlanUninstallRequest { + /// Required authenticated bound installation contract. + pub contract: CatalogClientContract, + /// Exact receipt to inspect. + pub installation_id: String, + /// Existing selected local session on this connection. + pub policy_session_id: String, +} + +/// Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is fetched separately and lazily. /// ///
    /// @@ -21283,7 +25465,7 @@ pub(crate) struct SkillProviderReadRequest { pub name: String, } -/// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. +/// Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter is optional: fields it omits come from the catalog descriptor, fields it declares must match the descriptor, and `allowed-tools` is read only from frontmatter. Related files and assets are not supported. /// ///
    /// @@ -21294,8 +25476,8 @@ pub(crate) struct SkillProviderReadRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SkillProviderReadResult { - /// Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. - pub markdown: String, + /// SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. + pub markdown: Option, } /// Skill names to mark as disabled in global configuration, replacing any previous list. @@ -21385,6 +25567,27 @@ pub struct SkillsEnableRequest { pub name: String, } +/// Persisted enablement update for one owned Skill installation. +/// +///
    +/// +/// **Experimental.** This type 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 SkillSetEnabledRequest { + /// Required authenticated bound Skill installation capability. + pub contract: CatalogClientContract, + /// Persisted enablement value. + pub enabled: bool, + /// Exact receipt identity to update. + pub installation_id: String, + /// Existing selected local session to reconcile after persistence. + pub policy_session_id: String, +} + /// Optional project paths to enumerate. /// ///
    @@ -21687,6 +25890,9 @@ pub struct SlashCommandShowDialogResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SlashCommandSetModelResult { + /// Auto routing profile selected by the command, when the model is Auto. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Discriminator for a set-model result. pub kind: SlashCommandSetModelResultKind, /// Model selected by the command. @@ -23834,7 +28040,7 @@ pub struct UserSettingMetadata { pub value: serde_json::Value, } -/// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. +/// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. /// ///
    /// @@ -23864,21 +28070,6 @@ pub struct UserSettingsSetRequest { pub settings: serde_json::Value, } -/// Outcome of writing user settings. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserSettingsSetResult { - /// Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. - pub shadowed_keys: Vec, -} - /// Current sharing status and shareable GitHub URL for a session. /// ///
    @@ -25486,6 +29677,101 @@ pub struct WorkspacesWriteAutopilotObjectiveResult { pub operation: String, } +/// The resident session the application has materialized on its own connection. +/// +///
    +/// +/// **Experimental.** This type 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 HostCreateSessionResult { + /// Runtime session UUID materialized on the application's SDK connection. + pub session_id: SessionId, +} + +/// Empty acknowledgement for a completed host lifecycle 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 HostReleaseSessionResult {} + +/// Empty acknowledgement for a completed host lifecycle 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 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. +/// +///
    +/// +/// **Experimental.** This type 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 HostGetConfigurationResult { + /// 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, +} + +/// Empty acknowledgement for a completed host lifecycle 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 HostReadyResult {} + /// List of Copilot models available to the resolved user, including capabilities and billing metadata. /// ///
    @@ -25536,6 +29822,86 @@ pub struct SandboxGetHostSupportResult { pub supported: bool, } +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaGetStatusResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRotateResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaTrustResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRemoveResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + /// Built-in tools available for the requested model, with their parameters and instructions. /// ///
    @@ -25583,6 +29949,39 @@ pub struct ExtensionsDiscoverResult { pub mode: DiscoveredExtensionMode, } +/// Skills discovered across global and project sources. +/// +///
    +/// +/// **Experimental.** This type 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 SkillsDiscoverResult { + /// Messages for skills that failed to load (e.g. malformed SKILL.md). Empty when host skills are excluded so host-local paths are not disclosed to multitenant callers. + #[serde(skip_serializing_if = "Option::is_none")] + pub errors: Option>, + /// All discovered skills across all sources + pub skills: Vec, +} + +/// Canonical locations where skills can be created so the runtime will recognize 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 SkillsGetDiscoveryPathsResult { + /// Canonical skill create/discovery directories, in priority order + pub paths: Vec, +} + /// Plugins installed in user/global state. /// ///
    @@ -25738,7 +30137,7 @@ pub struct PluginsMarketplacesRefreshResult { pub results: Vec, } -/// Skills discovered across global and project sources. +/// Agents discovered across user, project, plugin, and remote sources. /// ///
    /// @@ -25748,15 +30147,12 @@ pub struct PluginsMarketplacesRefreshResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SkillsDiscoverResult { - /// Messages for skills that failed to load (e.g. malformed SKILL.md). Empty when host skills are excluded so host-local paths are not disclosed to multitenant callers. - #[serde(skip_serializing_if = "Option::is_none")] - pub errors: Option>, - /// All discovered skills across all sources - pub skills: Vec, +pub struct AgentsDiscoverResult { + /// All discovered agents across all sources + pub agents: Vec, } -/// Canonical locations where skills can be created so the runtime will recognize them. +/// Canonical locations where custom agents can be created so the runtime will recognize them. /// ///
    /// @@ -25766,12 +30162,12 @@ pub struct SkillsDiscoverResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SkillsGetDiscoveryPathsResult { - /// Canonical skill create/discovery directories, in priority order - pub paths: Vec, +pub struct AgentsGetDiscoveryPathsResult { + /// Canonical agent create/discovery directories, in priority order + pub paths: Vec, } -/// Agents discovered across user, project, plugin, and remote sources. +/// Instruction sources discovered across user, repository, and plugin sources. /// ///
    /// @@ -25781,12 +30177,12 @@ pub struct SkillsGetDiscoveryPathsResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AgentsDiscoverResult { - /// All discovered agents across all sources - pub agents: Vec, +pub struct InstructionsDiscoverResult { + /// All discovered instruction sources + pub sources: Vec, } -/// Canonical locations where custom agents can be created so the runtime will recognize them. +/// Canonical files and directories where custom instructions can be created so the runtime will recognize them. /// ///
    /// @@ -25796,12 +30192,12 @@ pub struct AgentsDiscoverResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AgentsGetDiscoveryPathsResult { - /// Canonical agent create/discovery directories, in priority order - pub paths: Vec, +pub struct InstructionsGetDiscoveryPathsResult { + /// Canonical instruction create/discovery files and directories, in priority order + pub paths: Vec, } -/// Instruction sources discovered across user, repository, and plugin sources. +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
    /// @@ -25811,12 +30207,72 @@ pub struct AgentsGetDiscoveryPathsResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct InstructionsDiscoverResult { - /// All discovered instruction sources - pub sources: Vec, +pub(crate) struct GlobalStateLoadForConfigDirResult { + /// Whether the user has answered the prompt suggesting they install the desktop app. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_install_nudge_responded: Option, + /// Whether the app tip has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_tip_shown: Option, + /// Terminals the user has already been asked to set up, so the host does not ask twice. + #[serde(skip_serializing_if = "Option::is_none")] + pub asked_setup_terminals: Option>, + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_feedback_last_prompted_at: Option, + /// When the host first ran on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub first_launch_at: Option, + /// Plugins installed on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed_plugins: Option>, + /// Account used for the most recent sign-in. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) last_logged_in_user: Option, + /// Every account the host has signed in to on this machine. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) logged_in_users: Option>, + /// Whether the one-off cleanup of stored reasoning summaries has run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summaries_cleanup_done: Option, + /// Models the user selected recently, most recent first. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_model_ids: Option>, + /// Whether the user declined to trust the sandbox credential proxy CA. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_credential_proxy_ca_declined: Option, + /// Whether the sandbox onboarding has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_onboarding_shown: Option, + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff: Option, + /// Whether the user was recognized as GitHub staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_github: Option, + /// When the staff-only log level migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_log_level_migration_at: Option, + /// Whether the user was recognized as Microsoft staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_microsoft: Option, + /// When the staff-only model reset last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_model_reset_at: Option, + /// When the staff-only update channel migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_update_channel_migration_at: Option, + /// Folders where the user declined the init prompt, so it stays hidden there. + #[serde(skip_serializing_if = "Option::is_none")] + pub suppress_init_folders: Option>, + /// Folders the user has marked as trusted. + #[serde(skip_serializing_if = "Option::is_none")] + pub trusted_folders: Option>, } -/// Canonical files and directories where custom instructions can be created so the runtime will recognize them. +/// Slash commands available in the session, after applying any include/exclude filters. /// ///
    /// @@ -25826,12 +30282,12 @@ pub struct InstructionsDiscoverResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct InstructionsGetDiscoveryPathsResult { - /// Canonical instruction create/discovery files and directories, in priority order - pub paths: Vec, +pub struct CommandsListResult { + /// Commands available in this session + pub commands: Vec, } -/// Slash commands available in the session, after applying any include/exclude filters. +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. /// ///
    /// @@ -25841,9 +30297,45 @@ pub struct InstructionsGetDiscoveryPathsResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CommandsListResult { - /// Commands available in this session - pub commands: Vec, +pub(crate) struct GitHubOwnersNextRequestIdResult { + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + pub request_id: i64, +} + +/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitWorkingDirectoryContextResult { + /// Merge-base commit SHA (fork point from the remote default branch) + #[serde(skip_serializing_if = "Option::is_none")] + pub base_commit: Option, + /// Current git branch name + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, + /// Current working directory path + pub cwd: String, + /// Root directory of the git repository, resolved via git rev-parse + #[serde(skip_serializing_if = "Option::is_none")] + pub git_root: Option, + /// Head commit of the current git branch + #[serde(skip_serializing_if = "Option::is_none")] + pub head_commit: Option, + /// Hosting platform type of the repository + #[serde(skip_serializing_if = "Option::is_none")] + pub host_type: Option, + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com") + #[serde(skip_serializing_if = "Option::is_none")] + pub repository_host: Option, } /// Result of opening a session. @@ -26113,6 +30605,197 @@ pub struct SessionsGetRemoteControlStatusResult { pub status: serde_json::Value, } +/// Feature availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsGetCapabilitiesResult { + /// API version. + pub api_version: i64, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, + /// Whether results are cached. + pub conditional_cache: bool, + /// Whether accounts are selected by opaque ID. + pub opaque_account_selection: bool, +} + +/// Eligible accounts. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsGetAccountsResult { + /// Eligible accounts. + pub accounts: Vec, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, +} + +/// Entries for the selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsListResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + +/// Entries for the selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsRefreshResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetCatalogParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Normalized model-provider adapter definitions available to the session, not discovered instances. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetCatalogResult { + /// Available provider adapters ordered by adapterId. + pub providers: Vec, +} + +/// Provider instances found by a discovery operation. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersDiscoverResult { + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + pub instances: Vec, + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + pub outcome: ModelProviderOperationOutcome, +} + +/// Current health information for a provider instance. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetStatusResult { + /// Normalized provider instance. + pub instance: ModelProviderInstance, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + pub status: String, + /// Provider-reported version. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersModelsListResult { + /// Provider-native models in provider order. + pub models: Vec, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, +} + +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersModelsPrepareConfigurationResult { + /// Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. + pub model: ProviderModelConfig, + /// Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. + pub model_disposition: ModelProviderConfigurationDisposition, + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + pub provider: NamedProviderConfig, + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + pub provider_disposition: ModelProviderConfigurationDisposition, + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + pub selection_id: String, + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + pub warnings: Vec, +} + /// Identifies the target session. /// ///
    @@ -26210,6 +30893,21 @@ pub struct SessionSandboxDisableForSessionResult { pub success: bool, } +/// Result of accepting a sandbox path grant. +/// +///
    +/// +/// **Experimental.** This type 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 SessionSandboxGrantPathForRequestResult { + /// Whether this call resolved the pending request and added the path to the session's sandbox policy. + pub success: bool, +} + /// Result of aborting the current turn /// ///
    @@ -26396,6 +31094,41 @@ pub struct SessionGitHubAuthLastAuthErrorsParams { pub session_id: SessionId, } +/// Result of a non-interactive accounts mutation. +/// +///
    +/// +/// **Experimental.** This type 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 SessionAccountsSetResult { + /// For a logout, whether other signed-in accounts remain. + #[serde(skip_serializing_if = "Option::is_none")] + pub more_users: Option, + /// Whether the mutation was applied. + pub ok: bool, +} + +/// A started login flow: its opaque id and first step. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionAccountsLoginBeginResult { + /// Opaque flow id used to advance or cancel this login. + pub flow_id: String, + /// The first step of the flow. + pub step: AuthLoginStep, +} + /// Result of collecting a session debug bundle. /// ///
    @@ -26531,413 +31264,6 @@ pub struct SessionCanvasActionInvokeResult { pub result: Option, } -/// Complete current or terminal factory run envelope. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryRunResult { - /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. - #[serde(skip_serializing_if = "Option::is_none")] - pub attempt: Option, - /// Error message for an errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Machine-readable failure details for a halted or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure: Option, - /// Structured pause initiator metadata for a paused attempt. - #[serde(skip_serializing_if = "Option::is_none")] - pub pause_info: Option, - /// Reason for a halted or cancelled run. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Completed factory result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Factory run identifier. - pub run_id: String, - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, -} - -/// Resolved persisted factory identity and resumed run envelope. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryResumeResult { - /// Persisted factory name resolved for the resumed run. - pub factory_name: String, - /// Terminal resumed run envelope. - pub run: FactoryRunResult, -} - -/// Complete current or terminal factory run envelope. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryRunFromToolResult { - /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. - #[serde(skip_serializing_if = "Option::is_none")] - pub attempt: Option, - /// Error message for an errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Machine-readable failure details for a halted or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure: Option, - /// Structured pause initiator metadata for a paused attempt. - #[serde(skip_serializing_if = "Option::is_none")] - pub pause_info: Option, - /// Reason for a halted or cancelled run. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Completed factory result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Factory run identifier. - pub run_id: String, - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, -} - -/// Resolved persisted factory identity and resumed run envelope. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryResumeFromToolResult { - /// Persisted factory name resolved for the resumed run. - pub factory_name: String, - /// Terminal resumed run envelope. - pub run: FactoryRunResult, -} - -/// Complete current or terminal factory run envelope. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryGetRunResult { - /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. - #[serde(skip_serializing_if = "Option::is_none")] - pub attempt: Option, - /// Error message for an errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Machine-readable failure details for a halted or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure: Option, - /// Structured pause initiator metadata for a paused attempt. - #[serde(skip_serializing_if = "Option::is_none")] - pub pause_info: Option, - /// Reason for a halted or cancelled run. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Completed factory result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Factory run identifier. - pub run_id: String, - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, -} - -/// A page of factory runs in durable creation order. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryListRunsResult { - /// Whether terminal runs newer than this page exist. - #[serde(skip_serializing_if = "Option::is_none")] - pub has_more_newer: Option, - /// Newest terminal-run cursor in this page, or null when the terminal window is empty. - #[serde(skip_serializing_if = "Option::is_none")] - pub newest_seq: Option, - /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. - #[serde(skip_serializing_if = "Option::is_none")] - pub oldest_seq: Option, - /// Number of terminal runs older than this page. - #[serde(skip_serializing_if = "Option::is_none")] - pub omitted_older: Option, - /// Factory run summaries in durable creation order. - pub runs: Vec, -} - -/// Full factory run observability detail. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryGetRunDetailResult { - /// Epoch milliseconds when the current active segment started, or null while inactive. - pub active_segment_started_at: Option, - /// Durable identities and live statuses for direct factory agents. - pub agents: Vec, - /// Approved effective resource ceilings, or null until approved. - pub approved: Option, - /// Whether the durable run state currently passes runtime resume eligibility checks. - pub can_resume: bool, - /// Epoch milliseconds when the run completed, or null while nonterminal. - pub completed_at: Option, - /// Durable resource consumption. - pub consumed: FactoryRunConsumed, - /// Epoch milliseconds when the run was created. - pub created_at: i64, - /// Current phase identity, or null before any phase is entered. - pub current_phase: Option, - /// Resource ceilings declared by the factory. - pub declared_limits: FactoryDeclaredLimits, - /// Number of phases declared by the factory. - pub declared_phase_count: i64, - /// Human-readable factory description. - pub description: String, - /// Registered factory name. - pub factory_name: String, - /// Number of direct factory agents currently live. - pub live_agent_count: i64, - /// Epoch milliseconds when this live-overlay snapshot was observed. - pub observed_at: i64, - /// Lifecycle and timing observations for each factory phase. - pub phases: Vec, - /// Bidirectional page of durable factory progress. - pub progress: FactoryProgressPage, - /// Monotonic durable run revision. - pub revision: i64, - /// Factory run identifier. - pub run_id: String, - /// Epoch milliseconds when execution first started, or null before start. - pub started_at: Option, - /// Current factory run status. - pub status: FactoryRunStatus, - /// Terminal run outcome, or null while nonterminal. - pub terminal: Option, - /// Total direct factory agents spawned across all attempts. - pub total_spawned_agent_count: i64, - /// Epoch milliseconds when the durable run was last updated. - pub updated_at: i64, -} - -/// A bidirectional page of factory progress. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryGetRunProgressResult { - /// Whether progress records newer than this page exist. - pub has_more_newer: bool, - /// Whether progress records older than this page exist. - pub has_more_older: bool, - /// Newest sequence number in this page, or null when empty. - pub newest_seq: Option, - /// Oldest sequence number in this page, or null when empty. - pub oldest_seq: Option, - /// Progress records in sequence order. - pub records: Vec, - /// Run revision reflected by this page. - pub revision: i64, -} - -/// Complete current or terminal factory run envelope. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryCancelResult { - /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. - #[serde(skip_serializing_if = "Option::is_none")] - pub attempt: Option, - /// Error message for an errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Machine-readable failure details for a halted or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure: Option, - /// Structured pause initiator metadata for a paused attempt. - #[serde(skip_serializing_if = "Option::is_none")] - pub pause_info: Option, - /// Reason for a halted or cancelled run. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Completed factory result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Factory run identifier. - pub run_id: String, - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, -} - -/// Complete current or terminal factory run envelope. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryPauseResult { - /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. - #[serde(skip_serializing_if = "Option::is_none")] - pub attempt: Option, - /// Error message for an errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Machine-readable failure details for a halted or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure: Option, - /// Structured pause initiator metadata for a paused attempt. - #[serde(skip_serializing_if = "Option::is_none")] - pub pause_info: Option, - /// Reason for a halted or cancelled run. - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Completed factory result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Factory run identifier. - pub run_id: String, - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - #[serde(skip_serializing_if = "Option::is_none")] - pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, -} - -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryPauseAtCheckpointResult { - /// Whether this execution attempt must pause or may continue. - pub action: FactoryPauseCheckpointAction, -} - -/// Acknowledgement that a factory request was accepted. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryLogResult {} - -/// Result of one factory-scoped subagent 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 SessionFactoryAgentResult { - /// Agent result, omitted when the agent produced no result. - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, -} - -/// Result of reading a factory journal entry. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionFactoryJournalGetResult { - /// Whether the journal contained the requested key. - pub hit: bool, - /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. - #[serde(skip_serializing_if = "Option::is_none")] - pub result_json: Option, -} - -/// Acknowledgement that a factory request was accepted. -/// -///
    -/// -/// **Experimental.** This type 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 SessionFactoryJournalPutResult {} - /// Complete current or terminal workflow run envelope. /// ///
    @@ -27386,6 +31712,9 @@ pub struct SessionModelGetCurrentResult { /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. #[serde(skip_serializing_if = "Option::is_none")] pub pending_auto_tier: Option, + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_model_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -27560,6 +31889,9 @@ pub struct SessionModelListResult { /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub model_price_categories: Option>, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub providers: Option>, /// Per-quota snapshots returned alongside the model list, keyed by quota type. #[serde(skip_serializing_if = "Option::is_none")] pub quota_snapshots: Option>, @@ -28518,23 +32850,6 @@ pub struct SessionCustomizationsReloadParams { pub session_id: SessionId, } -/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionCustomizationsReloadResult { - /// Errors emitted while loading skills (e.g. skills that failed to load entirely) - pub errors: Vec, - /// Warnings emitted while loading skills (e.g. skills that loaded but had issues) - pub warnings: Vec, -} - /// Indicates whether fleet mode was successfully activated. /// ///
    @@ -29072,6 +33387,36 @@ pub struct SessionMcpListResult { pub servers: Vec, } +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredResult { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Tools exposed by the connected MCP server. Throws when the server is not connected. /// ///
    @@ -29279,6 +33624,49 @@ pub struct SessionMcpOauthHandlePendingRequestResult { pub success: bool, } +/// Effect-free preparation bound to the existing local session, requester and installation, with frozen 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 SessionMcpOauthPrepareLoginParams { + /// Name recorded by the authoritative owned installation receipt. + pub server_name: String, + /// Exact installation identity from owned inventory, never a server-name alias. + pub expected_installation_id: String, + /// Request a new authorisation rather than accepting a usable cached grant. + #[serde(skip_serializing_if = "Option::is_none")] + pub force_reauth: Option, + /// Display name used by the incumbent OAuth client-registration flow. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + /// Text shown on the loopback callback page after successful authorisation. + #[serde(skip_serializing_if = "Option::is_none")] + pub callback_success_message: Option, +} + +/// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. +/// +///
    +/// +/// **Experimental.** This type 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 SessionMcpOauthPrepareLoginResult { + /// Original expiry, not extended by consumption, retries or cancellation. + pub expires_at: String, + /// Retain with the original requester and use for one login or cancellation. + pub login_id: String, +} + /// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. /// ///
    @@ -29290,9 +33678,50 @@ pub struct SessionMcpOauthHandlePendingRequestResult { #[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. + #[serde(skip_serializing_if = "Option::is_none")] + pub login_id: Option, + /// Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Targets only the original prepared/applying owned login on this exact session requester. +/// +///
    +/// +/// **Experimental.** This type 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 SessionMcpOauthCancelLoginParams { + /// The same authoritative installation identity supplied during preparation. + pub expected_installation_id: String, + /// Runtime-issued login handle known before the effectful login request begins. + pub login_id: String, +} + +/// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. +/// +///
    +/// +/// **Experimental.** This type 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 SessionMcpOauthCancelLoginResult { + /// True after cancellation settles, false when the original login already connected successfully. + pub cancelled: bool, } /// Indicates whether the pending MCP OAuth response was accepted. @@ -29453,6 +33882,54 @@ pub struct SessionMcpResourcesListTemplatesResult { pub resource_templates: Vec, } +/// One page of prompts advertised by the named MCP server. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpPromptsListResult { + /// MCP result metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard result fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Opaque cursor for the next page, if the server has more prompts + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Prompts advertised by the server + pub prompts: Vec, +} + +/// Prompt messages returned by the MCP server without sending them to the model. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpPromptsGetResult { + /// MCP result metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard result fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Description of the prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Ordered prompt messages + pub messages: Vec, +} + /// Per-source session diagnostics configuration. /// ///
    @@ -29556,6 +34033,27 @@ pub struct SessionConnectorsGetCapabilitiesResult { pub max_poll_interval_ms: i64, /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. pub opaque_account_selection: bool, + /// Whether getAccount is supported. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_account_selection: Option, + /// Whether reconcile accepts forceConnectorName. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub targeted_reconcile: Option, +} + +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsGetAccountParams { + /// Target session identifier + pub session_id: SessionId, } /// Identifies the target session. @@ -29778,7 +34276,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. /// ///
    /// @@ -30054,6 +34552,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. /// ///
    @@ -30420,6 +34938,21 @@ pub struct SessionUiHandlePendingElicitationResult { pub success: bool, } +/// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanAskUserResult { + /// Whether the response was accepted. False if the request was already resolved by another client. + pub success: bool, +} + /// Indicates whether the pending UI request was resolved by this call. /// ///
    @@ -30435,6 +34968,21 @@ pub struct SessionUiHandlePendingUserInputResult { pub success: bool, } +/// Indicates whether the pending UI request was resolved by this call. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanUserInputResult { + /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. + pub success: bool, +} + /// Indicates whether the pending UI request was resolved by this call. /// ///
    @@ -30495,6 +35043,21 @@ pub struct SessionUiHandlePendingExitPlanModeResult { pub success: bool, } +/// Indicates whether the pending UI request was resolved by this call. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanExitPlanModeResult { + /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. + pub success: bool, +} + /// Identifies the target session. /// ///
    @@ -31071,19 +35634,19 @@ pub struct SessionMetadataActivityResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataContextInfoResultContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, /// Tokens consumed by the system prompt pub system_tokens: i64, @@ -31127,7 +35690,7 @@ pub struct SessionMetadataGetContextAttributionParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataGetContextAttributionResultContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -31174,7 +35737,7 @@ pub struct SessionMetadataGetContextAttributionResultContextAttributionEntriesIt #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataGetContextAttributionResultContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: SessionMetadataGetContextAttributionResultContextAttributionCategories, @@ -31184,13 +35747,13 @@ pub struct SessionMetadataGetContextAttributionResultContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -31341,7 +35904,7 @@ pub struct SessionContentExclusionCheckPathsResult { pub checks: Vec, } -/// Identifier of the spawned process, used to correlate streamed output and exit notifications. +/// Identifier of the spawned shell process, usable with shell.kill while the process is running. /// ///
    /// @@ -31352,7 +35915,7 @@ pub struct SessionContentExclusionCheckPathsResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionShellExecResult { - /// Unique identifier for tracking streamed output + /// Identifier usable with shell.kill while the process is running pub process_id: String, } @@ -32467,18 +37030,6 @@ pub struct ProviderTokenGetTokenResult { pub token: String, } -/// Acknowledgement that a factory request was accepted. -/// -///
    -/// -/// **Experimental.** This type 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 FactoryAbortResult {} - /// Acknowledgement that a workflow request was accepted. /// ///
    @@ -32588,6 +37139,16 @@ pub type CatalogResourceVersion = String; ///
    pub type ClientMetadata = HashMap; +/// Session account selection, or null. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +pub type ConnectorSessionAccountResult = Option; + /// HTTP headers as a map from lowercased header name to a list of values. Multi-valued headers (e.g. Set-Cookie) preserve all values. /// ///
    @@ -32618,7 +37179,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. /// ///
    /// @@ -32628,6 +37189,16 @@ pub type McpPlanSecretReference = String; ///
    pub type SandboxHostCapabilityName = String; +/// Current authentication information, or null when no authentication is active. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +pub type SessionAuthInfoResult = Option; + /// Ordered client metadata outcomes for the requested local sessions. /// ///
    @@ -32817,6 +37388,14 @@ pub enum UserAuthInfoType { User, } +/// Provider-owned account authentication. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AccountAuthInfoType { + #[serde(rename = "account")] + #[default] + Account, +} + /// Authentication via the `gh` CLI's saved credentials. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum GhCliAuthInfoType { @@ -32850,10 +37429,153 @@ pub enum AuthInfo { TokenProvider(TokenProviderAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } +/// The provider kind stamped on a signed-in account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AccountKind { + /// An OAuth github.com account. + #[serde(rename = "githubDotCom")] + GitHubDotCom, + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com. + #[serde(rename = "proxima")] + Proxima, + /// A GitHub (EMU) account derived from a base Entra identity. + #[serde(rename = "entraEmu")] + EntraEmu, + /// A base Microsoft Entra identity. + #[serde(rename = "entra")] + Entra, + /// A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra identity as an EMU account; its bearer is a Loki-scoped inference token consumed through the model-provider path, not the GitHub switcher. + #[serde(rename = "loki")] + Loki, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Account-collection query variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthEnumerateQueryAccountsKind { + #[serde(rename = "accounts")] + #[default] + Accounts, +} + +/// Account-collection query variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthEnumerateQueryProvidersKind { + #[serde(rename = "providers")] + #[default] + Providers, +} + +/// Selects which accounts collection to enumerate. A no-arg selector is the empty-payload variant. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum AuthEnumerateQuery { + Accounts(AuthEnumerateQueryAccounts), + Providers(AuthEnumerateQueryProviders), +} + +/// Account read-datum query variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthReadQueryActiveAccountKind { + #[serde(rename = "activeAccount")] + #[default] + ActiveAccount, +} + +/// Account read-datum query variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthReadQueryStatusKind { + #[serde(rename = "status")] + #[default] + Status, +} + +/// Account read-datum query variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthReadQueryLastErrorsKind { + #[serde(rename = "lastErrors")] + #[default] + LastErrors, +} + +/// Selects which typed accounts datum to read. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AuthReadQuery { + ActiveAccount(AuthReadQueryActiveAccount), + Status(AuthReadQueryStatus), + LastErrors(AuthReadQueryLastErrors), +} + +/// Account mutation command variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthWriteSwitchActiveKind { + #[serde(rename = "switchActive")] + #[default] + SwitchActive, +} + +/// Account mutation command variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthWriteLogoutKind { + #[serde(rename = "logout")] + #[default] + Logout, +} + +/// Account mutation command variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthWriteSetCredentialsKind { + #[serde(rename = "setCredentials")] + #[default] + SetCredentials, +} + +/// One non-interactive accounts mutation command (the selector is fused with its typed args). +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum AuthWrite { + SwitchActive(AuthWriteSwitchActive), + Logout(AuthWriteLogout), + SetCredentials(AuthWriteSetCredentials), +} + /// Resolved Anthropic adaptive-thinking capability for a model. /// ///
    @@ -33339,6 +38061,62 @@ pub enum AttachmentSelectionType { Selection, } +/// Enumerated account-collection variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthEnumerateValueAccountsKind { + #[serde(rename = "accounts")] + #[default] + Accounts, +} + +/// A provider a consumer may interactively sign in with. +/// +///
    +/// +/// **Experimental.** This type 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 LoginProviderKind { + /// OAuth github.com sign-in via the browser (web loopback + PKCE). + #[serde(rename = "githubDotCom")] + GitHubDotCom, + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com; the host is supplied interactively through the neutral input-required step. + #[serde(rename = "proxima")] + Proxima, + /// Microsoft Entra sign-in that derives a GitHub (EMU) credential. + #[serde(rename = "entra")] + Entra, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Enumerated account-collection variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthEnumerateValueProvidersKind { + #[serde(rename = "providers")] + #[default] + Providers, +} + +/// The enumerated collection, keyed by the same selector as the query. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum AuthEnumerateValue { + Accounts(AuthEnumerateValueAccounts), + Providers(AuthEnumerateValueProviders), +} + /// Authentication type /// ///
    @@ -33358,6 +38136,9 @@ pub enum AuthInfoType { /// Authentication from an interactive user sign-in. #[serde(rename = "user")] User, + /// Authentication from a selected provider-owned account, without a GitHub credential. + #[serde(rename = "account")] + Account, /// Authentication delegated to the GitHub CLI. #[serde(rename = "gh-cli")] GhCli, @@ -33379,6 +38160,141 @@ pub enum AuthInfoType { Unknown, } +/// Login flow step variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthLoginStepOpenUrlKind { + #[serde(rename = "open-url")] + #[default] + OpenUrl, +} + +/// Login flow step variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthLoginStepInputRequiredKind { + #[serde(rename = "input-required")] + #[default] + InputRequired, +} + +/// Login flow step variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthLoginStepAwaitingKind { + #[serde(rename = "awaiting")] + #[default] + Awaiting, +} + +/// Login flow step variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthLoginStepNeedsInteractionKind { + #[serde(rename = "needs-interaction")] + #[default] + NeedsInteraction, +} + +/// Login flow step variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthLoginStepCompletedKind { + #[serde(rename = "completed")] + #[default] + Completed, +} + +/// Disposition of a login attempt, including pending user decisions. +/// +///
    +/// +/// **Experimental.** This type 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 AuthLoginResultStatus { + /// 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, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Login flow step variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthLoginStepErrorKind { + #[serde(rename = "error")] + #[default] + Error, +} + +/// 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). +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum AuthLoginStep { + OpenUrl(AuthLoginStepOpenUrl), + InputRequired(AuthLoginStepInputRequired), + Awaiting(AuthLoginStepAwaiting), + NeedsInteraction(AuthLoginStepNeedsInteraction), + Completed(AuthLoginStepCompleted), + Error(AuthLoginStepError), +} + +/// Account read-datum variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthReadValueActiveAccountKind { + #[serde(rename = "activeAccount")] + #[default] + ActiveAccount, +} + +/// Account read-datum variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthReadValueStatusKind { + #[serde(rename = "status")] + #[default] + Status, +} + +/// Account read-datum variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthReadValueLastErrorsKind { + #[serde(rename = "lastErrors")] + #[default] + LastErrors, +} + +/// The read result, keyed by the same selector as the query. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum AuthReadValue { + ActiveAccount(AuthReadValueActiveAccount), + Status(AuthReadValueStatus), + LastErrors(AuthReadValueLastErrors), +} + /// Current normalized autopilot objective lifecycle status. /// ///
    @@ -33524,7 +38440,7 @@ pub enum CatalogAgentPluginMediaType { Unknown, } -/// Typed non-installable state for an AI skill candidate +/// Typed installability state for an AI skill candidate /// ///
    /// @@ -33534,7 +38450,19 @@ pub enum CatalogAgentPluginMediaType { ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum CatalogAiSkillInstallability { - /// AI skills are discovery-only on this surface. + /// This AI skill candidate carries verified materialisation metadata and the selected session may plan installation. + #[serde(rename = "installable")] + Installable, + /// Skill installation is understood but disabled for the selected session. + #[serde(rename = "feature-disabled")] + FeatureDisabled, + /// The candidate lacks verified materialisation metadata required for installation. + #[serde(rename = "materialisation-unavailable")] + MaterialisationUnavailable, + /// Policy refuses Skill installation for the selected session or authority. + #[serde(rename = "policy-forbids")] + PolicyForbids, + /// Compatibility value for discovery-only callers that did not negotiate Skill installation. #[serde(rename = "not-installable-kind")] NotInstallableKind, /// Unknown variant for forward compatibility. @@ -33789,6 +38717,18 @@ pub enum CatalogCapability { /// 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. #[serde(rename = "catalog-search-credential-required")] CatalogSearchCredentialRequired, + /// 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. + #[serde(rename = "catalog-search-session-bound")] + CatalogSearchSessionBound, + /// 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. + #[serde(rename = "mcp-confirmed-remote-installation")] + McpConfirmedRemoteInstallation, + /// 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. + #[serde(rename = "mcp-configured-remote-installation")] + McpConfiguredRemoteInstallation, + /// 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. + #[serde(rename = "skill-confirmed-installation")] + SkillConfirmedInstallation, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -33908,6 +38848,9 @@ pub enum CatalogHandleRejectionReason { ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum CatalogInvalidRequestField { + /// The selected existing attached session was missing, malformed or unavailable. + #[serde(rename = "policySessionId")] + PolicySessionId, /// The search query was empty or longer than permitted. #[serde(rename = "query")] Query, @@ -34974,6 +39917,31 @@ pub enum ConnectorConnectResult { Pending(ConnectorConnectResultPending), } +/// Availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorDiscoveryAvailability { + /// Enabled. + #[serde(rename = "enabled")] + Enabled, + /// Disabled. + #[serde(rename = "disabled")] + Disabled, + /// Unavailable. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Closed set of public task kinds a connection can negotiate. /// ///
    @@ -35024,6 +39992,124 @@ pub enum ContentFilterMode { Unknown, } +/// Result of reloading a customization component. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum CustomizationReloadStatus { + /// The component was refreshed successfully. + Reloaded, + /// The component was not configured, loaded, or eligible for refresh. + Skipped, + /// The component could not be refreshed; other components may still reload. + Failed, + /// Unknown variant for forward compatibility. + Unknown(String), +} + +impl Default for CustomizationReloadStatus { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} + +impl From for CustomizationReloadStatus { + fn from(value: String) -> Self { + match value.as_str() { + "reloaded" => Self::Reloaded, + "skipped" => Self::Skipped, + "failed" => Self::Failed, + _ => Self::Unknown(value), + } + } +} + +impl From for String { + fn from(value: CustomizationReloadStatus) -> Self { + match value { + CustomizationReloadStatus::Reloaded => "reloaded".to_owned(), + CustomizationReloadStatus::Skipped => "skipped".to_owned(), + CustomizationReloadStatus::Failed => "failed".to_owned(), + CustomizationReloadStatus::Unknown(value) => value, + } + } +} + +/// Component of session customization discovery. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum CustomizationReloadSubsystem { + /// Repository metadata and working-directory context. + RepositoryContext, + /// Session instructions and their cached dynamic context. + Instructions, + /// Discovered plugin configuration. + Plugins, + /// Configured session and plugin hooks. + Hooks, + /// Discovered skills. + Skills, + /// Discovered custom agents. + Agents, + /// Loaded MCP server configuration. + Mcp, + /// Configured session extensions. + Extensions, + /// Unknown variant for forward compatibility. + Unknown(String), +} + +impl Default for CustomizationReloadSubsystem { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} + +impl From for CustomizationReloadSubsystem { + fn from(value: String) -> Self { + match value.as_str() { + "repositoryContext" => Self::RepositoryContext, + "instructions" => Self::Instructions, + "plugins" => Self::Plugins, + "hooks" => Self::Hooks, + "skills" => Self::Skills, + "agents" => Self::Agents, + "mcp" => Self::Mcp, + "extensions" => Self::Extensions, + _ => Self::Unknown(value), + } + } +} + +impl From for String { + fn from(value: CustomizationReloadSubsystem) -> Self { + match value { + CustomizationReloadSubsystem::RepositoryContext => "repositoryContext".to_owned(), + CustomizationReloadSubsystem::Instructions => "instructions".to_owned(), + CustomizationReloadSubsystem::Plugins => "plugins".to_owned(), + CustomizationReloadSubsystem::Hooks => "hooks".to_owned(), + CustomizationReloadSubsystem::Skills => "skills".to_owned(), + CustomizationReloadSubsystem::Agents => "agents".to_owned(), + CustomizationReloadSubsystem::Mcp => "mcp".to_owned(), + CustomizationReloadSubsystem::Extensions => "extensions".to_owned(), + CustomizationReloadSubsystem::Unknown(value) => value, + } + } +} + /// Source category for a collected debug bundle entry. /// ///
    @@ -35493,6 +40579,93 @@ pub enum DiscoveredMcpServerType { Unknown, } +/// Kind of component that supplied a provider adapter or row. Attribution does not confer authority. +/// +///
    +/// +/// **Experimental.** This type 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 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, +} + +/// 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 ModelProviderOperationOutcomeCode { + /// The operation completed successfully; an empty inventory is valid. + #[serde(rename = "success")] + Success, + /// The provider or instance is absent during discovery, status, or model listing. Distinct from a successful empty inventory. + #[serde(rename = "absent")] + Absent, + /// The provider is configured or expected but could not be reached. + #[serde(rename = "unreachable")] + Unreachable, + /// The operation failed for a reason other than absence or reachability. + #[serde(rename = "failed")] + Failed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// GitHub Mission Control compute kind. +/// +///
    +/// +/// **Experimental.** This type 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 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. /// ///
    @@ -35723,7 +40896,38 @@ pub enum ExternalToolTextResultForLlmContentTextType { Text, } -/// Execution-critical factory storage operation. +/// Why the runtime is requesting a GitHub credential. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum GitHubTokenAcquireReason { + /// The runtime is acquiring the registration's first credential. + #[serde(rename = "initial")] + Initial, + /// The runtime is replacing a credential that is approaching expiry. + #[serde(rename = "refresh")] + Refresh, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// GitHub credential response variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum GitHubTokenAcquireResultTokenKind { + #[serde(rename = "token")] + #[default] + Token, +} + +/// GitHub credential response variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum GitHubTokenAcquireResultCancelledKind { + #[serde(rename = "cancelled")] + #[default] + Cancelled, +} + +/// SDK host response to a GitHub credential request. /// ///
    /// @@ -35731,48 +40935,324 @@ pub enum ExternalToolTextResultForLlmContentTextType { /// and may change or be removed in future SDK or CLI releases. /// ///
    +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GitHubTokenAcquireResult { + Token(GitHubTokenAcquireResultToken), + Cancelled(GitHubTokenAcquireResultCancelled), +} + +/// 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). #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FactoryDurableOperation { - /// Creating the durable run and declared phases. - #[serde(rename = "createRun")] - CreateRun, - /// Persisting the transition to running. - #[serde(rename = "markRunStarted")] - MarkRunStarted, - /// Persisting the terminal run envelope. - #[serde(rename = "finishRun")] - FinishRun, - /// Persisting subagent admission accounting. - #[serde(rename = "reserveAgent")] - ReserveAgent, - /// Rolling back an uncommitted subagent admission. - #[serde(rename = "releaseAgent")] - ReleaseAgent, - /// Persisting an idempotent model-usage charge. - #[serde(rename = "chargeCredit")] - ChargeCredit, - /// Persisting active execution time. - #[serde(rename = "addElapsed")] - AddElapsed, - /// Reading the authoritative AI-credit total. - #[serde(rename = "reconcileCreditTotal")] - ReconcileCreditTotal, - /// Reading a journal entry without treating storage failure as a cache miss. - #[serde(rename = "journalGet")] - JournalGet, - /// Persisting a journal entry before reporting success. - #[serde(rename = "journalPut")] - JournalPut, - /// Renewing the durable owner lease that proves this process still owns the run. - #[serde(rename = "refreshLease")] - RefreshLease, +pub enum HistoryCompactRequestTrigger { + /// User-requested compaction, e.g. the /compact command or a direct history.compact call. + #[serde(rename = "manual")] + Manual, + /// Compaction requested while switching to a model with a smaller context window. + #[serde(rename = "model_switch")] + ModelSwitch, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Reason a captured file was not restored. +/// +///
    +/// +/// **Experimental.** This type 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 HistoryFileRestoreSkipReason { + /// The file changed after Copilot's last captured write. + #[serde(rename = "user-modified")] + UserModified, + /// A faithful preimage was not captured. + #[serde(rename = "skipped-capture")] + SkippedCapture, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Reason a rewind read (rewind points, file-restore preview, or session diff) could not be answered from the session's file-change captures. +/// +///
    +/// +/// **Experimental.** This type 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 HistoryRewindUnavailableReason { + /// The session did not opt into file-change tracking before its first turn. + #[serde(rename = "file-change-tracking-disabled")] + FileChangeTrackingDisabled, + /// 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. + #[serde(rename = "session-busy")] + SessionBusy, + /// Remote-backed rewind routing is not supported. + #[serde(rename = "unsupported-remote-session")] + UnsupportedRemoteSession, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Aggregate file change represented by a rewind preview. +/// +///
    +/// +/// **Experimental.** This type 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 HistoryRewindChangeType { + /// The discarded turns created the file. + #[serde(rename = "created")] + Created, + /// The discarded turns deleted the file. + #[serde(rename = "deleted")] + Deleted, + /// The discarded turns modified the file. + #[serde(rename = "modified")] + Modified, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Scope of a rewind 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 HistoryRewindMode { + /// Discard conversation events while leaving files unchanged. + #[serde(rename = "conversation")] + Conversation, + /// Discard conversation events and restore captured files changed by those turns. + #[serde(rename = "conversation-and-files")] + ConversationAndFiles, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Outcome of a rewind request. +/// +///
    +/// +/// **Experimental.** This type 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 HistoryRewindOutcome { + /// The requested rewind completed; reachable in either mode. + #[serde(rename = "success")] + Success, + /// The session still has work that may mutate files or history; reachable in either mode. + #[serde(rename = "session-busy")] + SessionBusy, + /// A conversation-and-files rewind was requested for a session that did not enable capture; conversation-only rewinds never produce this. + #[serde(rename = "file-change-tracking-disabled")] + FileChangeTrackingDisabled, + /// Remote-backed rewind routing is not supported; reachable in either mode. + #[serde(rename = "unsupported-remote-session")] + UnsupportedRemoteSession, + /// File restore failed and all applied file changes were rolled back; only conversation-and-files rewinds produce this. + #[serde(rename = "files-rolled-back")] + FilesRolledBack, + /// File restore failed and its rollback could not fully restore the pre-rewind state; only conversation-and-files rewinds produce this. + #[serde(rename = "rollback-incomplete")] + RollbackIncomplete, + /// Conversation truncation failed. In conversation-and-files mode any files that were restored are left in place because conversation history cannot be un-truncated; in conversation-only mode no files are restored. Consult restoredFiles for what, if anything, was applied. + #[serde(rename = "truncation-failed")] + TruncationFailed, + /// The conversation was rewound (and, in conversation-and-files mode, captured files were restored), but persisted checkpoints could not be cleaned up; reachable in either mode. + #[serde(rename = "checkpoint-cleanup-failed")] + CheckpointCleanupFailed, + /// Files and conversation were rewound, but obsolete file snapshots could not be removed; only conversation-and-files rewinds produce this. + #[serde(rename = "snapshot-prune-failed")] + SnapshotPruneFailed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HostExitReason { + /// The owner requested disposal. + #[serde(rename = "disposed")] + Disposed, + /// The hosting task or its SDK transport exited; this does not mean the runtime process exited. + #[serde(rename = "exited")] + Exited, + /// The owning SDK connection disconnected. + #[serde(rename = "ownerDisconnected")] + OwnerDisconnected, + /// The runtime is shutting down. + #[serde(rename = "runtimeShutdown")] + RuntimeShutdown, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Reviewed resource discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum InstallationReviewMcpResource { + #[serde(rename = "mcp")] + #[default] + Mcp, +} + +/// Exact reviewed installation action. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationReviewInstallAction { + #[serde(rename = "install")] + #[default] + Install, +} + +/// Whether a planned configuration change would create or modify an entry +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpPlanConfigurationOperation { + /// Creates a configuration entry that does not exist yet. + #[serde(rename = "add")] + Add, + /// Modifies a configuration entry that already exists. + #[serde(rename = "update")] + Update, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Configuration scope an MCP install plan targets +/// +///
    +/// +/// **Experimental.** This type 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 McpPlanScope { + /// The user's own MCP configuration. + #[serde(rename = "user")] + User, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Transport exposed by a remote endpoint +/// +///
    +/// +/// **Experimental.** This type 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 McpPlanRemoteTransport { + /// An HTTP endpoint. + #[serde(rename = "http")] + Http, + /// A streamable HTTP endpoint. + #[serde(rename = "streamable-http")] + StreamableHttp, + /// A server-sent events endpoint. + #[serde(rename = "sse")] + Sse, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Where a required value is applied when the planned server is launched +/// +///
    +/// +/// **Experimental.** This type 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 McpPlanValueCategory { + /// Set as an environment variable on the launched process. + #[serde(rename = "environment-variable")] + EnvironmentVariable, + /// Passed to the runtime that launches the package. + #[serde(rename = "runtime-argument")] + RuntimeArgument, + /// Passed to the packaged server itself. + #[serde(rename = "package-argument")] + PackageArgument, + /// Sent as a request header to a remote endpoint. + #[serde(rename = "header")] + Header, + /// Substituted into the remote endpoint URL. + #[serde(rename = "url-variable")] + UrlVariable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// What policy decided for a planned 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, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpPlanPolicyDecision { + /// Policy permits the server. + #[serde(rename = "allowed")] + Allowed, + /// Policy forbids the server, so the plan cannot be applied. + #[serde(rename = "blocked")] + Blocked, + /// Policy permits the server only after an explicit approval. + #[serde(rename = "requires-approval")] + RequiresApproval, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Current or terminal state of a factory run. +/// Explicit backend selection is part of the final review; failures never switch backends. /// ///
    /// @@ -35781,35 +41261,20 @@ pub enum FactoryDurableOperation { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FactoryRunStatus { - /// The run was minted and is awaiting approval. - #[serde(rename = "pending")] - Pending, - /// The run is executing. - #[serde(rename = "running")] - Running, - /// The run completed successfully. - #[serde(rename = "completed")] - Completed, - /// The run was interrupted while resource budget remained. - #[serde(rename = "halted")] - Halted, - /// The current attempt stopped intentionally and the run may be resumed. - #[serde(rename = "paused")] - Paused, - /// The run was cancelled before completion. - #[serde(rename = "cancelled")] - Cancelled, - /// The factory body failed or reached a cumulative resource ceiling. - #[serde(rename = "error")] - Error, +pub enum McpInstallationSecretStorage { + /// The selected operating-system keychain, without fallback to file storage. + #[serde(rename = "keychain")] + Keychain, + /// The explicitly selected private file backend. + #[serde(rename = "private-file")] + PrivateFile, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Kind of factory progress line. +/// Discriminator for a package-backed transport choice /// ///
    /// @@ -35818,20 +41283,17 @@ pub enum FactoryRunStatus { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FactoryLogLineKind { - /// A narrator log line. - #[serde(rename = "log")] - Log, - /// A named factory phase marker. - #[serde(rename = "phase")] - Phase, +pub enum McpPlanPackageInstallMethod { + /// Install and run a local package. + #[serde(rename = "package")] + Package, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Action the runtime selected for a durable factory pause checkpoint. +/// Discriminator for a scalar required value /// ///
    /// @@ -35840,20 +41302,17 @@ pub enum FactoryLogLineKind { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FactoryPauseCheckpointAction { - /// The checkpoint was committed by a prior paused attempt, so execution may continue. - #[serde(rename = "continue")] - Continue, - /// This attempt claimed the checkpoint and must cooperatively stop. - #[serde(rename = "pause")] - Pause, +pub enum McpPlanRequiredValueScalarKind { + /// The value uses one scalar type. + #[serde(rename = "scalar")] + Scalar, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Derived lifecycle state of a factory phase. +/// Scalar type a required value must conform to /// ///
    /// @@ -35862,26 +41321,26 @@ pub enum FactoryPauseCheckpointAction { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FactoryPhaseStatus { - /// The phase has not been entered yet. - #[serde(rename = "pending")] - Pending, - /// The phase is currently entered and accumulating active time. - #[serde(rename = "active")] - Active, - /// The phase was entered and has since been closed. - #[serde(rename = "completed")] - Completed, - /// The phase was never entered because a later phase was entered or the run reached a terminal state. - #[serde(rename = "skipped")] - Skipped, +pub enum McpPlanScalarValueType { + /// Free text. + #[serde(rename = "string")] + String, + /// A number. + #[serde(rename = "number")] + Number, + /// A boolean. + #[serde(rename = "boolean")] + Boolean, + /// A filesystem path. + #[serde(rename = "path")] + Path, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Cumulative resource ceiling that stopped a factory run. +/// Discriminator for an enumerated required value /// ///
    /// @@ -35890,54 +41349,36 @@ pub enum FactoryPhaseStatus { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FactoryRunFailureKind { - /// The run admitted the approved maximum total number of subagents. - #[serde(rename = "maxTotalSubagents")] - MaxTotalSubagents, - /// The run reached the approved accumulated active-execution time in seconds. - #[serde(rename = "timeoutSeconds")] - TimeoutSeconds, - /// The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. - #[serde(rename = "maxAiCredits")] - MaxAiCredits, +pub enum McpPlanRequiredValueEnumKind { + /// The value uses a fixed non-empty enumeration. + #[serde(rename = "enum")] + Enum, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Why the runtime is requesting a GitHub credential. +/// Discriminator for an enumerated required value +/// +///
    +/// +/// **Experimental.** This type 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 GitHubTokenAcquireReason { - /// The runtime is acquiring the registration's first credential. - #[serde(rename = "initial")] - Initial, - /// The runtime is replacing a credential that is approaching expiry. - #[serde(rename = "refresh")] - Refresh, +pub enum McpPlanEnumValueType { + /// One of a fixed, non-empty set of permitted values. + #[serde(rename = "enum")] + Enum, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// GitHub credential response variant discriminator. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum GitHubTokenAcquireResultTokenKind { - #[serde(rename = "token")] - #[default] - Token, -} - -/// GitHub credential response variant discriminator. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum GitHubTokenAcquireResultCancelledKind { - #[serde(rename = "cancelled")] - #[default] - Cancelled, -} - -/// SDK host response to a GitHub credential request. +/// One non-secret value a transport choice needs, represented as a scalar or enumerated variant so enum values cannot be missing or attached to another type. /// ///
    /// @@ -35947,27 +41388,31 @@ pub enum GitHubTokenAcquireResultCancelledKind { ///
    #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] -pub enum GitHubTokenAcquireResult { - Token(GitHubTokenAcquireResultToken), - Cancelled(GitHubTokenAcquireResultCancelled), +pub enum McpPlanRequiredValue { + Scalar(McpPlanRequiredValueScalar), + Enum(McpPlanRequiredValueEnum), } -/// 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). +/// Transport exposed by a locally launched package +/// +///
    +/// +/// **Experimental.** This type 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 HistoryCompactRequestTrigger { - /// User-requested compaction, e.g. the /compact command or a direct history.compact call. - #[serde(rename = "manual")] - Manual, - /// Compaction requested while switching to a model with a smaller context window. - #[serde(rename = "model_switch")] - ModelSwitch, +pub enum McpPlanPackageTransport { + /// A locally launched process spoken to over standard input and output. + #[serde(rename = "stdio")] + Stdio, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Reason a captured file was not restored. +/// Discriminator for a remote-endpoint transport choice /// ///
    /// @@ -35976,20 +41421,17 @@ pub enum HistoryCompactRequestTrigger { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HistoryFileRestoreSkipReason { - /// The file changed after Copilot's last captured write. - #[serde(rename = "user-modified")] - UserModified, - /// A faithful preimage was not captured. - #[serde(rename = "skipped-capture")] - SkippedCapture, +pub enum McpPlanRemoteInstallMethod { + /// Connect to a remote endpoint. + #[serde(rename = "remote")] + Remote, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Reason a rewind read (rewind points, file-restore preview, or session diff) could not be answered from the session's file-change captures. +/// One eligible way to run the server, represented as a tagged package or remote variant so package identity and endpoint states cannot contradict the install method. /// ///
    /// @@ -35997,24 +41439,72 @@ pub enum HistoryFileRestoreSkipReason { /// and may change or be removed in future SDK or CLI releases. /// ///
    +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum McpPlanTransportChoice { + Package(McpPlanTransportChoicePackage), + Remote(McpPlanTransportChoiceRemote), +} + +/// Exact reviewed installation action. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HistoryRewindUnavailableReason { - /// The session did not opt into file-change tracking before its first turn. - #[serde(rename = "file-change-tracking-disabled")] - FileChangeTrackingDisabled, - /// 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. - #[serde(rename = "session-busy")] - SessionBusy, - /// Remote-backed rewind routing is not supported. - #[serde(rename = "unsupported-remote-session")] - UnsupportedRemoteSession, +pub enum McpInstallationReviewUninstallAction { + #[serde(rename = "uninstall")] + #[default] + Uninstall, +} + +/// Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum McpInstallationReview { + Install(McpInstallationReviewInstall), + Uninstall(McpInstallationReviewUninstall), +} + +/// Reviewed resource discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum InstallationReviewSkillResource { + #[serde(rename = "skill")] + #[default] + Skill, +} + +/// Exact reviewed Skill installation action. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationReviewInstallAction { + #[serde(rename = "install")] + #[default] + Install, +} + +/// +///
    +/// +/// **Experimental.** This type 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 SkillInstallationScope { + /// The user's personal Copilot home. + #[serde(rename = "personal")] + Personal, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Aggregate file change represented by a rewind preview. +/// Exact reviewed Skill installation action. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationReviewUninstallAction { + #[serde(rename = "uninstall")] + #[default] + Uninstall, +} + +/// Owned Skill state observed from files and receipts. /// ///
    /// @@ -36023,23 +41513,23 @@ pub enum HistoryRewindUnavailableReason { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HistoryRewindChangeType { - /// The discarded turns created the file. - #[serde(rename = "created")] - Created, - /// The discarded turns deleted the file. - #[serde(rename = "deleted")] - Deleted, - /// The discarded turns modified the file. +pub enum SkillInstallationOwnershipState { + /// Owned files and receipt evidence match. + #[serde(rename = "intact")] + Intact, + /// Owned files no longer match the receipt. #[serde(rename = "modified")] Modified, + /// Ownership evidence requires recovery before mutation. + #[serde(rename = "recovery-required")] + RecoveryRequired, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Scope of a rewind operation. +/// Bound-session observation after reconciling persisted enablement. /// ///
    /// @@ -36048,20 +41538,26 @@ pub enum HistoryRewindChangeType { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HistoryRewindMode { - /// Discard conversation events while leaving files unchanged. - #[serde(rename = "conversation")] - Conversation, - /// Discard conversation events and restore captured files changed by those turns. - #[serde(rename = "conversation-and-files")] - ConversationAndFiles, +pub enum SkillInstallationSessionState { + /// The selected session has loaded this Skill and it is enabled. + #[serde(rename = "loaded-enabled")] + LoadedEnabled, + /// The selected session has loaded this Skill or settings and it is disabled. + #[serde(rename = "loaded-disabled")] + LoadedDisabled, + /// The selected session has not loaded Skills after the latest change. + #[serde(rename = "not-loaded")] + NotLoaded, + /// The selected session could not be inspected. + #[serde(rename = "unknown")] + UnknownValue, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Outcome of a rewind request. +/// Safe verified Skill review fields. No raw credential, candidate handle or plan handle. /// ///
    /// @@ -36069,35 +41565,33 @@ pub enum HistoryRewindMode { /// and may change or be removed in future SDK or CLI releases. /// ///
    +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SkillInstallationReview { + Install(SkillInstallationReviewInstall), + Uninstall(SkillInstallationReviewUninstall), +} + +/// Only resource kinds with an implemented installation engine have a review variant. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum InstallationReview { + Mcp(InstallationReviewMcp), + Skill(InstallationReviewSkill), +} + +/// Explicit user decisions, never inferred from a permission grant or model response. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HistoryRewindOutcome { - /// The requested rewind completed; reachable in either mode. - #[serde(rename = "success")] - Success, - /// The session still has work that may mutate files or history; reachable in either mode. - #[serde(rename = "session-busy")] - SessionBusy, - /// A conversation-and-files rewind was requested for a session that did not enable capture; conversation-only rewinds never produce this. - #[serde(rename = "file-change-tracking-disabled")] - FileChangeTrackingDisabled, - /// Remote-backed rewind routing is not supported; reachable in either mode. - #[serde(rename = "unsupported-remote-session")] - UnsupportedRemoteSession, - /// File restore failed and all applied file changes were rolled back; only conversation-and-files rewinds produce this. - #[serde(rename = "files-rolled-back")] - FilesRolledBack, - /// File restore failed and its rollback could not fully restore the pre-rewind state; only conversation-and-files rewinds produce this. - #[serde(rename = "rollback-incomplete")] - RollbackIncomplete, - /// Conversation truncation failed. In conversation-and-files mode any files that were restored are left in place because conversation history cannot be un-truncated; in conversation-only mode no files are restored. Consult restoredFiles for what, if anything, was applied. - #[serde(rename = "truncation-failed")] - TruncationFailed, - /// The conversation was rewound (and, in conversation-and-files mode, captured files were restored), but persisted checkpoints could not be cleaned up; reachable in either mode. - #[serde(rename = "checkpoint-cleanup-failed")] - CheckpointCleanupFailed, - /// Files and conversation were rewound, but obsolete file snapshots could not be removed; only conversation-and-files rewinds produce this. - #[serde(rename = "snapshot-prune-failed")] - SnapshotPruneFailed, +pub enum InstallationDecision { + /// The user explicitly approved the exact review on this request. + #[serde(rename = "confirm")] + Confirm, + /// The user declined the reviewed operation. + #[serde(rename = "decline")] + Decline, + /// The user cancelled the pending decision without granting consent. + #[serde(rename = "cancel")] + Cancel, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -36305,6 +41799,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. /// ///
    @@ -36557,7 +42098,7 @@ pub enum McpHeadersHandlePendingHeadersRefreshRequest { Error(McpHeadersHandlePendingHeadersRefreshRequestError), } -/// Whether a planned configuration change would create or modify an entry +/// Bounded refusal categories, without echoing handles, credentials or configuration. /// ///
    /// @@ -36566,20 +42107,99 @@ pub enum McpHeadersHandlePendingHeadersRefreshRequest { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum McpPlanConfigurationOperation { - /// Creates a configuration entry that does not exist yet. - #[serde(rename = "add")] - Add, - /// Modifies a configuration entry that already exists. - #[serde(rename = "update")] - Update, +pub enum McpInstallationFailureReason { + /// The selected choice or request is unsupported or malformed. + #[serde(rename = "invalid-request")] + InvalidRequest, + /// The bounded original-connection operation limit was reached. + #[serde(rename = "operation-limit")] + OperationLimit, + /// The original operation was cancelled. + #[serde(rename = "cancelled")] + Cancelled, + /// Required installation capabilities were omitted. + #[serde(rename = "capability-required")] + CapabilityRequired, + /// The original host cannot receive human confirmation. + #[serde(rename = "confirmation-unavailable")] + ConfirmationUnavailable, + /// The confirmation response is malformed or mismatched. + #[serde(rename = "confirmation-invalid")] + ConfirmationInvalid, + /// Existing authenticated session and host authority is unavailable. + #[serde(rename = "policy-context-unavailable")] + PolicyContextUnavailable, + /// The original authority or policy changed. + #[serde(rename = "policy-changed")] + PolicyChanged, + /// Current managed policy refuses the operation. + #[serde(rename = "policy-denied")] + PolicyDenied, + /// Configuration changed after the reviewed snapshot. + #[serde(rename = "configuration-changed")] + ConfigurationChanged, + /// Installed configuration no longer matches ownership evidence. + #[serde(rename = "configuration-modified")] + ConfigurationModified, + /// No matching owned resource or original operation exists. + #[serde(rename = "resource-not-found")] + ResourceNotFound, + /// The original plan deadline elapsed. + #[serde(rename = "plan-expired")] + PlanExpired, + /// The one-use plan or prepared operation was already consumed. + #[serde(rename = "plan-replayed")] + PlanReplayed, + /// The handle belongs to a different runtime, session or connection. + #[serde(rename = "foreign-runtime")] + ForeignRuntime, + /// Fresh bound planning is required. + #[serde(rename = "replan-required")] + ReplanRequired, + /// Exact original source revalidation is unsupported. + #[serde(rename = "source-revalidation-unavailable")] + SourceRevalidationUnavailable, + /// The source differs from the retained commitment. + #[serde(rename = "source-changed")] + SourceChanged, + /// The original source could not be retrieved safely. + #[serde(rename = "source-unavailable")] + SourceUnavailable, + /// Authoritative Registry interpretation is unavailable. + #[serde(rename = "registry-unavailable")] + RegistryUnavailable, + /// The selected secret backend is unavailable. + #[serde(rename = "secret-store-unavailable")] + SecretStoreUnavailable, + /// Required owned admission and lifecycle support is absent. + #[serde(rename = "lifecycle-unavailable")] + LifecycleUnavailable, + /// A storage operation failed; inspect any allocated operation before retrying. + #[serde(rename = "write-failed")] + WriteFailed, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Configuration scope an MCP install plan targets +/// Installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementOutcomeRecoveryRequiredKind { + #[serde(rename = "recovery-required")] + #[default] + RecoveryRequired, +} + +/// Installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementOutcomeInstallPreparedKind { + #[serde(rename = "install-prepared")] + #[default] + InstallPrepared, +} + +/// Configuration ownership and setup observations, distinct from tool permissions. /// ///
    /// @@ -36588,17 +42208,160 @@ pub enum McpPlanConfigurationOperation { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum McpPlanScope { - /// The user's own MCP configuration. - #[serde(rename = "user")] - User, +pub enum McpInstallationState { + /// Owned configuration exists; inventory alone does not grant activation. + #[serde(rename = "needs-setup")] + NeedsSetup, + /// The selected authorised session reports an active installation. + #[serde(rename = "active")] + Active, + /// The selected server requires explicit sign-in. + #[serde(rename = "authentication-required")] + AuthenticationRequired, + /// The selected server could not be activated. + #[serde(rename = "activation-failed")] + ActivationFailed, + /// Owned configuration no longer matches its receipt. + #[serde(rename = "configuration-modified")] + ConfigurationModified, + /// Confirmed durable work or unsafe evidence requires recovery. + #[serde(rename = "recovery-required")] + RecoveryRequired, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// What policy decided for a planned server +/// Installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementOutcomeListedKind { + #[serde(rename = "listed")] + #[default] + Listed, +} + +/// Installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementOutcomeRecoveredKind { + #[serde(rename = "recovered")] + #[default] + Recovered, +} + +/// Installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementOutcomeUninstallPlannedKind { + #[serde(rename = "uninstall-planned")] + #[default] + UninstallPlanned, +} + +/// Installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementOutcomeOperationKind { + #[serde(rename = "operation")] + #[default] + Operation, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOperationStatusPreparingPhase { + #[serde(rename = "preparing")] + #[default] + Preparing, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOperationStatusPreparedPhase { + #[serde(rename = "prepared")] + #[default] + Prepared, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOperationStatusAwaitingConfirmationPhase { + #[serde(rename = "awaiting-confirmation")] + #[default] + AwaitingConfirmation, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOperationStatusRevalidatingPhase { + #[serde(rename = "revalidating")] + #[default] + Revalidating, +} + +/// Original operation progress discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOperationStatusApplyingPhase { + #[serde(rename = "applying")] + #[default] + Applying, +} + +/// Terminal installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOutcomeInstalledKind { + #[serde(rename = "installed")] + #[default] + Installed, +} + +/// Terminal installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOutcomeUninstalledKind { + #[serde(rename = "uninstalled")] + #[default] + Uninstalled, +} + +/// Terminal installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOutcomeRolledBackKind { + #[serde(rename = "rolled-back")] + #[default] + RolledBack, +} + +/// Terminal installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOutcomeRecoveryRequiredKind { + #[serde(rename = "recovery-required")] + #[default] + RecoveryRequired, +} + +/// Terminal installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOutcomeDeclinedKind { + #[serde(rename = "declined")] + #[default] + Declined, +} + +/// Terminal installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOutcomeCancelledKind { + #[serde(rename = "cancelled")] + #[default] + Cancelled, +} + +/// Terminal installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationOutcomeRefusedKind { + #[serde(rename = "refused")] + #[default] + Refused, +} + +/// Terminal mutation result. Uncertainty is not approval, rollback or permission to replay. /// ///
    /// @@ -36606,21 +42369,119 @@ pub enum McpPlanScope { /// and may change or be removed in future SDK or CLI releases. /// ///
    +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum McpInstallationOutcome { + Installed(McpInstallationOutcomeInstalled), + Uninstalled(McpInstallationOutcomeUninstalled), + RolledBack(McpInstallationOutcomeRolledBack), + RecoveryRequired(McpInstallationOutcomeRecoveryRequired), + Declined(McpInstallationOutcomeDeclined), + Cancelled(McpInstallationOutcomeCancelled), + Refused(McpInstallationOutcomeRefused), +} + +/// Original operation progress discriminator. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum McpPlanPolicyDecision { - /// Policy permits the server. - #[serde(rename = "allowed")] - Allowed, - /// Policy forbids the server, so the plan cannot be applied. - #[serde(rename = "blocked")] - Blocked, - /// Policy permits the server only after an explicit approval. - #[serde(rename = "requires-approval")] - RequiresApproval, - /// Unknown variant for forward compatibility. +pub enum McpInstallationOperationStatusCompletedPhase { + #[serde(rename = "completed")] #[default] - #[serde(other)] - Unknown, + Completed, +} + +/// Status snapshot from the original connection, independent of new-work account availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum McpInstallationOperationStatus { + Preparing(McpInstallationOperationStatusPreparing), + Prepared(McpInstallationOperationStatusPrepared), + AwaitingConfirmation(McpInstallationOperationStatusAwaitingConfirmation), + Revalidating(McpInstallationOperationStatusRevalidating), + Applying(McpInstallationOperationStatusApplying), + Completed(McpInstallationOperationStatusCompleted), +} + +/// Installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementOutcomeRefusedKind { + #[serde(rename = "refused")] + #[default] + Refused, +} + +/// Read-only or recovery management result, never permission to activate or replay. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum McpInstallationManagementOutcome { + RecoveryRequired(McpInstallationManagementOutcomeRecoveryRequired), + InstallPrepared(McpInstallationManagementOutcomeInstallPrepared), + Listed(McpInstallationManagementOutcomeListed), + Recovered(McpInstallationManagementOutcomeRecovered), + UninstallPlanned(McpInstallationManagementOutcomeUninstallPlanned), + Operation(McpInstallationManagementOutcomeOperation), + Refused(McpInstallationManagementOutcomeRefused), +} + +/// A handled management outcome with an honoured contract. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationManagementResultOutcomeKind { + #[serde(rename = "outcome")] + #[default] + Outcome, +} + +/// Management result with contract receipt, or a typed request/negotiation refusal. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum McpInstallationManagementResult { + Outcome(McpInstallationManagementResultOutcome), + NegotiationRefused(CatalogNegotiationRefusedError), + InvalidRequest(CatalogInvalidRequestError), +} + +/// A handled operation outcome with an honoured contract. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpInstallationResultOutcomeKind { + #[serde(rename = "outcome")] + #[default] + Outcome, +} + +/// An installation result together with the exact honoured contract, or a negotiation refusal. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum McpInstallationResult { + Outcome(McpInstallationResultOutcome), + NegotiationRefused(CatalogNegotiationRefusedError), + InvalidRequest(CatalogInvalidRequestError), } /// Consumer allowed to call an MCP tool. @@ -36698,6 +42559,28 @@ pub enum McpOauthLoginGrantType { Unknown, } +/// Outcome of starting the original prepared owned 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, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpOwnedOauthLoginStatus { + /// The original requester may open the returned authorisation URL. + #[serde(rename = "awaiting-browser")] + AwaitingBrowser, + /// Cached credentials were accepted and the original server finished reconnecting. + #[serde(rename = "connected")] + Connected, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Why a passive MCP OAuth probe determined authentication is needed. /// ///
    @@ -36772,25 +42655,6 @@ pub enum McpOauthProbeResult { Failed(McpOauthProbeResultFailed), } -/// Discriminator for an enumerated required value -/// -///
    -/// -/// **Experimental.** This type 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 McpPlanEnumValueType { - /// One of a fixed, non-empty set of permitted values. - #[serde(rename = "enum")] - Enum, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Discriminator: a plan was computed and nothing was changed #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpPlanInstallPlannedKind { @@ -36931,89 +42795,7 @@ pub enum McpPlanInstallResult { Unavailable(CatalogUnavailableError), } -/// Discriminator for a package-backed transport choice -/// -///
    -/// -/// **Experimental.** This type 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 McpPlanPackageInstallMethod { - /// Install and run a local package. - #[serde(rename = "package")] - Package, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Transport exposed by a locally launched package -/// -///
    -/// -/// **Experimental.** This type 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 McpPlanPackageTransport { - /// A locally launched process spoken to over standard input and output. - #[serde(rename = "stdio")] - Stdio, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Discriminator for a remote-endpoint transport choice -/// -///
    -/// -/// **Experimental.** This type 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 McpPlanRemoteInstallMethod { - /// Connect to a remote endpoint. - #[serde(rename = "remote")] - Remote, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Transport exposed by a remote endpoint -/// -///
    -/// -/// **Experimental.** This type 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 McpPlanRemoteTransport { - /// An HTTP endpoint. - #[serde(rename = "http")] - Http, - /// A streamable HTTP endpoint. - #[serde(rename = "streamable-http")] - StreamableHttp, - /// A server-sent events endpoint. - #[serde(rename = "sse")] - Sse, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Where a required value is applied when the planned server is launched +/// The sender role of an MCP prompt message. /// ///
    /// @@ -37022,109 +42804,19 @@ pub enum McpPlanRemoteTransport { /// ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum McpPlanValueCategory { - /// Set as an environment variable on the launched process. - #[serde(rename = "environment-variable")] - EnvironmentVariable, - /// Passed to the runtime that launches the package. - #[serde(rename = "runtime-argument")] - RuntimeArgument, - /// Passed to the packaged server itself. - #[serde(rename = "package-argument")] - PackageArgument, - /// Sent as a request header to a remote endpoint. - #[serde(rename = "header")] - Header, - /// Substituted into the remote endpoint URL. - #[serde(rename = "url-variable")] - UrlVariable, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Discriminator for a scalar required value -/// -///
    -/// -/// **Experimental.** This type 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 McpPlanRequiredValueScalarKind { - /// The value uses one scalar type. - #[serde(rename = "scalar")] - Scalar, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Scalar type a required value must conform to -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum McpPlanScalarValueType { - /// Free text. - #[serde(rename = "string")] - String, - /// A number. - #[serde(rename = "number")] - Number, - /// A boolean. - #[serde(rename = "boolean")] - Boolean, - /// A filesystem path. - #[serde(rename = "path")] - Path, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Discriminator for an enumerated required value -/// -///
    -/// -/// **Experimental.** This type 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 McpPlanRequiredValueEnumKind { - /// The value uses a fixed non-empty enumeration. - #[serde(rename = "enum")] - Enum, +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, } -/// One non-secret value a transport choice needs, represented as a scalar or enumerated variant so enum values cannot be missing or attached to another 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, Serialize, Deserialize)] -#[serde(untagged)] -pub enum McpPlanRequiredValue { - Scalar(McpPlanRequiredValueScalar), - Enum(McpPlanRequiredValueEnum), -} - /// 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. /// ///
    @@ -37425,7 +43117,7 @@ pub enum ModelPolicyState { Unknown, } -/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// 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. /// ///
    /// @@ -37434,13 +43126,128 @@ pub enum ModelPolicyState { /// ///
    #[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 ModelProviderKind { + /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. + #[serde(rename = "copilot")] + Copilot, + /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. + #[serde(rename = "loki")] + Loki, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// When the runtime may run an adapter without an explicit user action. +/// +///
    +/// +/// **Experimental.** This type 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 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)] @@ -37516,6 +43323,97 @@ pub enum ProviderConfigWireApi { Unknown, } +/// Transport to be used for provider requests. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointTransport { + /// HTTP request/streaming transport. + #[serde(rename = "http")] + Http, + /// WebSocket transport. + #[serde(rename = "websockets")] + Websockets, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Provider family. Matches the `type` field of a BYOK provider config. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointType { + /// OpenAI-compatible endpoint (use the OpenAI client library). + #[serde(rename = "openai")] + Openai, + /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). + #[serde(rename = "azure")] + Azure, + /// Anthropic endpoint (use the Anthropic client library). + #[serde(rename = "anthropic")] + Anthropic, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Wire API to be used, when required for the provider type. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointWireApi { + /// Classic chat-completions request shape. + #[serde(rename = "completions")] + Completions, + /// Newer responses request shape. + #[serde(rename = "responses")] + Responses, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelSwitchAutoTierStatus { + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + #[serde(rename = "unchanged")] + Unchanged, + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + #[serde(rename = "pending")] + Pending, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. /// ///
    @@ -37701,12 +43599,12 @@ pub enum PermissionDecisionApproveForSessionApprovalExtensionManagementKind { ExtensionManagement, } -/// Approval covering factory operations. +/// Approval covering workflow operations. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalFactoryKind { - #[serde(rename = "factory")] +pub enum PermissionDecisionApproveForSessionApprovalWorkflowKind { + #[serde(rename = "workflow")] #[default] - Factory, + Workflow, } /// Approval covering an extension's request to access a permission-gated capability. @@ -37744,7 +43642,7 @@ pub enum PermissionDecisionApproveForSessionApproval { Memory(PermissionDecisionApproveForSessionApprovalMemory), CustomTool(PermissionDecisionApproveForSessionApprovalCustomTool), ExtensionManagement(PermissionDecisionApproveForSessionApprovalExtensionManagement), - Factory(PermissionDecisionApproveForSessionApprovalFactory), + Workflow(PermissionDecisionApproveForSessionApprovalWorkflow), ExtensionPermissionAccess(PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess), ExtensionEnvAccess(PermissionDecisionApproveForSessionApprovalExtensionEnvAccess), } @@ -37757,6 +43655,14 @@ pub enum PermissionDecisionApproveForSessionKind { ApproveForSession, } +/// Approve read-only file-tool directory access for this session +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionDecisionApproveReadOnlyForSessionKind { + #[serde(rename = "approve-read-only-for-session")] + #[default] + ApproveReadOnlyForSession, +} + /// Approval scoped to specific command identifiers. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PermissionDecisionApproveForLocationApprovalCommandsKind { @@ -37821,12 +43727,12 @@ pub enum PermissionDecisionApproveForLocationApprovalExtensionManagementKind { ExtensionManagement, } -/// Approval covering factory operations. +/// Approval covering workflow operations. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalFactoryKind { - #[serde(rename = "factory")] +pub enum PermissionDecisionApproveForLocationApprovalWorkflowKind { + #[serde(rename = "workflow")] #[default] - Factory, + Workflow, } /// Approval covering an extension's request to access a permission-gated capability. @@ -37864,7 +43770,7 @@ pub enum PermissionDecisionApproveForLocationApproval { Memory(PermissionDecisionApproveForLocationApprovalMemory), CustomTool(PermissionDecisionApproveForLocationApprovalCustomTool), ExtensionManagement(PermissionDecisionApproveForLocationApprovalExtensionManagement), - Factory(PermissionDecisionApproveForLocationApprovalFactory), + Workflow(PermissionDecisionApproveForLocationApprovalWorkflow), ExtensionPermissionAccess( PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess, ), @@ -37988,6 +43894,7 @@ pub enum PermissionDecisionDeniedByPermissionRequestHookKind { pub enum PermissionDecision { ApproveOnce(PermissionDecisionApproveOnce), ApproveForSession(PermissionDecisionApproveForSession), + ApproveReadOnlyForSession(PermissionDecisionApproveReadOnlyForSession), ApproveForLocation(PermissionDecisionApproveForLocation), ApprovePermanently(PermissionDecisionApprovePermanently), Reject(PermissionDecisionReject), @@ -38150,12 +44057,12 @@ pub enum PermissionsLocationsAddToolApprovalDetailsExtensionManagementKind { ExtensionManagement, } -/// Approval covering factory operations. +/// Approval covering workflow operations. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionsLocationsAddToolApprovalDetailsFactoryKind { - #[serde(rename = "factory")] +pub enum PermissionsLocationsAddToolApprovalDetailsWorkflowKind { + #[serde(rename = "workflow")] #[default] - Factory, + Workflow, } /// Approval covering an extension's request to access a permission-gated capability. @@ -38193,7 +44100,7 @@ pub enum PermissionsLocationsAddToolApprovalDetails { Memory(PermissionsLocationsAddToolApprovalDetailsMemory), CustomTool(PermissionsLocationsAddToolApprovalDetailsCustomTool), ExtensionManagement(PermissionsLocationsAddToolApprovalDetailsExtensionManagement), - Factory(PermissionsLocationsAddToolApprovalDetailsFactory), + Workflow(PermissionsLocationsAddToolApprovalDetailsWorkflow), ExtensionPermissionAccess(PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess), ExtensionEnvAccess(PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess), } @@ -38220,7 +44127,7 @@ pub enum PermissionLocationType { Unknown, } -/// Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. +/// 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. /// ///
    /// @@ -38242,7 +44149,7 @@ pub enum PermissionModeSource { /// The mode was set at startup by the `defaultPermissionMode` user setting. #[serde(rename = "user_setting")] UserSetting, - /// The mode was set at startup by authenticated organization targeting. + /// The mode was set at startup because the authenticated account matched an organization targeting policy. #[serde(rename = "organization_targeting")] OrganizationTargeting, /// The mode was set through an RPC caller. @@ -38451,75 +44358,6 @@ pub enum ProtocolStaticSectionAction { Unknown, } -/// Transport to be used for provider requests. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointTransport { - /// HTTP request/streaming transport. - #[serde(rename = "http")] - Http, - /// WebSocket transport. - #[serde(rename = "websockets")] - Websockets, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Provider family. Matches the `type` field of a BYOK provider config. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointType { - /// OpenAI-compatible endpoint (use the OpenAI client library). - #[serde(rename = "openai")] - Openai, - /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). - #[serde(rename = "azure")] - Azure, - /// Anthropic endpoint (use the Anthropic client library). - #[serde(rename = "anthropic")] - Anthropic, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Wire API to be used, when required for the provider type. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointWireApi { - /// Classic chat-completions request shape. - #[serde(rename = "completions")] - Completions, - /// Newer responses request shape. - #[serde(rename = "responses")] - Responses, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Attachment type discriminator #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PushAttachmentBlobType { @@ -38836,7 +44674,7 @@ pub enum ResponseFormatType { JsonSchema, } -/// Origin of the sandbox choice supplied by an internal client. +/// Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. /// ///
    /// @@ -38855,7 +44693,7 @@ pub enum SandboxConfigSource { /// The user's persisted settings disabled the sandbox. #[serde(rename = "user_disabled")] UserDisabled, - /// A command-line flag selected the sandbox state for this session. + /// An explicit session-scoped choice selected the sandbox state, such as a command-line flag. #[serde(rename = "session_flag")] SessionFlag, /// The user disabled the sandbox for the current session. @@ -38873,6 +44711,34 @@ pub enum SandboxConfigSource { Unknown, } +/// State of the persistent certificate authority of the sandbox credential proxy. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SandboxProxyCaState { + /// This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. + #[serde(rename = "unsupported")] + Unsupported, + /// OS trust does not include the certificate authority, or none is stored. + #[serde(rename = "notInstalled")] + NotInstalled, + /// OS trust includes the stored certificate authority. + #[serde(rename = "installed")] + Installed, + /// The runtime could not read the certificate authority or the OS trust store. + #[serde(rename = "error")] + Error, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// A session-scoped sandbox transition applied while handling a slash command /// ///
    @@ -39570,6 +45436,7 @@ pub enum SettableAuthInfo { Token(SettableTokenAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } @@ -39744,6 +45611,391 @@ pub enum SkillDiscoveryScope { Unknown, } +/// Bounded refusal categories for verified Skill installation management. +/// +///
    +/// +/// **Experimental.** This type 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 SkillInstallationFailureReason { + /// The selected session does not have the Agent Finder Skill installation feature flag enabled for acquisition. + #[serde(rename = "feature-disabled")] + FeatureDisabled, + /// The request is unsupported or malformed. + #[serde(rename = "invalid-request")] + InvalidRequest, + /// The bounded operation limit was reached. + #[serde(rename = "operation-limit")] + OperationLimit, + /// The original operation was cancelled. + #[serde(rename = "cancelled")] + Cancelled, + /// The original host cannot receive human confirmation. + #[serde(rename = "confirmation-unavailable")] + ConfirmationUnavailable, + /// The confirmation response is malformed or mismatched. + #[serde(rename = "confirmation-invalid")] + ConfirmationInvalid, + /// Existing authenticated session and host authority is unavailable. + #[serde(rename = "policy-context-unavailable")] + PolicyContextUnavailable, + /// The original authority or policy changed. + #[serde(rename = "policy-changed")] + PolicyChanged, + /// No matching owned resource or original operation exists. + #[serde(rename = "resource-not-found")] + ResourceNotFound, + /// The original plan deadline elapsed. + #[serde(rename = "plan-expired")] + PlanExpired, + /// The one-use plan was already consumed. + #[serde(rename = "plan-replayed")] + PlanReplayed, + /// The handle belongs to a different runtime, session or connection. + #[serde(rename = "foreign-runtime")] + ForeignRuntime, + /// The handle is not the expected Skill handle kind. + #[serde(rename = "wrong-kind")] + WrongKind, + /// The handle was minted for a different search result or authority. + #[serde(rename = "search-mismatch")] + SearchMismatch, + /// The handle or operation expired. + #[serde(rename = "expired")] + Expired, + /// The candidate handle is not a verified installable Skill candidate. + #[serde(rename = "invalid-candidate")] + InvalidCandidate, + /// The verified Skill descriptor could not be retrieved safely. + #[serde(rename = "descriptor-unavailable")] + DescriptorUnavailable, + /// The verified Skill descriptor failed validation. + #[serde(rename = "descriptor-invalid")] + DescriptorInvalid, + /// The Skill entrypoint could not be retrieved or verified. + #[serde(rename = "entrypoint-unavailable")] + EntrypointUnavailable, + /// The Skill entrypoint is not a valid Skill. + #[serde(rename = "invalid-skill")] + InvalidSkill, + /// The reviewed Skill payload could not be acquired. + #[serde(rename = "payload-unavailable")] + PayloadUnavailable, + /// The acquired payload no longer matches the reviewed descriptor. + #[serde(rename = "payload-mismatch")] + PayloadMismatch, + /// The retained Skill source changed after planning. + #[serde(rename = "source-changed")] + SourceChanged, + /// The selected runtime cannot inspect or control the requested lifecycle operation. + #[serde(rename = "lifecycle-unavailable")] + LifecycleUnavailable, + /// Durable Skill installation evidence requires recovery before mutation. + #[serde(rename = "recovery-required")] + RecoveryRequired, + /// The Skill entrypoint exceeds the bounded complete review size. + #[serde(rename = "review-too-large")] + ReviewTooLarge, + /// Skill installation storage or admission is busy. + #[serde(rename = "busy")] + Busy, + /// An owned Skill with the same identity or target already exists. + #[serde(rename = "already-installed")] + AlreadyInstalled, + /// Installed Skill files no longer match ownership evidence. + #[serde(rename = "configuration-modified")] + ConfigurationModified, + /// A storage operation failed; inspect durable state before retrying. + #[serde(rename = "write-failed")] + WriteFailed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeRecoveryRequiredKind { + #[serde(rename = "recovery-required")] + #[default] + RecoveryRequired, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeInstallPlannedKind { + #[serde(rename = "install-planned")] + #[default] + InstallPlanned, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeListedKind { + #[serde(rename = "listed")] + #[default] + Listed, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeRecoveredKind { + #[serde(rename = "recovered")] + #[default] + Recovered, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeRolledBackKind { + #[serde(rename = "rolled-back")] + #[default] + RolledBack, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeUninstallPlannedKind { + #[serde(rename = "uninstall-planned")] + #[default] + UninstallPlanned, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeOperationKind { + #[serde(rename = "operation")] + #[default] + Operation, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOperationStatusPreparingPhase { + #[serde(rename = "preparing")] + #[default] + Preparing, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOperationStatusPreparedPhase { + #[serde(rename = "prepared")] + #[default] + Prepared, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOperationStatusAwaitingConfirmationPhase { + #[serde(rename = "awaiting-confirmation")] + #[default] + AwaitingConfirmation, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOperationStatusRevalidatingPhase { + #[serde(rename = "revalidating")] + #[default] + Revalidating, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOperationStatusApplyingPhase { + #[serde(rename = "applying")] + #[default] + Applying, +} + +/// Terminal Skill installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOutcomeInstalledKind { + #[serde(rename = "installed")] + #[default] + Installed, +} + +/// Terminal Skill installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOutcomeUninstalledKind { + #[serde(rename = "uninstalled")] + #[default] + Uninstalled, +} + +/// Terminal Skill installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOutcomeRolledBackKind { + #[serde(rename = "rolled-back")] + #[default] + RolledBack, +} + +/// Terminal Skill installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOutcomeRecoveryRequiredKind { + #[serde(rename = "recovery-required")] + #[default] + RecoveryRequired, +} + +/// Terminal Skill installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOutcomeDeclinedKind { + #[serde(rename = "declined")] + #[default] + Declined, +} + +/// Terminal Skill installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOutcomeCancelledKind { + #[serde(rename = "cancelled")] + #[default] + Cancelled, +} + +/// Terminal Skill installation outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOutcomeRefusedKind { + #[serde(rename = "refused")] + #[default] + Refused, +} + +/// Terminal verified Skill mutation result. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum SkillInstallationOutcome { + Installed(SkillInstallationOutcomeInstalled), + Uninstalled(SkillInstallationOutcomeUninstalled), + RolledBack(SkillInstallationOutcomeRolledBack), + RecoveryRequired(SkillInstallationOutcomeRecoveryRequired), + Declined(SkillInstallationOutcomeDeclined), + Cancelled(SkillInstallationOutcomeCancelled), + Refused(SkillInstallationOutcomeRefused), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationOperationStatusCompletedPhase { + #[serde(rename = "completed")] + #[default] + Completed, +} + +/// Status snapshot from the original connection, independent of new-work account availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SkillInstallationOperationStatus { + Preparing(SkillInstallationOperationStatusPreparing), + Prepared(SkillInstallationOperationStatusPrepared), + AwaitingConfirmation(SkillInstallationOperationStatusAwaitingConfirmation), + Revalidating(SkillInstallationOperationStatusRevalidating), + Applying(SkillInstallationOperationStatusApplying), + Completed(SkillInstallationOperationStatusCompleted), +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeEnabledChangedKind { + #[serde(rename = "enabled-changed")] + #[default] + EnabledChanged, +} + +/// Skill installation management outcome discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementOutcomeRefusedKind { + #[serde(rename = "refused")] + #[default] + Refused, +} + +/// Management outcome for verified Skill inventory, planning and removal. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum SkillInstallationManagementOutcome { + RecoveryRequired(SkillInstallationManagementOutcomeRecoveryRequired), + InstallPlanned(SkillInstallationManagementOutcomeInstallPlanned), + Listed(SkillInstallationManagementOutcomeListed), + Recovered(SkillInstallationManagementOutcomeRecovered), + RolledBack(SkillInstallationManagementOutcomeRolledBack), + UninstallPlanned(SkillInstallationManagementOutcomeUninstallPlanned), + Operation(SkillInstallationManagementOutcomeOperation), + EnabledChanged(SkillInstallationManagementOutcomeEnabledChanged), + Refused(SkillInstallationManagementOutcomeRefused), +} + +/// Discriminator: handled management outcome. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationManagementResultOutcomeKind { + #[serde(rename = "outcome")] + #[default] + Outcome, +} + +/// Skill installation management result with the honoured contract, or a typed refusal. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum SkillInstallationManagementResult { + Outcome(SkillInstallationManagementResultOutcome), + NegotiationRefused(CatalogNegotiationRefusedError), + InvalidRequest(CatalogInvalidRequestError), +} + +/// Discriminator: handled operation outcome. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInstallationResultOutcomeKind { + #[serde(rename = "outcome")] + #[default] + Outcome, +} + +/// Skill installation result with the honoured contract, or a typed request/negotiation refusal. +/// +///
    +/// +/// **Experimental.** This 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, Deserialize)] +#[serde(untagged)] +pub enum SkillInstallationResult { + Outcome(SkillInstallationResultOutcome), + NegotiationRefused(CatalogNegotiationRefusedError), + InvalidRequest(CatalogInvalidRequestError), +} + /// Discriminator for an add-timeline-entry result. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SlashCommandAddTimelineEntryResultKind { @@ -40685,3 +46937,19 @@ pub enum SessionDiagnosticsReadResultEntriesItemSource { #[default] Mcp, } + +/// Compatibility alias for the confirmation review wrapper. +pub type InstallationConfirmationRequestReview = InstallationReview; + +/// Resource discriminator for installation confirmation reviews. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum InstallationReviewResource { + #[serde(rename = "mcp")] + #[default] + Mcp, + #[serde(rename = "skill")] + Skill, +} + +/// Compatibility alias for the confirmation review resource discriminator. +pub type InstallationConfirmationRequestReviewResource = InstallationReviewResource; diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 5f9e03c499..98590e2ad9 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -57,6 +57,20 @@ impl<'a> ClientRpc<'a> { } } + /// `connectors.*` sub-namespace. + pub fn connectors(&self) -> ClientRpcConnectors<'a> { + ClientRpcConnectors { + client: self.client, + } + } + + /// `environments.*` sub-namespace. + pub fn environments(&self) -> ClientRpcEnvironments<'a> { + ClientRpcEnvironments { + client: self.client, + } + } + /// `extensions.*` sub-namespace. pub fn extensions(&self) -> ClientRpcExtensions<'a> { ClientRpcExtensions { @@ -64,6 +78,34 @@ impl<'a> ClientRpc<'a> { } } + /// `git.*` sub-namespace. + pub fn git(&self) -> ClientRpcGit<'a> { + ClientRpcGit { + client: self.client, + } + } + + /// `gitHubOwners.*` sub-namespace. + pub fn git_hub_owners(&self) -> ClientRpcGitHubOwners<'a> { + ClientRpcGitHubOwners { + client: self.client, + } + } + + /// `gitHubRepository.*` sub-namespace. + pub fn git_hub_repository(&self) -> ClientRpcGitHubRepository<'a> { + ClientRpcGitHubRepository { + client: self.client, + } + } + + /// `globalState.*` sub-namespace. + pub fn global_state(&self) -> ClientRpcGlobalState<'a> { + ClientRpcGlobalState { + client: self.client, + } + } + /// `hooks.*` sub-namespace. pub fn hooks(&self) -> ClientRpcHooks<'a> { ClientRpcHooks { @@ -71,6 +113,13 @@ impl<'a> ClientRpc<'a> { } } + /// `host.*` sub-namespace. + pub fn host(&self) -> ClientRpcHost<'a> { + ClientRpcHost { + client: self.client, + } + } + /// `instructions.*` sub-namespace. pub fn instructions(&self) -> ClientRpcInstructions<'a> { ClientRpcInstructions { @@ -520,26 +569,14 @@ impl<'a> ClientRpcAgents<'a> { .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `catalog.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcCatalog<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcCatalog<'a> { - /// 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. - /// - /// Wire method: `catalog.search`. - /// - /// # Parameters + /// Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. /// - /// * `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. + /// Wire method: `agents.getBuiltins`. /// /// # Returns /// - /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. + /// The agents this runtime ships, named so a consumer can tell them apart from authored ones. /// ///
    /// @@ -548,26 +585,26 @@ impl<'a> ClientRpcCatalog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn search(&self, params: CatalogSearchRequest) -> Result { - let wire_params = serde_json::to_value(params)?; + pub(crate) async fn get_builtins(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::CATALOG_SEARCH, Some(wire_params)) + .call(rpc_methods::AGENTS_GETBUILTINS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. /// - /// Wire method: `catalog.select`. + /// Wire method: `agents.getAvailableBuiltins`. /// /// # Parameters /// - /// * `params` - Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + /// * `params` - The feature flags to evaluate shipped agents against. /// /// # Returns /// - /// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. + /// The shipped agents available under the requested flags. /// ///
    /// @@ -576,33 +613,29 @@ impl<'a> ClientRpcCatalog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn select( + pub(crate) async fn get_available_builtins( &self, - params: CatalogSelectionRequest, - ) -> Result { + params: AgentsGetAvailableBuiltinsRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::CATALOG_SELECT, Some(wire_params)) + .call(rpc_methods::AGENTS_GETAVAILABLEBUILTINS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `commands.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcCommands<'a> { - pub(crate) client: &'a Client, -} - -impl<'a> ClientRpcCommands<'a> { - /// 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. + /// Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. /// - /// Wire method: `commands.list`. + /// Wire method: `agents.getBuiltinDefinition`. + /// + /// # Parameters + /// + /// * `params` - The shipped agent whose definition to load. /// /// # Returns /// - /// Slash commands available in the session, after applying any include/exclude filters. + /// One shipped agent's definition. /// ///
    /// @@ -611,30 +644,29 @@ impl<'a> ClientRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({}); + pub(crate) async fn get_builtin_definition( + &self, + params: AgentsGetBuiltinDefinitionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::COMMANDS_LIST, Some(wire_params)) + .call(rpc_methods::AGENTS_GETBUILTINDEFINITION, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `extensions.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcExtensions<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcExtensions<'a> { - /// Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. + /// Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. /// - /// Wire method: `extensions.discover`. + /// Wire method: `agents.getBuiltinListingDefinition`. + /// + /// # Parameters + /// + /// * `params` - The shipped agent whose listing entry to load. /// /// # Returns /// - /// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + /// One shipped agent, projected for a listing. /// ///
    /// @@ -643,22 +675,32 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn discover(&self) -> Result { - let wire_params = serde_json::json!({}); + pub(crate) async fn get_builtin_listing_definition( + &self, + params: AgentsGetBuiltinListingDefinitionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::EXTENSIONS_DISCOVER, Some(wire_params)) + .call( + rpc_methods::AGENTS_GETBUILTINLISTINGDEFINITION, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. + /// Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. /// - /// Wire method: `extensions.enable`. + /// Wire method: `agents.customAgentInitialModelDecision`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifiers to persistently enable for future sessions. + /// * `params` - The models a custom agent asks for, and the models actually available. + /// + /// # Returns + /// + /// The model to switch to, and the warning to show when the agent's preference could not be met. /// ///
    /// @@ -667,22 +709,40 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn enable(&self, params: DiscoveredExtensionsEnableRequest) -> Result<(), Error> { + pub(crate) async fn custom_agent_initial_model_decision( + &self, + params: AgentsCustomAgentInitialModelDecisionParams, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::EXTENSIONS_ENABLE, Some(wire_params)) + .call( + rpc_methods::AGENTS_CUSTOMAGENTINITIALMODELDECISION, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } +} - /// Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. +/// `catalog.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcCatalog<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcCatalog<'a> { + /// 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. /// - /// Wire method: `extensions.disable`. + /// Wire method: `catalog.search`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifiers to persistently disable for future sessions. + /// * `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. + /// + /// # Returns + /// + /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. /// ///
    /// @@ -691,34 +751,26 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn disable(&self, params: DiscoveredExtensionsDisableRequest) -> Result<(), Error> { + pub async fn search(&self, params: CatalogSearchRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::EXTENSIONS_DISABLE, Some(wire_params)) + .call(rpc_methods::CATALOG_SEARCH, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } -} - -/// `hooks.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcHooks<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcHooks<'a> { - /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + /// 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. /// - /// Wire method: `hooks.discover`. + /// Wire method: `catalog.search`. /// /// # Parameters /// - /// * `params` - Optional project paths and host-exclusion behavior for server-scoped hook discovery. + /// * `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. /// /// # Returns /// - /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. /// ///
    /// @@ -727,37 +779,31 @@ impl<'a> ClientRpcHooks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn discover( + /// + /// Accepts [`CatalogSearchOptions`], including inputs added after [`CatalogSearchRequest`]. + pub async fn search_with_options( &self, - params: HooksDiscoverRequest, - ) -> Result { + params: CatalogSearchOptions, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::HOOKS_DISCOVER, Some(wire_params)) + .call(rpc_methods::CATALOG_SEARCH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `instructions.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcInstructions<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcInstructions<'a> { - /// Discovers instruction sources across user, repository, and plugin sources. + /// 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. /// - /// Wire method: `instructions.discover`. + /// Wire method: `catalog.select`. /// /// # Parameters /// - /// * `params` - Optional project paths to include in instruction discovery. + /// * `params` - Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. /// /// # Returns /// - /// Instruction sources discovered across user, repository, and plugin sources. + /// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. /// ///
    /// @@ -766,29 +812,33 @@ impl<'a> ClientRpcInstructions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn discover( + pub async fn select( &self, - params: InstructionsDiscoverRequest, - ) -> Result { + params: CatalogSelectionRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::INSTRUCTIONS_DISCOVER, Some(wire_params)) + .call(rpc_methods::CATALOG_SELECT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// 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. - /// - /// Wire method: `instructions.getDiscoveryPaths`. - /// - /// # Parameters +/// `commands.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcCommands<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcCommands<'a> { + /// 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. /// - /// * `params` - Optional project paths to include when enumerating instruction discovery targets. + /// Wire method: `commands.list`. /// /// # Returns /// - /// Canonical files and directories where custom instructions can be created so the runtime will recognize them. + /// Slash commands available in the session, after applying any include/exclude filters. /// ///
    /// @@ -797,36 +847,30 @@ impl<'a> ClientRpcInstructions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_discovery_paths( - &self, - params: InstructionsGetDiscoveryPathsRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call( - rpc_methods::INSTRUCTIONS_GETDISCOVERYPATHS, - Some(wire_params), - ) + .call(rpc_methods::COMMANDS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `llmInference.*` RPCs. +/// `connectors.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcLlmInference<'a> { +pub struct ClientRpcConnectors<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcLlmInference<'a> { - /// Registers an SDK client as the LLM inference callback provider. +impl<'a> ClientRpcConnectors<'a> { + /// Returns feature availability. /// - /// Wire method: `llmInference.setProvider`. + /// Wire method: `connectors.getCapabilities`. /// /// # Returns /// - /// Indicates whether the calling client was registered as the LLM inference provider. + /// Feature availability. /// ///
    /// @@ -835,26 +879,22 @@ impl<'a> ClientRpcLlmInference<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set_provider(&self) -> Result { + pub async fn get_capabilities(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::LLMINFERENCE_SETPROVIDER, Some(wire_params)) + .call(rpc_methods::CONNECTORS_GETCAPABILITIES, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. - /// - /// Wire method: `llmInference.httpResponseStart`. - /// - /// # Parameters + /// Returns eligible accounts. /// - /// * `params` - Response head. + /// Wire method: `connectors.getAccounts`. /// /// # Returns /// - /// Whether the start frame was accepted. + /// Eligible accounts. /// ///
    /// @@ -863,32 +903,26 @@ impl<'a> ClientRpcLlmInference<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn http_response_start( - &self, - params: LlmInferenceHttpResponseStartRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + pub async fn get_accounts(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call( - rpc_methods::LLMINFERENCE_HTTPRESPONSESTART, - Some(wire_params), - ) + .call(rpc_methods::CONNECTORS_GETACCOUNTS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// Lists entries for the selected account. /// - /// Wire method: `llmInference.httpResponseChunk`. + /// Wire method: `connectors.list`. /// /// # Parameters /// - /// * `params` - A response body chunk or terminal error. + /// * `params` - Selected account. /// /// # Returns /// - /// Whether the chunk was accepted. + /// Entries for the selected account. /// ///
    /// @@ -897,36 +931,29 @@ impl<'a> ClientRpcLlmInference<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn http_response_chunk( + pub async fn list( &self, - params: LlmInferenceHttpResponseChunkRequest, - ) -> Result { + params: ConnectorDiscoveryAccountRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call( - rpc_methods::LLMINFERENCE_HTTPRESPONSECHUNK, - Some(wire_params), - ) + .call(rpc_methods::CONNECTORS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `managedSettings.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcManagedSettings<'a> { - pub(crate) client: &'a Client, -} - -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. + /// Refreshes entries for the selected account. /// - /// Wire method: `managedSettings.read`. + /// Wire method: `connectors.refresh`. + /// + /// # Parameters + /// + /// * `params` - Selected account. /// /// # Returns /// - /// Validated device-managed settings discovered before a session exists. + /// Entries for the selected account. /// ///
    /// @@ -935,18 +962,37 @@ impl<'a> ClientRpcManagedSettings<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn read(&self) -> Result { - let wire_params = serde_json::json!({}); + pub async fn refresh( + &self, + params: ConnectorDiscoveryAccountRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MANAGEDSETTINGS_READ, Some(wire_params)) + .call(rpc_methods::CONNECTORS_REFRESH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} + +/// `environments.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcEnvironments<'a> { + pub(crate) client: &'a Client, +} - /// 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. +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: `managedSettings.clearCache`. + /// 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. /// ///
    /// @@ -955,41 +1001,29 @@ impl<'a> ClientRpcManagedSettings<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn clear_cache(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({}); + pub async fn list( + &self, + params: EnvironmentsListRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MANAGEDSETTINGS_CLEARCACHE, Some(wire_params)) + .call(rpc_methods::ENVIRONMENTS_LIST, Some(wire_params)) .await?; - Ok(()) - } -} - -/// `mcp.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcMcp<'a> { - pub(crate) client: &'a Client, -} - -impl<'a> ClientRpcMcp<'a> { - /// `mcp.config.*` sub-namespace. - pub fn config(&self) -> ClientRpcMcpConfig<'a> { - ClientRpcMcpConfig { - client: self.client, - } + Ok(serde_json::from_value(_value)?) } - /// Discovers MCP servers from user, workspace, plugin, and builtin sources. + /// Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. /// - /// Wire method: `mcp.discover`. + /// Wire method: `environments.get`. /// /// # Parameters /// - /// * `params` - Optional working directory used as context for MCP server discovery. + /// * `params` - Identify a Mission Control environment to retrieve. /// /// # Returns /// - /// MCP servers discovered from user, workspace, plugin, and built-in sources. + /// Safe discovery information for the requested environment. /// ///
    /// @@ -998,26 +1032,29 @@ impl<'a> ClientRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn discover(&self, params: McpDiscoverRequest) -> Result { + pub async fn get( + &self, + params: EnvironmentsGetRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MCP_DISCOVER, Some(wire_params)) + .call(rpc_methods::ENVIRONMENTS_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// 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: `mcp.planInstall`. + /// Wire method: `environments.delete`. /// /// # Parameters /// - /// * `params` - A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + /// * `params` - Identify a user-managed Mission Control environment to delete. /// /// # Returns /// - /// Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. + /// Acknowledgement that the requested environment was deleted. /// ///
    /// @@ -1026,33 +1063,33 @@ impl<'a> ClientRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn plan_install( + pub async fn delete( &self, - params: McpPlanInstallRequest, - ) -> Result { + params: EnvironmentsDeleteRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MCP_PLANINSTALL, Some(wire_params)) + .call(rpc_methods::ENVIRONMENTS_DELETE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `mcp.config.*` RPCs. +/// `extensions.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcMcpConfig<'a> { +pub struct ClientRpcExtensions<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcMcpConfig<'a> { - /// Lists MCP servers from user configuration. +impl<'a> ClientRpcExtensions<'a> { + /// Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. /// - /// Wire method: `mcp.config.list`. + /// Wire method: `extensions.discover`. /// /// # Returns /// - /// User-configured MCP servers, keyed by server name. + /// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. /// ///
    /// @@ -1061,22 +1098,22 @@ impl<'a> ClientRpcMcpConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { + pub async fn discover(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::MCP_CONFIG_LIST, Some(wire_params)) + .call(rpc_methods::EXTENSIONS_DISCOVER, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Adds an MCP server to user configuration. + /// Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. /// - /// Wire method: `mcp.config.add`. + /// Wire method: `extensions.enable`. /// /// # Parameters /// - /// * `params` - MCP server name and configuration to add to user configuration. + /// * `params` - Source-qualified extension identifiers to persistently enable for future sessions. /// ///
    /// @@ -1085,22 +1122,22 @@ impl<'a> ClientRpcMcpConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn add(&self, params: McpConfigAddRequest) -> Result<(), Error> { + pub async fn enable(&self, params: DiscoveredExtensionsEnableRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MCP_CONFIG_ADD, Some(wire_params)) + .call(rpc_methods::EXTENSIONS_ENABLE, Some(wire_params)) .await?; Ok(()) } - /// Updates an MCP server in user configuration. + /// Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. /// - /// Wire method: `mcp.config.update`. + /// Wire method: `extensions.disable`. /// /// # Parameters /// - /// * `params` - MCP server name and replacement configuration to write to user configuration. + /// * `params` - Source-qualified extension identifiers to persistently disable for future sessions. /// ///
    /// @@ -1109,22 +1146,34 @@ impl<'a> ClientRpcMcpConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn update(&self, params: McpConfigUpdateRequest) -> Result<(), Error> { + pub async fn disable(&self, params: DiscoveredExtensionsDisableRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MCP_CONFIG_UPDATE, Some(wire_params)) + .call(rpc_methods::EXTENSIONS_DISABLE, Some(wire_params)) .await?; Ok(()) } +} - /// Removes an MCP server from user configuration. +/// `git.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcGit<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcGit<'a> { + /// Reads the remote that the branch checked out in a working tree tracks, as `branch..remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. /// - /// Wire method: `mcp.config.remove`. + /// Wire method: `git.currentBranchRemote`. /// /// # Parameters /// - /// * `params` - MCP server name to remove from user configuration. + /// * `params` - Working-tree path a git query applies to. + /// + /// # Returns + /// + /// The remote the checked-out branch tracks. /// ///
    /// @@ -1133,22 +1182,29 @@ impl<'a> ClientRpcMcpConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn remove(&self, params: McpConfigRemoveRequest) -> Result<(), Error> { + pub(crate) async fn current_branch_remote( + &self, + params: GitCwdRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MCP_CONFIG_REMOVE, Some(wire_params)) + .call(rpc_methods::GIT_CURRENTBRANCHREMOTE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Enables MCP servers in user configuration for new sessions. + /// Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. /// - /// Wire method: `mcp.config.enable`. + /// Wire method: `git.workingDirectoryContext`. /// /// # Parameters /// - /// * `params` - MCP server names to enable for new sessions. + /// * `params` - Working-tree path a git query applies to. + /// + /// # Returns + /// + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. /// ///
    /// @@ -1157,22 +1213,29 @@ impl<'a> ClientRpcMcpConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn enable(&self, params: McpConfigEnableRequest) -> Result<(), Error> { + pub(crate) async fn working_directory_context( + &self, + params: GitCwdRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MCP_CONFIG_ENABLE, Some(wire_params)) + .call(rpc_methods::GIT_WORKINGDIRECTORYCONTEXT, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Disables MCP servers in user configuration for new sessions. + /// Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. /// - /// Wire method: `mcp.config.disable`. + /// Wire method: `git.reposFromRemotes`. /// /// # Parameters /// - /// * `params` - MCP server names to disable for new sessions. + /// * `params` - Git working tree whose GitHub remotes should be listed. + /// + /// # Returns + /// + /// The GitHub repositories a working tree's remotes point at. /// ///
    /// @@ -1181,18 +1244,33 @@ impl<'a> ClientRpcMcpConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn disable(&self, params: McpConfigDisableRequest) -> Result<(), Error> { + pub(crate) async fn repos_from_remotes( + &self, + params: GitReposFromRemotesRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MCP_CONFIG_DISABLE, Some(wire_params)) + .call(rpc_methods::GIT_REPOSFROMREMOTES, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } +} - /// Drops this runtime process's in-memory MCP server-definition cache so the next MCP config read observes disk. +/// `gitHubOwners.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcGitHubOwners<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcGitHubOwners<'a> { + /// Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. /// - /// Wire method: `mcp.config.reload`. + /// Wire method: `gitHubOwners.nextRequestId`. + /// + /// # Returns + /// + /// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. /// ///
    /// @@ -1201,30 +1279,26 @@ impl<'a> ClientRpcMcpConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reload(&self) -> Result<(), Error> { + pub(crate) async fn next_request_id(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::MCP_CONFIG_RELOAD, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_NEXTREQUESTID, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } -} - -/// `models.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcModels<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcModels<'a> { - /// Lists Copilot models available to the authenticated user. + /// Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. /// - /// Wire method: `models.list`. + /// Wire method: `gitHubOwners.list`. + /// + /// # Parameters + /// + /// * `params` - Credential to list owners under, and the request id that makes the listing cancellable. /// /// # Returns /// - /// List of Copilot models available to the resolved user, including capabilities and billing metadata. + /// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. /// ///
    /// @@ -1233,26 +1307,29 @@ impl<'a> ClientRpcModels<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({}); + pub(crate) async fn list( + &self, + params: GitHubOwnersListRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MODELS_LIST, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists Copilot models available to the authenticated user. + /// Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. /// - /// Wire method: `models.list`. + /// Wire method: `gitHubOwners.cancel`. /// /// # Parameters /// - /// * `params` - Optional opaque account selection or compatibility GitHub token used to list models. + /// * `params` - The owner listing to abandon. /// /// # Returns /// - /// List of Copilot models available to the resolved user, including capabilities and billing metadata. + /// Whether the id named a running owner listing. /// ///
    /// @@ -1261,22 +1338,37 @@ impl<'a> ClientRpcModels<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list_with_params(&self, params: ModelsListRequest) -> Result { + pub(crate) async fn cancel( + &self, + params: GitHubOwnersCancelRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MODELS_LIST, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_CANCEL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Returns the running runtime's complete catalog of well-known built-in model IDs without authentication or network access. +/// `gitHubRepository.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcGitHubRepository<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcGitHubRepository<'a> { + /// Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. /// - /// Wire method: `models.getBuiltInCatalog`. + /// Wire method: `gitHubRepository.atPath`. + /// + /// # Parameters + /// + /// * `params` - Working-tree path whose owning GitHub repository should be resolved. /// /// # Returns /// - /// The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. + /// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. /// ///
    /// @@ -1285,44 +1377,33 @@ impl<'a> ClientRpcModels<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_built_in_catalog(&self) -> Result { - let wire_params = serde_json::json!({}); + pub(crate) async fn at_path( + &self, + params: GitHubRepositoryAtPathRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::MODELS_GETBUILTINCATALOG, Some(wire_params)) + .call(rpc_methods::GITHUBREPOSITORY_ATPATH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `plugins.*` RPCs. +/// `globalState.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcPlugins<'a> { +pub struct ClientRpcGlobalState<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcPlugins<'a> { - /// `plugins.builtin.*` sub-namespace. - pub fn builtin(&self) -> ClientRpcPluginsBuiltin<'a> { - ClientRpcPluginsBuiltin { - client: self.client, - } - } - - /// `plugins.marketplaces.*` sub-namespace. - pub fn marketplaces(&self) -> ClientRpcPluginsMarketplaces<'a> { - ClientRpcPluginsMarketplaces { - client: self.client, - } - } - - /// Lists plugins installed in user/global state. +impl<'a> ClientRpcGlobalState<'a> { + /// Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. /// - /// Wire method: `plugins.list`. + /// Wire method: `globalState.load`. /// /// # Returns /// - /// Plugins installed in user/global state. + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
    /// @@ -1331,26 +1412,26 @@ impl<'a> ClientRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { + pub(crate) async fn load(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::PLUGINS_LIST, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_LOAD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Installs a plugin from a marketplace, GitHub repo, URL, or local path. + /// Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. /// - /// Wire method: `plugins.install`. + /// Wire method: `globalState.loadForConfigDir`. /// /// # Parameters /// - /// * `params` - Plugin source and optional working directory for relative-path resolution. + /// * `params` - Selects the configuration directory whose machine-wide state to read. /// /// # Returns /// - /// Result of installing a plugin. + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
    /// @@ -1359,25 +1440,25 @@ impl<'a> ClientRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn install( + pub(crate) async fn load_for_config_dir( &self, - params: PluginsInstallRequest, - ) -> Result { + params: GlobalStateLoadForConfigDirRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_INSTALL, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_LOADFORCONFIGDIR, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Uninstalls an installed plugin. + /// Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. /// - /// Wire method: `plugins.uninstall`. + /// Wire method: `globalState.writeKey`. /// /// # Parameters /// - /// * `params` - Name (or spec) of the plugin to uninstall. + /// * `params` - A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. /// ///
    /// @@ -1386,26 +1467,34 @@ impl<'a> ClientRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn uninstall(&self, params: PluginsUninstallRequest) -> Result<(), Error> { + pub(crate) async fn write_key(&self, params: GlobalStateWriteKeyRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_UNINSTALL, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_WRITEKEY, Some(wire_params)) .await?; Ok(()) } +} - /// Updates an installed plugin to its latest published version. +/// `hooks.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcHooks<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcHooks<'a> { + /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. /// - /// Wire method: `plugins.update`. + /// Wire method: `hooks.discover`. /// /// # Parameters /// - /// * `params` - Name (or spec) of the plugin to update. + /// * `params` - Optional project paths and host-exclusion behavior for server-scoped hook discovery. /// /// # Returns /// - /// Result of updating a single plugin. + /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. /// ///
    /// @@ -1414,22 +1503,37 @@ impl<'a> ClientRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn update(&self, params: PluginsUpdateRequest) -> Result { + pub async fn discover( + &self, + params: HooksDiscoverRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_UPDATE, Some(wire_params)) + .call(rpc_methods::HOOKS_DISCOVER, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Updates every installed plugin to its latest published version. +/// `host.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcHost<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcHost<'a> { + /// Publishes an attached resident session for this listener's lifetime without copying it. /// - /// Wire method: `plugins.updateAll`. + /// Wire method: `host.publishSession`. + /// + /// # Parameters + /// + /// * `params` - Publishes a resident session attached to the listener's owning connection. /// /// # Returns /// - /// Result of updating all installed plugins. + /// The existing runtime identity and its resource on the listener. /// ///
    /// @@ -1438,22 +1542,29 @@ impl<'a> ClientRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn update_all(&self) -> Result { - let wire_params = serde_json::json!({}); + pub async fn publish_session( + &self, + params: HostPublishSessionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_UPDATEALL, Some(wire_params)) + .call(rpc_methods::HOST_PUBLISHSESSION, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Enables installed plugins for new sessions. + /// Requests app-owned materialization over the owning SDK participant. /// - /// Wire method: `plugins.enable`. + /// Wire method: `host.createSession`. /// /// # Parameters /// - /// * `params` - Plugin names (or specs) to enable, plus the optional working directory the repository-controlled guard is evaluated against. + /// * `params` - One application-owned session handoff, requested by the supervised hosting participant. + /// + /// # Returns + /// + /// The resident session the application has materialized on its own connection. /// ///
    /// @@ -1462,22 +1573,29 @@ impl<'a> ClientRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn enable(&self, params: PluginsEnableRequest) -> Result<(), Error> { + pub(crate) async fn create_session( + &self, + params: HostSessionCreateRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_ENABLE, Some(wire_params)) + .call(rpc_methods::HOST_CREATESESSION, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Disables installed plugins for new sessions. + /// Releases app ownership retention after AHP detaches. /// - /// Wire method: `plugins.disable`. + /// Wire method: `host.releaseSession`. /// /// # Parameters /// - /// * `params` - Plugin names (or specs) to disable, plus the optional working directory the repository-controlled guard is evaluated against. + /// * `params` - Ends one participation, not the application's session lifetime. + /// + /// # Returns + /// + /// Empty acknowledgement for a completed host lifecycle operation. /// ///
    /// @@ -1486,30 +1604,29 @@ impl<'a> ClientRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn disable(&self, params: PluginsDisableRequest) -> Result<(), Error> { + pub(crate) async fn release_session( + &self, + params: HostSessionReleaseRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_DISABLE, Some(wire_params)) + .call(rpc_methods::HOST_RELEASESESSION, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } -} -/// `plugins.builtin.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcPluginsBuiltin<'a> { - pub(crate) client: &'a Client, -} - -impl<'a> ClientRpcPluginsBuiltin<'a> { - /// Replaces this server's trusted built-in plugin directories while no sessions are active. + /// Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. /// - /// Wire method: `plugins.builtin.set`. + /// Wire method: `host.start`. /// /// # Parameters /// - /// * `params` - Trusted built-in plugin directories to use for this runtime process. + /// * `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. /// ///
    /// @@ -1518,30 +1635,26 @@ impl<'a> ClientRpcPluginsBuiltin<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set(&self, params: PluginsBuiltinSetRequest) -> Result<(), Error> { + pub async fn start(&self, params: HostStartRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_BUILTIN_SET, Some(wire_params)) + .call(rpc_methods::HOST_START, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } -} - -/// `plugins.marketplaces.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcPluginsMarketplaces<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcPluginsMarketplaces<'a> { - /// Lists all registered marketplaces (defaults + user-added). + /// Stops a listener owned by this SDK connection and joins its cleanup without deleting sessions. /// - /// Wire method: `plugins.marketplaces.list`. + /// Wire method: `host.dispose`. + /// + /// # Parameters + /// + /// * `params` - Stops a connection-owned listener and joins its teardown. /// /// # Returns /// - /// All registered marketplaces, including built-in defaults. + /// Empty acknowledgement for a completed host lifecycle operation. /// ///
    /// @@ -1550,26 +1663,22 @@ impl<'a> ClientRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({}); + pub async fn dispose(&self, params: HostDisposeRequest) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_MARKETPLACES_LIST, Some(wire_params)) + .call(rpc_methods::HOST_DISPOSE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Registers a new marketplace from a source (owner/repo, URL, or local path). - /// - /// Wire method: `plugins.marketplaces.add`. - /// - /// # Parameters + /// Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant. /// - /// * `params` - Marketplace source and optional working directory for relative-path resolution. + /// Wire method: `host.getEnvironmentCredentials`. /// /// # Returns /// - /// Result of registering a new marketplace. + /// Private credentials delivered only to a runtime-owned Mission Control hosting participant. /// ///
    /// @@ -1578,29 +1687,27 @@ impl<'a> ClientRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn add( + pub(crate) async fn get_environment_credentials( &self, - params: PluginsMarketplacesAddRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + ) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::PLUGINS_MARKETPLACES_ADD, Some(wire_params)) + .call( + rpc_methods::HOST_GETENVIRONMENTCREDENTIALS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// 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`. - /// - /// Wire method: `plugins.marketplaces.remove`. - /// - /// # Parameters + /// Returns listener settings only to the supervised hosting participant over its SDK connection. /// - /// * `params` - Name of the marketplace to remove and an optional force flag. + /// Wire method: `host.getConfiguration`. /// /// # Returns /// - /// Outcome of the remove attempt, including dependent-plugin info when applicable. + /// Normalized listener settings delivered only to the supervised hosting participant. /// ///
    /// @@ -1609,29 +1716,26 @@ impl<'a> ClientRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn remove( - &self, - params: PluginsMarketplacesRemoveRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + pub(crate) async fn get_configuration(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::PLUGINS_MARKETPLACES_REMOVE, Some(wire_params)) + .call(rpc_methods::HOST_GETCONFIGURATION, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists plugins advertised by a registered marketplace. + /// Reports a supervised hosting participant's bound AHP endpoint after its SDK handshake. /// - /// Wire method: `plugins.marketplaces.browse`. + /// Wire method: `host.ready`. /// /// # Parameters /// - /// * `params` - Name of the marketplace whose plugin catalog to fetch. + /// * `params` - Readiness reported by the supervised hosting participant on its own SDK connection. /// /// # Returns /// - /// Plugins advertised by the marketplace. + /// Empty acknowledgement for a completed host lifecycle operation. /// ///
    /// @@ -1640,25 +1744,34 @@ impl<'a> ClientRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn browse( - &self, - params: PluginsMarketplacesBrowseRequest, - ) -> Result { + pub(crate) async fn ready(&self, params: HostReadyRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_MARKETPLACES_BROWSE, Some(wire_params)) + .call(rpc_methods::HOST_READY, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Re-fetches one or all registered marketplace catalogs. +/// `instructions.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcInstructions<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcInstructions<'a> { + /// Discovers instruction sources across user, repository, and plugin sources. /// - /// Wire method: `plugins.marketplaces.refresh`. + /// Wire method: `instructions.discover`. + /// + /// # Parameters + /// + /// * `params` - Optional project paths to include in instruction discovery. /// /// # Returns /// - /// Result of refreshing one or more marketplace catalogs. + /// Instruction sources discovered across user, repository, and plugin sources. /// ///
    /// @@ -1667,26 +1780,29 @@ impl<'a> ClientRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn refresh(&self) -> Result { - let wire_params = serde_json::json!({}); + pub async fn discover( + &self, + params: InstructionsDiscoverRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_MARKETPLACES_REFRESH, Some(wire_params)) + .call(rpc_methods::INSTRUCTIONS_DISCOVER, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Re-fetches one or all registered marketplace catalogs. + /// 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. /// - /// Wire method: `plugins.marketplaces.refresh`. + /// Wire method: `instructions.getDiscoveryPaths`. /// /// # Parameters /// - /// * `params` - Optional marketplace name; omit to refresh all. + /// * `params` - Optional project paths to include when enumerating instruction discovery targets. /// /// # Returns /// - /// Result of refreshing one or more marketplace catalogs. + /// Canonical files and directories where custom instructions can be created so the runtime will recognize them. /// ///
    /// @@ -1695,29 +1811,36 @@ impl<'a> ClientRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn refresh_with_params( + pub async fn get_discovery_paths( &self, - params: PluginsMarketplacesRefreshRequest, - ) -> Result { + params: InstructionsGetDiscoveryPathsRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::PLUGINS_MARKETPLACES_REFRESH, Some(wire_params)) + .call( + rpc_methods::INSTRUCTIONS_GETDISCOVERYPATHS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } } -/// `runtime.*` RPCs. +/// `llmInference.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcRuntime<'a> { +pub struct ClientRpcLlmInference<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcRuntime<'a> { - /// Gracefully shuts down an SDK-owned runtime. The response is sent only after cleanup completes; callers may then terminate the owned runtime process. +impl<'a> ClientRpcLlmInference<'a> { + /// Registers an SDK client as the LLM inference callback provider. /// - /// Wire method: `runtime.shutdown`. + /// Wire method: `llmInference.setProvider`. + /// + /// # Returns + /// + /// Indicates whether the calling client was registered as the LLM inference provider. /// ///
    /// @@ -1726,30 +1849,26 @@ impl<'a> ClientRpcRuntime<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn shutdown(&self) -> Result<(), Error> { + pub async fn set_provider(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::RUNTIME_SHUTDOWN, Some(wire_params)) + .call(rpc_methods::LLMINFERENCE_SETPROVIDER, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } -} - -/// `sandbox.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcSandbox<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcSandbox<'a> { - /// Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command. + /// 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. /// - /// Wire method: `sandbox.getHostSupport`. + /// Wire method: `llmInference.httpResponseStart`. + /// + /// # Parameters + /// + /// * `params` - Response head. /// /// # Returns /// - /// 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. + /// Whether the start frame was accepted. /// ///
    /// @@ -1758,34 +1877,32 @@ impl<'a> ClientRpcSandbox<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_host_support(&self) -> Result { - let wire_params = serde_json::json!({}); + pub async fn http_response_start( + &self, + params: LlmInferenceHttpResponseStartRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SANDBOX_GETHOSTSUPPORT, Some(wire_params)) + .call( + rpc_methods::LLMINFERENCE_HTTPRESPONSESTART, + 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). + /// 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. /// - /// Wire method: `secrets.addFilterValues`. + /// Wire method: `llmInference.httpResponseChunk`. /// /// # Parameters /// - /// * `params` - Secret values to add to the redaction filter. + /// * `params` - A response body chunk or terminal error. /// /// # Returns /// - /// Confirmation that the secret values were registered. + /// Whether the chunk was accepted. /// ///
    /// @@ -1794,37 +1911,36 @@ impl<'a> ClientRpcSecrets<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn add_filter_values( + pub async fn http_response_chunk( &self, - params: SecretsAddFilterValuesRequest, - ) -> Result { + params: LlmInferenceHttpResponseChunkRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SECRETS_ADDFILTERVALUES, Some(wire_params)) + .call( + rpc_methods::LLMINFERENCE_HTTPRESPONSECHUNK, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } } -/// `sessionFs.*` RPCs. +/// `managedSettings.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcSessionFs<'a> { +pub struct ClientRpcManagedSettings<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcSessionFs<'a> { - /// Registers an SDK client as the session filesystem provider. - /// - /// Wire method: `sessionFs.setProvider`. - /// - /// # Parameters +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. `managedSettings.resolve` returns the same device settings together with the account's server policy. /// - /// * `params` - 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. + /// Wire method: `managedSettings.read`. /// /// # Returns /// - /// Indicates whether the calling client was registered as the session filesystem provider. + /// Validated device-managed settings discovered before a session exists. /// ///
    /// @@ -1833,33 +1949,18 @@ impl<'a> ClientRpcSessionFs<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set_provider( - &self, - params: SessionFsSetProviderRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + pub async fn read(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONFS_SETPROVIDER, Some(wire_params)) + .call(rpc_methods::MANAGEDSETTINGS_READ, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `sessions.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcSessions<'a> { - pub(crate) client: &'a Client, -} - -impl<'a> ClientRpcSessions<'a> { - /// Creates or resumes a local session and returns the opened session ID. - /// - /// Wire method: `sessions.open`. + /// 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. /// - /// # Returns - /// - /// Result of opening a session. + /// Wire method: `managedSettings.clearCache`. /// ///
    /// @@ -1868,26 +1969,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn open(&self) -> Result { + pub async fn clear_cache(&self) -> Result<(), Error> { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONS_OPEN, Some(wire_params)) + .call(rpc_methods::MANAGEDSETTINGS_CLEARCACHE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Creates a new session by forking persisted history from an existing session. - /// - /// Wire method: `sessions.fork`. - /// - /// # Parameters + /// 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. /// - /// * `params` - Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. + /// Wire method: `managedSettings.resolve`. /// /// # Returns /// - /// Identifier and optional friendly name assigned to the newly forked session. + /// Effective enterprise managed settings for an account, resolved without a session. /// ///
    /// @@ -1896,26 +1993,26 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn fork(&self, params: SessionsForkRequest) -> Result { - let wire_params = serde_json::to_value(params)?; + pub async fn resolve(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONS_FORK, Some(wire_params)) + .call(rpc_methods::MANAGEDSETTINGS_RESOLVE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Connects to an existing remote session and exposes it as an SDK session. + /// 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: `sessions.connect`. + /// Wire method: `managedSettings.resolve`. /// /// # Parameters /// - /// * `params` - Remote session connection parameters. + /// * `params` - Optional opaque account selection or GitHub token whose managed settings are resolved. /// /// # Returns /// - /// Remote session connection result. + /// Effective enterprise managed settings for an account, resolved without a session. /// ///
    /// @@ -1924,25 +2021,25 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn connect( + pub async fn resolve_with_params( &self, - params: ConnectRemoteSessionParams, - ) -> Result { + params: ManagedSettingsResolveRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_CONNECT, Some(wire_params)) + .call(rpc_methods::MANAGEDSETTINGS_RESOLVE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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.). + /// 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: `sessions.list`. + /// Wire method: `managedSettings.schema`. /// /// # Returns /// - /// Sessions matching the filter, ordered most-recently-modified first. + /// The authoring JSON schema for managed settings recognized by this runtime. /// ///
    /// @@ -1951,26 +2048,26 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { + pub async fn schema(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONS_LIST, Some(wire_params)) + .call(rpc_methods::MANAGEDSETTINGS_SCHEMA, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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.). + /// 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: `sessions.list`. + /// Wire method: `managedSettings.validate`. /// /// # Parameters /// - /// * `params` - Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + /// * `params` - A candidate managed-settings document to validate without applying it. /// /// # Returns /// - /// Sessions matching the filter, ordered most-recently-modified first. + /// Result of validating a managed-settings document. /// ///
    /// @@ -1979,29 +2076,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list_with_params( + pub async fn validate( &self, - params: SessionsListRequest, - ) -> Result { + params: ManagedSettingsValidateRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_LIST, Some(wire_params)) + .call(rpc_methods::MANAGEDSETTINGS_VALIDATE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Reads lightweight persisted metadata for one local session without opening it. + /// 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: `sessions.getMetadata`. + /// Wire method: `managedSettings.compose`. /// /// # Parameters /// - /// * `params` - Session ID whose persisted metadata should be read. + /// * `params` - Candidate managed-settings documents to merge without applying them. /// /// # Returns /// - /// Persisted local session metadata when the session exists. + /// The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. /// ///
    /// @@ -2010,29 +2107,51 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn get_metadata( + pub async fn compose( &self, - params: SessionsGetMetadataRequest, - ) -> Result { + params: ManagedSettingsComposeRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_GETMETADATA, Some(wire_params)) + .call(rpc_methods::MANAGEDSETTINGS_COMPOSE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// 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. +/// `mcp.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcMcp<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcMcp<'a> { + /// `mcp.config.*` sub-namespace. + pub fn config(&self) -> ClientRpcMcpConfig<'a> { + ClientRpcMcpConfig { + client: self.client, + } + } + + /// `mcp.installations.*` sub-namespace. + pub fn installations(&self) -> ClientRpcMcpInstallations<'a> { + ClientRpcMcpInstallations { + client: self.client, + } + } + + /// Discovers MCP servers from user, workspace, plugin, and builtin sources. /// - /// Wire method: `sessions.getClientMetadata`. + /// Wire method: `mcp.discover`. /// /// # Parameters /// - /// * `params` - Bounded batch request for client-owned metadata from persisted local sessions. + /// * `params` - Optional working directory used as context for MCP server discovery. /// /// # Returns /// - /// Ordered client metadata outcomes for the requested local sessions. + /// MCP servers discovered from user, workspace, plugin, and built-in sources. /// ///
    /// @@ -2041,29 +2160,26 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_client_metadata( - &self, - params: SessionsGetClientMetadataRequest, - ) -> Result { + pub async fn discover(&self, params: McpDiscoverRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_GETCLIENTMETADATA, Some(wire_params)) + .call(rpc_methods::MCP_DISCOVER, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// 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. /// - /// Wire method: `sessions.readPersistedEvents`. + /// Wire method: `mcp.planInstall`. /// /// # Parameters /// - /// * `params` - Pagination options for reading an inactive or active local session's persisted event journal. + /// * `params` - A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. /// /// # Returns /// - /// Batch of session events returned by a read, with cursor and continuation metadata. + /// Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. /// ///
    /// @@ -2072,29 +2188,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn read_persisted_events( + pub async fn plan_install( &self, - params: SessionsReadPersistedEventsRequest, - ) -> Result { + params: McpPlanInstallRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_READPERSISTEDEVENTS, Some(wire_params)) + .call(rpc_methods::MCP_PLANINSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. + /// 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. /// - /// Wire method: `sessions.listNonEmptySessionIds`. + /// Wire method: `mcp.planInstall`. /// /// # Parameters /// - /// * `params` - Limit for non-empty local session IDs. + /// * `params` - A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. /// /// # Returns /// - /// Recent local session IDs that contain user-visible history. + /// Outcome of an mcp.planInstall call: either a normalised plan, or one typed refusal. Nothing is written in either case. /// ///
    /// @@ -2103,32 +2219,31 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn list_non_empty_session_ids( + /// + /// Accepts [`McpPlanInstallOptions`], including inputs added after [`McpPlanInstallRequest`]. + pub async fn plan_install_with_options( &self, - params: SessionsListNonEmptySessionIdsRequest, - ) -> Result { + params: McpPlanInstallOptions, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call( - rpc_methods::SESSIONS_LISTNONEMPTYSESSIONIDS, - Some(wire_params), - ) + .call(rpc_methods::MCP_PLANINSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Finds the local session bound to a GitHub task ID, if any. + /// 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. /// - /// Wire method: `sessions.findByTaskId`. + /// Wire method: `mcp.prepareInstall`. /// /// # Parameters /// - /// * `params` - GitHub task ID to look up. + /// * `params` - Side-effect-free preparation of one original bound remote MCP choice. /// /// # Returns /// - /// ID of the local session bound to the given GitHub task, or omitted when none. + /// Management result with contract receipt, or a typed request/negotiation refusal. /// ///
    /// @@ -2137,29 +2252,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn find_by_task_id( + pub async fn prepare_install( &self, - params: SessionsFindByTaskIDRequest, - ) -> Result { + params: McpPrepareInstallRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_FINDBYTASKID, Some(wire_params)) + .call(rpc_methods::MCP_PREPAREINSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Resolves a UUID prefix to a unique session ID, if exactly one session matches. + /// 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. /// - /// Wire method: `sessions.findByPrefix`. + /// Wire method: `mcp.applyInstall`. /// /// # Parameters /// - /// * `params` - UUID prefix to resolve to a unique session ID. + /// * `params` - Applies exactly one previously prepared operation on its original connection. /// /// # Returns /// - /// Session ID matching the prefix, omitted when no unique match exists. + /// An installation result together with the exact honoured contract, or a negotiation refusal. /// ///
    /// @@ -2168,29 +2283,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn find_by_prefix( + pub async fn apply_install( &self, - params: SessionsFindByPrefixRequest, - ) -> Result { + params: McpApplyInstallRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_FINDBYPREFIX, Some(wire_params)) + .call(rpc_methods::MCP_APPLYINSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the most-relevant prior session for a given working-directory context. + /// 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. /// - /// Wire method: `sessions.getLastForContext`. + /// Wire method: `mcp.planUninstall`. /// /// # Parameters /// - /// * `params` - Optional working-directory context used to score session relevance. + /// * `params` - Read-only preparation of one owned removal under fresh selected-session authority. /// /// # Returns /// - /// Most-relevant session ID for the supplied context, or omitted when no sessions exist. + /// Management result with contract receipt, or a typed request/negotiation refusal. /// ///
    /// @@ -2199,29 +2314,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_last_for_context( + pub async fn plan_uninstall( &self, - params: SessionsGetLastForContextRequest, - ) -> Result { + params: McpPlanUninstallRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_GETLASTFORCONTEXT, Some(wire_params)) + .call(rpc_methods::MCP_PLANUNINSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// 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. /// - /// Wire method: `sessions.getEventFilePath`. + /// Wire method: `mcp.applyUninstall`. /// /// # Parameters /// - /// * `params` - Session ID whose event-log file path to compute. + /// * `params` - One-use application of the exact retained removal plan. /// /// # Returns /// - /// Absolute path to the session's events.jsonl file on disk. + /// An installation result together with the exact honoured contract, or a negotiation refusal. /// ///
    /// @@ -2230,25 +2345,33 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn get_event_file_path( + pub async fn apply_uninstall( &self, - params: SessionsGetEventFilePathRequest, - ) -> Result { + params: McpApplyUninstallRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_GETEVENTFILEPATH, Some(wire_params)) + .call(rpc_methods::MCP_APPLYUNINSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Returns the on-disk byte size of each session's workspace directory. +/// `mcp.config.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcMcpConfig<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcMcpConfig<'a> { + /// Lists MCP servers from user configuration. /// - /// Wire method: `sessions.getSizes`. + /// Wire method: `mcp.config.list`. /// /// # Returns /// - /// Map of sessionId -> on-disk size in bytes for each session's workspace directory. + /// User-configured MCP servers, keyed by server name. /// ///
    /// @@ -2257,26 +2380,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_sizes(&self) -> Result { + pub async fn list(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONS_GETSIZES, Some(wire_params)) + .call(rpc_methods::MCP_CONFIG_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the subset of the supplied session IDs that are currently held by another running process. + /// Adds an MCP server to user configuration. /// - /// Wire method: `sessions.checkInUse`. + /// Wire method: `mcp.config.add`. /// /// # Parameters /// - /// * `params` - Session IDs to test for live in-use locks. - /// - /// # Returns - /// - /// Session IDs from the input set that are currently in use by another process. + /// * `params` - MCP server name and configuration to add to user configuration. /// ///
    /// @@ -2285,29 +2404,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn check_in_use( - &self, - params: SessionsCheckInUseRequest, - ) -> Result { + pub async fn add(&self, params: McpConfigAddRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_CHECKINUSE, Some(wire_params)) + .call(rpc_methods::MCP_CONFIG_ADD, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// 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. + /// Updates an MCP server in user configuration. /// - /// Wire method: `sessions.getPersistedRemoteSteerable`. + /// Wire method: `mcp.config.update`. /// /// # Parameters /// - /// * `params` - Session ID to look up the persisted remote-steerable flag for. - /// - /// # Returns - /// - /// The session's persisted remote-steerable flag, or omitted when no value has been persisted. + /// * `params` - MCP server name and replacement configuration to write to user configuration. /// ///
    /// @@ -2316,32 +2428,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn get_persisted_remote_steerable( - &self, - params: SessionsGetPersistedRemoteSteerableRequest, - ) -> Result { + pub async fn update(&self, params: McpConfigUpdateRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call( - rpc_methods::SESSIONS_GETPERSISTEDREMOTESTEERABLE, - Some(wire_params), - ) + .call(rpc_methods::MCP_CONFIG_UPDATE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Closes a session: emits shutdown, flushes pending events, releases the in-use lock, and disposes the active session. + /// Removes an MCP server from user configuration. /// - /// Wire method: `sessions.close`. + /// Wire method: `mcp.config.remove`. /// /// # Parameters /// - /// * `params` - Session ID to close. - /// - /// # Returns - /// - /// Closes a session: emits shutdown, flushes pending events to disk, releases the in-use lock, disposes the active session. Idempotent: succeeds even if the session is not currently active. + /// * `params` - MCP server name to remove from user configuration. /// ///
    /// @@ -2350,26 +2452,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn close(&self, params: SessionsCloseRequest) -> Result { + pub async fn remove(&self, params: McpConfigRemoveRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_CLOSE, Some(wire_params)) + .call(rpc_methods::MCP_CONFIG_REMOVE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Closes, deactivates, and deletes a set of sessions, returning the bytes freed per session. + /// Enables MCP servers in user configuration for new sessions. /// - /// Wire method: `sessions.bulkDelete`. + /// Wire method: `mcp.config.enable`. /// /// # Parameters /// - /// * `params` - Session IDs to close, deactivate, and delete from disk. - /// - /// # Returns - /// - /// Map of sessionId -> bytes freed by removing the session's workspace directory. + /// * `params` - MCP server names to enable for new sessions. /// ///
    /// @@ -2378,25 +2476,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn bulk_delete( - &self, - params: SessionsBulkDeleteRequest, - ) -> Result { + pub async fn enable(&self, params: McpConfigEnableRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_BULKDELETE, Some(wire_params)) + .call(rpc_methods::MCP_CONFIG_ENABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Deletes one local session from disk after running the same lifecycle hooks as the session manager. + /// Disables MCP servers in user configuration for new sessions. /// - /// Wire method: `sessions.delete`. + /// Wire method: `mcp.config.disable`. /// /// # Parameters /// - /// * `params` - Session ID to delete from disk. + /// * `params` - MCP server names to disable for new sessions. /// ///
    /// @@ -2405,26 +2500,54 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn delete(&self, params: SessionsDeleteRequest) -> Result<(), Error> { + pub async fn disable(&self, params: McpConfigDisableRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_DELETE, Some(wire_params)) + .call(rpc_methods::MCP_CONFIG_DISABLE, Some(wire_params)) .await?; Ok(()) } - /// Deletes sessions older than the given threshold, with optional dry-run and exclusion list. + /// Drops this runtime process's in-memory MCP server-definition cache so the next MCP config read observes disk. /// - /// Wire method: `sessions.pruneOld`. + /// Wire method: `mcp.config.reload`. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn reload(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::MCP_CONFIG_RELOAD, Some(wire_params)) + .await?; + Ok(()) + } +} + +/// `mcp.installations.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcMcpInstallations<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcMcpInstallations<'a> { + /// 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. + /// + /// Wire method: `mcp.installations.list`. /// /// # Parameters /// - /// * `params` - Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). + /// * `params` - New-work inventory or recovery request under an explicitly selected existing session. /// /// # Returns /// - /// Outcome of the prune operation: deleted IDs, dry-run candidates, skipped IDs, total bytes freed, and the dry-run flag. + /// Management result with contract receipt, or a typed request/negotiation refusal. /// ///
    /// @@ -2433,29 +2556,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn prune_old( + pub async fn list( &self, - params: SessionsPruneOldRequest, - ) -> Result { + params: McpInstallationsRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_PRUNEOLD, Some(wire_params)) + .call(rpc_methods::MCP_INSTALLATIONS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Flushes a session's pending events to disk. + /// 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. /// - /// Wire method: `sessions.save`. + /// Wire method: `mcp.installations.recover`. /// /// # Parameters /// - /// * `params` - Session ID whose pending events should be flushed to disk. + /// * `params` - New-work inventory or recovery request under an explicitly selected existing session. /// /// # Returns /// - /// Flush a session's pending events to disk. No-op when no writer exists for the session (e.g., already closed). + /// Management result with contract receipt, or a typed request/negotiation refusal. /// ///
    /// @@ -2464,26 +2587,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn save(&self, params: SessionsSaveRequest) -> Result { + pub async fn recover( + &self, + params: McpInstallationsRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_SAVE, Some(wire_params)) + .call(rpc_methods::MCP_INSTALLATIONS_RECOVER, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Releases the in-use lock held by this process for a session. + /// 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. /// - /// Wire method: `sessions.releaseLock`. + /// Wire method: `mcp.installations.status`. /// /// # Parameters /// - /// * `params` - Session ID whose in-use lock should be released. + /// * `params` - Existing-operation control. A new session selector is deliberately not accepted. /// /// # Returns /// - /// Release the in-use lock held by this process for the given session. No-op when this process does not currently hold a lock for the session. + /// Management result with contract receipt, or a typed request/negotiation refusal. /// ///
    /// @@ -2492,29 +2618,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn release_lock( + pub async fn status( &self, - params: SessionsReleaseLockRequest, - ) -> Result { + params: McpInstallationOperationRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_RELEASELOCK, Some(wire_params)) + .call(rpc_methods::MCP_INSTALLATIONS_STATUS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Backfills missing summary and context fields on the supplied session metadata records. + /// 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. /// - /// Wire method: `sessions.enrichMetadata`. + /// Wire method: `mcp.installations.cancel`. /// /// # Parameters /// - /// * `params` - Session metadata records to enrich with summary and context information. + /// * `params` - Existing-operation control. A new session selector is deliberately not accepted. /// /// # Returns /// - /// The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted. + /// Management result with contract receipt, or a typed request/negotiation refusal. /// ///
    /// @@ -2523,29 +2649,33 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn enrich_metadata( + pub async fn cancel( &self, - params: SessionsEnrichMetadataRequest, - ) -> Result { + params: McpInstallationOperationRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_ENRICHMETADATA, Some(wire_params)) + .call(rpc_methods::MCP_INSTALLATIONS_CANCEL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Reloads user, plugin, and (optionally) repo hooks on the active session. - /// - /// Wire method: `sessions.reloadPluginHooks`. - /// - /// # Parameters +/// `models.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcModels<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcModels<'a> { + /// Lists Copilot models available to the authenticated user. /// - /// * `params` - Active session ID and an optional flag for deferring repo-level hooks until folder trust. + /// Wire method: `models.list`. /// /// # Returns /// - /// Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId. + /// List of Copilot models available to the resolved user, including capabilities and billing metadata. /// ///
    /// @@ -2554,29 +2684,26 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reload_plugin_hooks( - &self, - params: SessionsReloadPluginHooksRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONS_RELOADPLUGINHOOKS, Some(wire_params)) + .call(rpc_methods::MODELS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Loads previously-deferred repo-level hooks on the active session, returning queued startup prompts. + /// Lists Copilot models available to the authenticated user. /// - /// Wire method: `sessions.loadDeferredRepoHooks`. + /// Wire method: `models.list`. /// /// # Parameters /// - /// * `params` - Active session ID whose deferred repo-level hooks should be loaded. + /// * `params` - Optional opaque account selection or compatibility GitHub token used to list models. /// /// # Returns /// - /// Queued repo-level startup prompts and the total hook command count after loading. + /// List of Copilot models available to the resolved user, including capabilities and billing metadata. /// ///
    /// @@ -2585,32 +2712,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn load_deferred_repo_hooks( - &self, - params: SessionsLoadDeferredRepoHooksRequest, - ) -> Result { + pub async fn list_with_params(&self, params: ModelsListRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call( - rpc_methods::SESSIONS_LOADDEFERREDREPOHOOKS, - Some(wire_params), - ) + .call(rpc_methods::MODELS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Replaces the manager-wide additional plugins registered with the session manager. - /// - /// Wire method: `sessions.setAdditionalPlugins`. - /// - /// # Parameters + /// Returns the running runtime's complete catalog of well-known built-in model IDs without authentication or network access. /// - /// * `params` - Manager-wide additional plugins to register; replaces any previously-configured set. + /// Wire method: `models.getBuiltInCatalog`. /// /// # Returns /// - /// Replace the manager-wide additional plugins. New session creations and subsequent hook reloads see the new set; already-running sessions keep their existing hook installation until the next reload. + /// The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. /// ///
    /// @@ -2619,32 +2736,44 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set_additional_plugins( - &self, - params: SessionsSetAdditionalPluginsRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + pub async fn get_built_in_catalog(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call( - rpc_methods::SESSIONS_SETADDITIONALPLUGINS, - Some(wire_params), - ) + .call(rpc_methods::MODELS_GETBUILTINCATALOG, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// 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. - /// - /// Wire method: `sessions.getBoardEntryCount`. - /// - /// # Parameters +/// `plugins.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcPlugins<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcPlugins<'a> { + /// `plugins.builtin.*` sub-namespace. + pub fn builtin(&self) -> ClientRpcPluginsBuiltin<'a> { + ClientRpcPluginsBuiltin { + client: self.client, + } + } + + /// `plugins.marketplaces.*` sub-namespace. + pub fn marketplaces(&self) -> ClientRpcPluginsMarketplaces<'a> { + ClientRpcPluginsMarketplaces { + client: self.client, + } + } + + /// Lists plugins installed in user/global state. /// - /// * `params` - Session ID whose board entry count should be returned. + /// Wire method: `plugins.list`. /// /// # Returns /// - /// Dynamic-context board entry count, when available. + /// Plugins installed in user/global state. /// ///
    /// @@ -2653,29 +2782,26 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn get_board_entry_count( - &self, - params: SessionsGetBoardEntryCountRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONS_GETBOARDENTRYCOUNT, Some(wire_params)) + .call(rpc_methods::PLUGINS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// Installs a plugin from a marketplace, GitHub repo, URL, or local path. /// - /// Wire method: `sessions.startRemoteControl`. + /// Wire method: `plugins.install`. /// /// # Parameters /// - /// * `params` - Parameters for attaching the remote-control singleton to a session. + /// * `params` - Plugin source and optional working directory for relative-path resolution. /// /// # Returns /// - /// Wrapper for the singleton's current status. + /// Result of installing a plugin. /// ///
    /// @@ -2684,29 +2810,25 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn start_remote_control( + pub async fn install( &self, - params: SessionsStartRemoteControlRequest, - ) -> Result { + params: PluginsInstallRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_STARTREMOTECONTROL, Some(wire_params)) + .call(rpc_methods::PLUGINS_INSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// Uninstalls an installed plugin. /// - /// Wire method: `sessions.transferRemoteControl`. + /// Wire method: `plugins.uninstall`. /// /// # Parameters /// - /// * `params` - Parameters for atomically rebinding the remote-control singleton. - /// - /// # Returns - /// - /// Outcome of a transferRemoteControl call. + /// * `params` - Name (or spec) of the plugin to uninstall. /// ///
    /// @@ -2715,32 +2837,26 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn transfer_remote_control( - &self, - params: SessionsTransferRemoteControlRequest, - ) -> Result { + pub async fn uninstall(&self, params: PluginsUninstallRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call( - rpc_methods::SESSIONS_TRANSFERREMOTECONTROL, - Some(wire_params), - ) + .call(rpc_methods::PLUGINS_UNINSTALL, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// 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. + /// Updates an installed plugin to its latest published version. /// - /// Wire method: `sessions.setRemoteControlSteering`. + /// Wire method: `plugins.update`. /// /// # Parameters /// - /// * `params` - Patch for the singleton's steering state. + /// * `params` - Name (or spec) of the plugin to update. /// /// # Returns /// - /// Wrapper for the singleton's current status. + /// Result of updating a single plugin. /// ///
    /// @@ -2749,28 +2865,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set_remote_control_steering( - &self, - params: SessionsSetRemoteControlSteeringRequest, - ) -> Result { + pub async fn update(&self, params: PluginsUpdateRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call( - rpc_methods::SESSIONS_SETREMOTECONTROLSTEERING, - Some(wire_params), - ) + .call(rpc_methods::PLUGINS_UPDATE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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). + /// Updates every installed plugin to its latest published version. /// - /// Wire method: `sessions.stopRemoteControl`. + /// Wire method: `plugins.updateAll`. /// /// # Returns /// - /// Outcome of a stopRemoteControl call. + /// Result of updating all installed plugins. /// ///
    /// @@ -2779,26 +2889,22 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn stop_remote_control(&self) -> Result { + pub async fn update_all(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::SESSIONS_STOPREMOTECONTROL, Some(wire_params)) + .call(rpc_methods::PLUGINS_UPDATEALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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). + /// Enables installed plugins for new sessions. /// - /// Wire method: `sessions.stopRemoteControl`. + /// Wire method: `plugins.enable`. /// /// # Parameters /// - /// * `params` - Parameters for stopping the remote-control singleton. - /// - /// # Returns - /// - /// Outcome of a stopRemoteControl call. + /// * `params` - Plugin names (or specs) to enable, plus the optional working directory the repository-controlled guard is evaluated against. /// ///
    /// @@ -2807,16 +2913,1620 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn stop_remote_control_with_params( - &self, - params: SessionsStopRemoteControlRequest, - ) -> Result { + pub async fn enable(&self, params: PluginsEnableRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_STOPREMOTECONTROL, Some(wire_params)) + .call(rpc_methods::PLUGINS_ENABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) + } + + /// Disables installed plugins for new sessions. + /// + /// Wire method: `plugins.disable`. + /// + /// # Parameters + /// + /// * `params` - Plugin names (or specs) to disable, plus the optional working directory the repository-controlled guard is evaluated against. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn disable(&self, params: PluginsDisableRequest) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::PLUGINS_DISABLE, Some(wire_params)) + .await?; + Ok(()) + } +} + +/// `plugins.builtin.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcPluginsBuiltin<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcPluginsBuiltin<'a> { + /// Replaces this server's trusted built-in plugin directories while no sessions are active. + /// + /// Wire method: `plugins.builtin.set`. + /// + /// # Parameters + /// + /// * `params` - Trusted built-in plugin directories to use for this runtime process. + /// + ///
    + /// + /// **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 set(&self, params: PluginsBuiltinSetRequest) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::PLUGINS_BUILTIN_SET, Some(wire_params)) + .await?; + Ok(()) + } +} + +/// `plugins.marketplaces.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcPluginsMarketplaces<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcPluginsMarketplaces<'a> { + /// Lists all registered marketplaces (defaults + user-added). + /// + /// Wire method: `plugins.marketplaces.list`. + /// + /// # Returns + /// + /// All registered marketplaces, including built-in defaults. + /// + ///
    + /// + /// **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) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::PLUGINS_MARKETPLACES_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Registers a new marketplace from a source (owner/repo, URL, or local path). + /// + /// Wire method: `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 wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::PLUGINS_MARKETPLACES_ADD, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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`. + /// + /// Wire method: `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 wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::PLUGINS_MARKETPLACES_REMOVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists plugins advertised by a registered marketplace. + /// + /// Wire method: `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 wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::PLUGINS_MARKETPLACES_BROWSE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Re-fetches one or all registered marketplace catalogs. + /// + /// Wire method: `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!({}); + let _value = self + .client + .call(rpc_methods::PLUGINS_MARKETPLACES_REFRESH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Re-fetches one or all registered marketplace catalogs. + /// + /// Wire method: `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 wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::PLUGINS_MARKETPLACES_REFRESH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `runtime.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcRuntime<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcRuntime<'a> { + /// Gracefully shuts down an SDK-owned runtime. The response is sent only after cleanup completes; callers may then terminate the owned runtime process. + /// + /// Wire method: `runtime.shutdown`. + /// + ///
    + /// + /// **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 shutdown(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::RUNTIME_SHUTDOWN, Some(wire_params)) + .await?; + Ok(()) + } +} + +/// `sandbox.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSandbox<'a> { + pub(crate) client: &'a Client, +} + +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`. + /// + /// # Returns + /// + /// 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. + /// + ///
    + /// + /// **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_host_support(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SANDBOX_GETHOSTSUPPORT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `sandbox.proxyCa.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSandboxProxyCa<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcSandboxProxyCa<'a> { + /// Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. + /// + /// Wire method: `sandbox.proxyCa.getStatus`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn get_status( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_GETSTATUS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. + /// + /// Wire method: `sandbox.proxyCa.create`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Result of creating the persistent certificate authority of the sandbox credential proxy. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn create( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_CREATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. + /// + /// Wire method: `sandbox.proxyCa.rotate`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn rotate( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_ROTATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. + /// + /// Wire method: `sandbox.proxyCa.trust`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn trust( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_TRUST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. + /// + /// Wire method: `sandbox.proxyCa.remove`. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn remove(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_REMOVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `secrets.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSecrets<'a> { + 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 + .call(rpc_methods::SECRETS_ADDFILTERVALUES, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `sessionFs.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSessionFs<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcSessionFs<'a> { + /// Registers an SDK client as the session filesystem provider. + /// + /// Wire method: `sessionFs.setProvider`. + /// + /// # Parameters + /// + /// * `params` - 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. + /// + /// # Returns + /// + /// Indicates whether the calling client was registered as the session filesystem provider. + /// + ///
    + /// + /// **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 set_provider( + &self, + params: SessionFsSetProviderRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONFS_SETPROVIDER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `sessions.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSessions<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcSessions<'a> { + /// Creates or resumes a local session and returns the opened session ID. + /// + /// Wire method: `sessions.open`. + /// + /// # Returns + /// + /// Result of opening 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 open(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SESSIONS_OPEN, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Creates a new session by forking persisted history from an existing session. + /// + /// Wire method: `sessions.fork`. + /// + /// # Parameters + /// + /// * `params` - Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. + /// + /// # Returns + /// + /// Identifier and optional friendly name assigned to the newly forked 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 fork(&self, params: SessionsForkRequest) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_FORK, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Connects to an existing remote session and exposes it as an SDK session. + /// + /// Wire method: `sessions.connect`. + /// + /// # Parameters + /// + /// * `params` - Remote session connection parameters. + /// + /// # Returns + /// + /// Remote session connection result. + /// + ///
    + /// + /// **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 connect( + &self, + params: ConnectRemoteSessionParams, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_CONNECT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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.). + /// + /// Wire method: `sessions.list`. + /// + /// # Returns + /// + /// Sessions matching the filter, ordered most-recently-modified first. + /// + ///
    + /// + /// **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) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SESSIONS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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.). + /// + /// Wire method: `sessions.list`. + /// + /// # Parameters + /// + /// * `params` - Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + /// + /// # Returns + /// + /// Sessions matching the filter, ordered most-recently-modified first. + /// + ///
    + /// + /// **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_with_params( + &self, + params: SessionsListRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Reads lightweight persisted metadata for one local session without opening it. + /// + /// Wire method: `sessions.getMetadata`. + /// + /// # Parameters + /// + /// * `params` - Session ID whose persisted metadata should be read. + /// + /// # Returns + /// + /// Persisted local session metadata when the session exists. + /// + ///
    + /// + /// **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_metadata( + &self, + params: SessionsGetMetadataRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_GETMETADATA, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.getClientMetadata`. + /// + /// # Parameters + /// + /// * `params` - Bounded batch request for client-owned metadata from persisted local sessions. + /// + /// # Returns + /// + /// Ordered client metadata outcomes for the requested local sessions. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn get_client_metadata( + &self, + params: SessionsGetClientMetadataRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_GETCLIENTMETADATA, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.readPersistedEvents`. + /// + /// # Parameters + /// + /// * `params` - Pagination options for reading an inactive or active local session's persisted event journal. + /// + /// # Returns + /// + /// Batch of session events returned by a read, with cursor and continuation metadata. + /// + ///
    + /// + /// **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 read_persisted_events( + &self, + params: SessionsReadPersistedEventsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_READPERSISTEDEVENTS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. + /// + /// Wire method: `sessions.listNonEmptySessionIds`. + /// + /// # Parameters + /// + /// * `params` - Limit for non-empty local session IDs. + /// + /// # Returns + /// + /// Recent local session IDs that contain user-visible history. + /// + ///
    + /// + /// **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 list_non_empty_session_ids( + &self, + params: SessionsListNonEmptySessionIdsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_LISTNONEMPTYSESSIONIDS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Finds the local session bound to a GitHub task ID, if any. + /// + /// Wire method: `sessions.findByTaskId`. + /// + /// # Parameters + /// + /// * `params` - GitHub task ID to look up. + /// + /// # Returns + /// + /// ID of the local session bound to the given GitHub task, or omitted when none. + /// + ///
    + /// + /// **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 find_by_task_id( + &self, + params: SessionsFindByTaskIDRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_FINDBYTASKID, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Resolves a UUID prefix to a unique session ID, if exactly one session matches. + /// + /// Wire method: `sessions.findByPrefix`. + /// + /// # Parameters + /// + /// * `params` - UUID prefix to resolve to a unique session ID. + /// + /// # Returns + /// + /// Session ID matching the prefix, omitted when no unique match exists. + /// + ///
    + /// + /// **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 find_by_prefix( + &self, + params: SessionsFindByPrefixRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_FINDBYPREFIX, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the most-relevant prior session for a given working-directory context. + /// + /// Wire method: `sessions.getLastForContext`. + /// + /// # Parameters + /// + /// * `params` - Optional working-directory context used to score session relevance. + /// + /// # Returns + /// + /// Most-relevant session ID for the supplied context, or omitted when no sessions exist. + /// + ///
    + /// + /// **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_last_for_context( + &self, + params: SessionsGetLastForContextRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_GETLASTFORCONTEXT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.getEventFilePath`. + /// + /// # Parameters + /// + /// * `params` - Session ID whose event-log file path to compute. + /// + /// # Returns + /// + /// Absolute path to the session's events.jsonl file on disk. + /// + ///
    + /// + /// **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_event_file_path( + &self, + params: SessionsGetEventFilePathRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_GETEVENTFILEPATH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the on-disk byte size of each session's workspace directory. + /// + /// Wire method: `sessions.getSizes`. + /// + /// # Returns + /// + /// Map of sessionId -> on-disk size in bytes for each session's workspace directory. + /// + ///
    + /// + /// **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_sizes(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SESSIONS_GETSIZES, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the subset of the supplied session IDs that are currently held by another running process. + /// + /// Wire method: `sessions.checkInUse`. + /// + /// # Parameters + /// + /// * `params` - Session IDs to test for live in-use locks. + /// + /// # Returns + /// + /// Session IDs from the input set that are currently in use by another process. + /// + ///
    + /// + /// **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 check_in_use( + &self, + params: SessionsCheckInUseRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_CHECKINUSE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.getPersistedRemoteSteerable`. + /// + /// # Parameters + /// + /// * `params` - Session ID to look up the persisted remote-steerable flag for. + /// + /// # Returns + /// + /// The session's persisted remote-steerable flag, or omitted when no value has been persisted. + /// + ///
    + /// + /// **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_persisted_remote_steerable( + &self, + params: SessionsGetPersistedRemoteSteerableRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_GETPERSISTEDREMOTESTEERABLE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Closes a session: emits shutdown, flushes pending events, releases the in-use lock, and disposes the active session. + /// + /// Wire method: `sessions.close`. + /// + /// # Parameters + /// + /// * `params` - Session ID to close. + /// + /// # Returns + /// + /// Closes a session: emits shutdown, flushes pending events to disk, releases the in-use lock, disposes the active session. Idempotent: succeeds even if the session is not currently active. + /// + ///
    + /// + /// **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 close(&self, params: SessionsCloseRequest) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_CLOSE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Closes, deactivates, and deletes a set of sessions, returning the bytes freed per session. + /// + /// Wire method: `sessions.bulkDelete`. + /// + /// # Parameters + /// + /// * `params` - Session IDs to close, deactivate, and delete from disk. + /// + /// # Returns + /// + /// Map of sessionId -> bytes freed by removing the session's workspace directory. + /// + ///
    + /// + /// **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 bulk_delete( + &self, + params: SessionsBulkDeleteRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_BULKDELETE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Deletes one local session from disk after running the same lifecycle hooks as the session manager. + /// + /// Wire method: `sessions.delete`. + /// + /// # Parameters + /// + /// * `params` - Session ID to delete from disk. + /// + ///
    + /// + /// **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 delete(&self, params: SessionsDeleteRequest) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_DELETE, Some(wire_params)) + .await?; + Ok(()) + } + + /// Deletes sessions older than the given threshold, with optional dry-run and exclusion list. + /// + /// Wire method: `sessions.pruneOld`. + /// + /// # Parameters + /// + /// * `params` - Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). + /// + /// # Returns + /// + /// Outcome of the prune operation: deleted IDs, dry-run candidates, skipped IDs, total bytes freed, and the dry-run flag. + /// + ///
    + /// + /// **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 prune_old( + &self, + params: SessionsPruneOldRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_PRUNEOLD, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Flushes a session's pending events to disk. + /// + /// Wire method: `sessions.save`. + /// + /// # Parameters + /// + /// * `params` - Session ID whose pending events should be flushed to disk. + /// + /// # Returns + /// + /// Flush a session's pending events to disk. No-op when no writer exists for the session (e.g., already closed). + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn save(&self, params: SessionsSaveRequest) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_SAVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Releases the in-use lock held by this process for a session. + /// + /// Wire method: `sessions.releaseLock`. + /// + /// # Parameters + /// + /// * `params` - Session ID whose in-use lock should be released. + /// + /// # Returns + /// + /// Release the in-use lock held by this process for the given session. No-op when this process does not currently hold a lock for the session. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn release_lock( + &self, + params: SessionsReleaseLockRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_RELEASELOCK, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Backfills missing summary and context fields on the supplied session metadata records. + /// + /// Wire method: `sessions.enrichMetadata`. + /// + /// # Parameters + /// + /// * `params` - Session metadata records to enrich with summary and context information. + /// + /// # Returns + /// + /// The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn enrich_metadata( + &self, + params: SessionsEnrichMetadataRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_ENRICHMETADATA, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + /// + /// Wire method: `sessions.createWorkspace`. + /// + /// # Parameters + /// + /// * `params` - Identity, state location and starting context for a workspace record. + /// + /// # Returns + /// + /// The workspace record that was written. + /// + ///
    + /// + /// **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 create_workspace( + &self, + params: SessionsCreateWorkspaceRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_CREATEWORKSPACE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + /// + /// Wire method: `sessions.loadWorkspace`. + /// + /// # Parameters + /// + /// * `params` - Where the session's state lives, as a root directory and the session ID under it. + /// + /// # Returns + /// + /// The workspace record on disk, omitted when the session has none. + /// + ///
    + /// + /// **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 load_workspace( + &self, + params: SessionsLoadWorkspaceRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_LOADWORKSPACE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + /// + /// Wire method: `sessions.updateWorkspaceFields`. + /// + /// # Parameters + /// + /// * `params` - Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. + /// + /// # Returns + /// + /// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. + /// + ///
    + /// + /// **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 update_workspace_fields( + &self, + params: SessionsUpdateWorkspaceFieldsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_UPDATEWORKSPACEFIELDS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Reloads user, plugin, and (optionally) repo hooks on the active session. + /// + /// Wire method: `sessions.reloadPluginHooks`. + /// + /// # Parameters + /// + /// * `params` - Active session ID and an optional flag for deferring repo-level hooks until folder trust. + /// + /// # Returns + /// + /// Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn reload_plugin_hooks( + &self, + params: SessionsReloadPluginHooksRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_RELOADPLUGINHOOKS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Loads previously-deferred repo-level hooks on the active session, returning queued startup prompts. + /// + /// Wire method: `sessions.loadDeferredRepoHooks`. + /// + /// # Parameters + /// + /// * `params` - Active session ID whose deferred repo-level hooks should be loaded. + /// + /// # Returns + /// + /// Queued repo-level startup prompts and the total hook command count after loading. + /// + ///
    + /// + /// **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 load_deferred_repo_hooks( + &self, + params: SessionsLoadDeferredRepoHooksRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_LOADDEFERREDREPOHOOKS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Replaces the manager-wide additional plugins registered with the session manager. + /// + /// Wire method: `sessions.setAdditionalPlugins`. + /// + /// # Parameters + /// + /// * `params` - Manager-wide additional plugins to register; replaces any previously-configured set. + /// + /// # Returns + /// + /// Replace the manager-wide additional plugins. New session creations and subsequent hook reloads see the new set; already-running sessions keep their existing hook installation until the next reload. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn set_additional_plugins( + &self, + params: SessionsSetAdditionalPluginsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_SETADDITIONALPLUGINS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.getBoardEntryCount`. + /// + /// # Parameters + /// + /// * `params` - Session ID whose board entry count should be returned. + /// + /// # Returns + /// + /// Dynamic-context board entry count, when available. + /// + ///
    + /// + /// **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_board_entry_count( + &self, + params: SessionsGetBoardEntryCountRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_GETBOARDENTRYCOUNT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.startRemoteControl`. + /// + /// # Parameters + /// + /// * `params` - Parameters for attaching the remote-control singleton to a session. + /// + /// # Returns + /// + /// Wrapper for the singleton's current status. + /// + ///
    + /// + /// **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 start_remote_control( + &self, + params: SessionsStartRemoteControlRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_STARTREMOTECONTROL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.transferRemoteControl`. + /// + /// # Parameters + /// + /// * `params` - Parameters for atomically rebinding the remote-control singleton. + /// + /// # Returns + /// + /// Outcome of a transferRemoteControl 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 async fn transfer_remote_control( + &self, + params: SessionsTransferRemoteControlRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_TRANSFERREMOTECONTROL, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `sessions.setRemoteControlSteering`. + /// + /// # Parameters + /// + /// * `params` - Patch for the singleton's steering state. + /// + /// # Returns + /// + /// Wrapper for the singleton's current status. + /// + ///
    + /// + /// **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 set_remote_control_steering( + &self, + params: SessionsSetRemoteControlSteeringRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_SETREMOTECONTROLSTEERING, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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). + /// + /// Wire method: `sessions.stopRemoteControl`. + /// + /// # Returns + /// + /// Outcome of a stopRemoteControl 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 async fn stop_remote_control(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SESSIONS_STOPREMOTECONTROL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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). + /// + /// Wire method: `sessions.stopRemoteControl`. + /// + /// # Parameters + /// + /// * `params` - Parameters for stopping the remote-control singleton. + /// + /// # Returns + /// + /// Outcome of a stopRemoteControl 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 async fn stop_remote_control_with_params( + &self, + params: SessionsStopRemoteControlRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_STOPREMOTECONTROL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) } /// Returns the current state of the remote-control singleton, including the attached session id and frontend URL when active. @@ -2891,6 +4601,137 @@ impl<'a> ClientRpcSkills<'a> { } } + /// `skills.installations.*` sub-namespace. + pub fn installations(&self) -> ClientRpcSkillsInstallations<'a> { + ClientRpcSkillsInstallations { + client: self.client, + } + } + + /// 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. + /// + /// Wire method: `skills.planInstall`. + /// + /// # Parameters + /// + /// * `params` - Side-effect-free planning of one verified Agent Finder Skill candidate. + /// + /// # Returns + /// + /// Skill installation management result with the honoured contract, or a typed refusal. + /// + ///
    + /// + /// **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 plan_install( + &self, + params: SkillPlanInstallRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_PLANINSTALL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Consumes one verified Skill installation plan, requests explicit human consent through installations.confirm on the original connection, then revalidates and installs the Skill disabled. + /// + /// Wire method: `skills.applyInstall`. + /// + /// # Parameters + /// + /// * `params` - Applies exactly one retained verified Skill installation plan. + /// + /// # Returns + /// + /// Skill installation result with the honoured contract, or a typed request/negotiation refusal. + /// + ///
    + /// + /// **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 apply_install( + &self, + params: SkillApplyInstallRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_APPLYINSTALL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `skills.planUninstall`. + /// + /// # Parameters + /// + /// * `params` - Read-only preparation of one owned Skill removal under fresh selected-session authority. + /// + /// # Returns + /// + /// Skill installation management result with the honoured contract, or a typed refusal. + /// + ///
    + /// + /// **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 plan_uninstall( + &self, + params: SkillPlanUninstallRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_PLANUNINSTALL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Consumes an owned Skill removal plan, requests explicit human consent through installations.confirm, refuses drift, and removes the exact owned files through quarantine. + /// + /// Wire method: `skills.applyUninstall`. + /// + /// # Parameters + /// + /// * `params` - One-use application of the exact retained Skill removal plan. + /// + /// # Returns + /// + /// Skill installation result with the honoured contract, or a typed request/negotiation refusal. + /// + ///
    + /// + /// **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 apply_uninstall( + &self, + params: SkillApplyUninstallRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_APPLYUNINSTALL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Discovers skills across global and project sources. /// /// Wire method: `skills.discover`. @@ -2985,16 +4826,182 @@ impl<'a> ClientRpcSkillsConfig<'a> { Some(wire_params), ) .await?; - Ok(()) + Ok(()) + } + + /// Atomically adds or removes one skill from the disabled list. + /// + /// Wire method: `skills.config.setSkillDisabled`. + /// + /// # Parameters + /// + /// * `params` - Adds or removes a single skill from the global disabled list, leaving every other entry untouched. + /// + ///
    + /// + /// **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 set_skill_disabled( + &self, + params: SkillsConfigSetSkillDisabledRequest, + ) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SKILLS_CONFIG_SETSKILLDISABLED, + Some(wire_params), + ) + .await?; + Ok(()) + } +} + +/// `skills.installations.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSkillsInstallations<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcSkillsInstallations<'a> { + /// Lists owned verified Agent Finder Skill installations for the selected existing session. Listing is never gated by the Skill-install feature flag. + /// + /// Wire method: `skills.installations.list`. + /// + /// # Parameters + /// + /// * `params` - Inventory request under an explicitly selected existing session. + /// + /// # Returns + /// + /// Skill installation management result with the honoured contract, or a typed refusal. + /// + ///
    + /// + /// **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: SkillInstallationsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_INSTALLATIONS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `skills.installations.recover`. + /// + /// # Parameters + /// + /// * `params` - Inventory request under an explicitly selected existing session. + /// + /// # Returns + /// + /// Skill installation management result with the honoured contract, or a typed refusal. + /// + ///
    + /// + /// **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 recover( + &self, + params: SkillInstallationsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_INSTALLATIONS_RECOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Inspects a known Skill installation operation on its original runtime connection. Status is never gated by the Skill-install feature flag. + /// + /// Wire method: `skills.installations.status`. + /// + /// # Parameters + /// + /// * `params` - Existing-operation control. A new session selector is deliberately not accepted. + /// + /// # Returns + /// + /// Skill installation management result with the honoured contract, or a typed refusal. + /// + ///
    + /// + /// **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 status( + &self, + params: SkillInstallationOperationRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_INSTALLATIONS_STATUS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Requests cancellation of a known Skill installation operation before commit. Already-started durable work requires recovery instead of silent replay. + /// + /// Wire method: `skills.installations.cancel`. + /// + /// # Parameters + /// + /// * `params` - Existing-operation control. A new session selector is deliberately not accepted. + /// + /// # Returns + /// + /// Skill installation management result with the honoured contract, or a typed refusal. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn cancel( + &self, + params: SkillInstallationOperationRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SKILLS_INSTALLATIONS_CANCEL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) } - /// Atomically adds or removes one skill from the disabled list. + /// 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. /// - /// Wire method: `skills.config.setSkillDisabled`. + /// Wire method: `skills.installations.setEnabled`. /// /// # Parameters /// - /// * `params` - Adds or removes a single skill from the global disabled list, leaving every other entry untouched. + /// * `params` - Persisted enablement update for one owned Skill installation. + /// + /// # Returns + /// + /// Skill installation management result with the honoured contract, or a typed refusal. /// ///
    /// @@ -3003,19 +5010,19 @@ impl<'a> ClientRpcSkillsConfig<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set_skill_disabled( + pub async fn set_enabled( &self, - params: SkillsConfigSetSkillDisabledRequest, - ) -> Result<(), Error> { + params: SkillSetEnabledRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client .call( - rpc_methods::SKILLS_CONFIG_SETSKILLDISABLED, + rpc_methods::SKILLS_INSTALLATIONS_SETENABLED, Some(wire_params), ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } } @@ -3077,33 +5084,13 @@ pub struct ClientRpcUserSettings<'a> { } impl<'a> ClientRpcUserSettings<'a> { - /// Drops this runtime process's in-memory user settings cache so the next settings read observes disk. - /// - /// Wire method: `user.settings.reload`. - /// - ///
    - /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
    - pub async fn reload(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({}); - let _value = self - .client - .call(rpc_methods::USER_SETTINGS_RELOAD, Some(wire_params)) - .await?; - Ok(()) - } - - /// Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. + /// Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. /// /// Wire method: `user.settings.get`. /// /// # Returns /// - /// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + /// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. /// ///
    /// @@ -3121,7 +5108,7 @@ impl<'a> ClientRpcUserSettings<'a> { Ok(serde_json::from_value(_value)?) } - /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. + /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. /// /// Wire method: `user.settings.set`. /// @@ -3129,10 +5116,6 @@ impl<'a> ClientRpcUserSettings<'a> { /// /// * `params` - Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. /// - /// # Returns - /// - /// Outcome of writing user settings. - /// ///
    /// /// **Experimental.** This API is part of an experimental wire-protocol surface @@ -3140,16 +5123,13 @@ impl<'a> ClientRpcUserSettings<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set( - &self, - params: UserSettingsSetRequest, - ) -> Result { + pub async fn set(&self, params: UserSettingsSetRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client .call(rpc_methods::USER_SETTINGS_SET, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } } @@ -3160,6 +5140,13 @@ pub struct SessionRpc<'a> { } impl<'a> SessionRpc<'a> { + /// `session.accounts.*` sub-namespace. + pub fn accounts(&self) -> SessionRpcAccounts<'a> { + SessionRpcAccounts { + session: self.session, + } + } + /// `session.agent.*` sub-namespace. pub fn agent(&self) -> SessionRpcAgent<'a> { SessionRpcAgent { @@ -3244,13 +5231,6 @@ impl<'a> SessionRpc<'a> { } } - /// `session.factory.*` sub-namespace. - pub fn factory(&self) -> SessionRpcFactory<'a> { - SessionRpcFactory { - session: self.session, - } - } - /// `session.fleet.*` sub-namespace. pub fn fleet(&self) -> SessionRpcFleet<'a> { SessionRpcFleet { @@ -3370,6 +5350,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.providers.*` sub-namespace. + pub fn providers(&self) -> SessionRpcProviders<'a> { + SessionRpcProviders { + session: self.session, + } + } + /// `session.queue.*` sub-namespace. pub fn queue(&self) -> SessionRpcQueue<'a> { SessionRpcQueue { @@ -3741,49 +5728,31 @@ impl<'a> SessionRpc<'a> { } } -/// `session.agent.*` RPCs. +/// `session.accounts.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcAgent<'a> { +pub struct SessionRpcAccounts<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcAgent<'a> { - /// Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. - /// - /// Wire method: `session.agent.list`. - /// - /// # Returns - /// - /// Agents available to the session. - /// - ///
    - /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
    - pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); - let _value = self - .session - .client() - .call(rpc_methods::SESSION_AGENT_LIST, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) +impl<'a> SessionRpcAccounts<'a> { + /// `session.accounts.login.*` sub-namespace. + pub fn login(&self) -> SessionRpcAccountsLogin<'a> { + SessionRpcAccountsLogin { + session: self.session, + } } - /// Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. + /// Enumerate a typed accounts collection: the signed-in accounts, or the providers offered for interactive login. /// - /// Wire method: `session.agent.list`. + /// Wire method: `session.accounts.enumerate`. /// /// # Parameters /// - /// * `params` - Controls whether built-in agents and authored prompt text are included. + /// * `params` - Enumerate request carrying the typed collection query. /// /// # Returns /// - /// Agents available to the session. + /// The enumerated collection, keyed by the same selector as the query. /// ///
    /// @@ -3792,50 +5761,31 @@ impl<'a> SessionRpcAgent<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list_with_params(&self, params: AgentListRequest) -> Result { + pub async fn enumerate( + &self, + params: AccountsEnumerateRequest, + ) -> 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_AGENT_LIST, Some(wire_params)) + .call(rpc_methods::SESSION_ACCOUNTS_ENUMERATE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors. /// - /// Wire method: `session.agent.setPrompt`. + /// Wire method: `session.accounts.get`. /// /// # Parameters /// - /// * `params` - An in-memory authored prompt override for an available agent. - /// - ///
    - /// - /// **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 set_prompt(&self, params: AgentSetPromptRequest) -> 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_AGENT_SETPROMPT, Some(wire_params)) - .await?; - Ok(()) - } - - /// Gets the currently selected custom agent for the session. - /// - /// Wire method: `session.agent.getCurrent`. + /// * `params` - Read request carrying the typed datum query. /// /// # Returns /// - /// The currently selected custom agent, or null when using the default agent. + /// The read result, keyed by the same selector as the query. /// ///
    /// @@ -3844,27 +5794,28 @@ impl<'a> SessionRpcAgent<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_current(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn get(&self, params: AccountsGetRequest) -> 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_AGENT_GETCURRENT, Some(wire_params)) + .call(rpc_methods::SESSION_ACCOUNTS_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Selects a custom agent for subsequent turns in the session. + /// Apply one non-interactive accounts mutation: switch the active account, log an account out, or set credentials from a token. /// - /// Wire method: `session.agent.select`. + /// Wire method: `session.accounts.set`. /// /// # Parameters /// - /// * `params` - Name of the custom agent to select for subsequent turns. + /// * `params` - Mutation request carrying the typed write command. /// /// # Returns /// - /// The newly selected custom agent. + /// Result of a non-interactive accounts mutation. /// ///
    /// @@ -3873,153 +5824,36 @@ impl<'a> SessionRpcAgent<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn select(&self, params: AgentSelectRequest) -> Result { + pub async fn set(&self, params: AccountsSetRequest) -> 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_AGENT_SELECT, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } - - /// Clears the selected custom agent and returns the session to the default agent. - /// - /// Wire method: `session.agent.deselect`. - /// - ///
    - /// - /// **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 deselect(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); - let _value = self - .session - .client() - .call(rpc_methods::SESSION_AGENT_DESELECT, Some(wire_params)) - .await?; - Ok(()) - } - - /// Reloads custom agent definitions and returns the refreshed list. - /// - /// Wire method: `session.agent.reload`. - /// - /// # Returns - /// - /// Custom agents available to the session after reloading definitions from disk. - /// - ///
    - /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
    - pub async fn reload(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); - let _value = self - .session - .client() - .call(rpc_methods::SESSION_AGENT_RELOAD, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } -} - -/// `session.autopilotObjective.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcAutopilotObjective<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcAutopilotObjective<'a> { - /// Reads the current canonical autopilot objective state for this session. - /// - /// Wire method: `session.autopilotObjective.getState`. - /// - /// # Returns - /// - /// Canonical runtime state for the session's current autopilot objective. - /// - ///
    - /// - /// **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_state(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); - let _value = self - .session - .client() - .call( - rpc_methods::SESSION_AUTOPILOTOBJECTIVE_GETSTATE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_ACCOUNTS_SET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.canvas.*` RPCs. +/// `session.accounts.login.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcCanvas<'a> { +pub struct SessionRpcAccountsLogin<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcCanvas<'a> { - /// `session.canvas.action.*` sub-namespace. - pub fn action(&self) -> SessionRpcCanvasAction<'a> { - SessionRpcCanvasAction { - session: self.session, - } - } - - /// `session.canvas.provider.*` sub-namespace. - pub fn provider(&self) -> SessionRpcCanvasProvider<'a> { - SessionRpcCanvasProvider { - session: self.session, - } - } - - /// Lists canvases declared for the session. +impl<'a> SessionRpcAccountsLogin<'a> { + /// Begin an interactive login flow for a provider kind (dispatch is kind-only) and return its opaque flow id and first step. /// - /// Wire method: `session.canvas.list`. - /// - /// # Returns - /// - /// Declared canvases available in this 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. + /// Wire method: `session.accounts.login.begin`. /// - ///
    - 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_CANVAS_LIST, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } - - /// Lists currently open canvas instances for the live session. + /// # Parameters /// - /// Wire method: `session.canvas.listOpen`. + /// * `params` - Begin an interactive login flow for a provider kind. Dispatch is kind-only. /// /// # Returns /// - /// Live open-canvas snapshot. + /// A started login flow: its opaque id and first step. /// ///
    /// @@ -4028,27 +5862,28 @@ impl<'a> SessionRpcCanvas<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list_open(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn begin(&self, params: AuthLoginBeginRequest) -> 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_CANVAS_LISTOPEN, Some(wire_params)) + .call(rpc_methods::SESSION_ACCOUNTS_LOGIN_BEGIN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Opens or focuses a canvas instance. + /// Advance an in-flight login flow, optionally fulfilling an input-required step, and return the next step. /// - /// Wire method: `session.canvas.open`. + /// Wire method: `session.accounts.login.advance`. /// /// # Parameters /// - /// * `params` - Canvas open parameters. + /// * `params` - Advance an in-flight login flow, optionally fulfilling an input-required step. /// /// # Returns /// - /// Open canvas instance snapshot. + /// 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). /// ///
    /// @@ -4057,24 +5892,27 @@ impl<'a> SessionRpcCanvas<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn open(&self, params: CanvasOpenRequest) -> Result { + pub async fn advance(&self, params: AuthLoginAdvanceRequest) -> 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_CANVAS_OPEN, Some(wire_params)) + .call( + rpc_methods::SESSION_ACCOUNTS_LOGIN_ADVANCE, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Closes an open canvas instance. + /// Cancel an in-flight login flow and release its resources. /// - /// Wire method: `session.canvas.close`. + /// Wire method: `session.accounts.login.cancel`. /// /// # Parameters /// - /// * `params` - Canvas close parameters. + /// * `params` - Cancel an in-flight login flow. /// ///
    /// @@ -4083,36 +5921,35 @@ impl<'a> SessionRpcCanvas<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn close(&self, params: CanvasCloseRequest) -> Result<(), Error> { + pub async fn cancel(&self, params: AuthLoginCancelRequest) -> 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_CANVAS_CLOSE, Some(wire_params)) + .call( + rpc_methods::SESSION_ACCOUNTS_LOGIN_CANCEL, + Some(wire_params), + ) .await?; Ok(()) } } -/// `session.canvas.action.*` RPCs. +/// `session.agent.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcCanvasAction<'a> { +pub struct SessionRpcAgent<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcCanvasAction<'a> { - /// Invokes an action on an open canvas instance. - /// - /// Wire method: `session.canvas.action.invoke`. - /// - /// # Parameters +impl<'a> SessionRpcAgent<'a> { + /// Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. /// - /// * `params` - Canvas action invocation parameters. + /// Wire method: `session.agent.list`. /// /// # Returns /// - /// Canvas action invocation result. + /// Agents available to the session. /// ///
    /// @@ -4121,35 +5958,27 @@ impl<'a> SessionRpcCanvasAction<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn invoke( - &self, - params: CanvasActionInvokeRequest, - ) -> 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 list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_CANVAS_ACTION_INVOKE, Some(wire_params)) + .call(rpc_methods::SESSION_AGENT_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.canvas.provider.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcCanvasProvider<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcCanvasProvider<'a> { - /// Registers an internal canvas provider connection and its contributions. + /// Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. /// - /// Wire method: `session.canvas.provider.register`. + /// Wire method: `session.agent.list`. /// /// # Parameters /// - /// * `params` - Internal canvas provider registration parameters. + /// * `params` - Controls whether built-in agents and authored prompt text are included. + /// + /// # Returns + /// + /// Agents available to the session. /// ///
    /// @@ -4158,30 +5987,24 @@ impl<'a> SessionRpcCanvasProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn register( - &self, - params: CanvasProviderRegisterRequest, - ) -> Result<(), Error> { + pub async fn list_with_params(&self, params: AgentListRequest) -> 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_CANVAS_PROVIDER_REGISTER, - Some(wire_params), - ) + .call(rpc_methods::SESSION_AGENT_LIST, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Unregisters an internal canvas provider connection. + /// 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. /// - /// Wire method: `session.canvas.provider.unregister`. + /// Wire method: `session.agent.setPrompt`. /// /// # Parameters /// - /// * `params` - Internal canvas provider unregistration parameters. + /// * `params` - An in-memory authored prompt override for an available agent. /// ///
    /// @@ -4190,38 +6013,24 @@ impl<'a> SessionRpcCanvasProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn unregister( - &self, - params: CanvasProviderUnregisterRequest, - ) -> Result<(), Error> { + pub async fn set_prompt(&self, params: AgentSetPromptRequest) -> 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_CANVAS_PROVIDER_UNREGISTER, - Some(wire_params), - ) + .call(rpc_methods::SESSION_AGENT_SETPROMPT, Some(wire_params)) .await?; Ok(()) } -} - -/// `session.commands.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcCommands<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcCommands<'a> { - /// Lists slash commands available in the session. + /// Gets the currently selected custom agent for the session. /// - /// Wire method: `session.commands.list`. + /// Wire method: `session.agent.getCurrent`. /// /// # Returns /// - /// Slash commands available in the session, after applying any include/exclude filters. + /// The currently selected custom agent, or null when using the default agent. /// ///
    /// @@ -4230,27 +6039,27 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { + pub async fn get_current(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_COMMANDS_LIST, Some(wire_params)) + .call(rpc_methods::SESSION_AGENT_GETCURRENT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists slash commands available in the session. + /// Selects a custom agent for subsequent turns in the session. /// - /// Wire method: `session.commands.list`. + /// Wire method: `session.agent.select`. /// /// # Parameters /// - /// * `params` - Optional filters controlling which command sources to include in the listing. + /// * `params` - Name of the custom agent to select for subsequent turns. /// /// # Returns /// - /// Slash commands available in the session, after applying any include/exclude filters. + /// The newly selected custom agent. /// ///
    /// @@ -4259,31 +6068,20 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list_with_params( - &self, - params: CommandsListRequest, - ) -> Result { + pub async fn select(&self, params: AgentSelectRequest) -> 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_COMMANDS_LIST, Some(wire_params)) + .call(rpc_methods::SESSION_AGENT_SELECT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Invokes a slash command in the session. - /// - /// Wire method: `session.commands.invoke`. - /// - /// # Parameters - /// - /// * `params` - Slash command name and optional raw input string to invoke. - /// - /// # Returns + /// Clears the selected custom agent and returns the session to the default agent. /// - /// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). + /// Wire method: `session.agent.deselect`. /// ///
    /// @@ -4292,31 +6090,23 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn invoke( - &self, - params: CommandsInvokeRequest, - ) -> 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 deselect(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_COMMANDS_INVOKE, Some(wire_params)) + .call(rpc_methods::SESSION_AGENT_DESELECT, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Finalizes persistence associated with a client-applied slash-command effect. - /// - /// Wire method: `session.commands.finalizeInvocationEffect`. - /// - /// # Parameters + /// Reloads custom agent definitions and returns the refreshed list. /// - /// * `params` - The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. + /// Wire method: `session.agent.reload`. /// /// # Returns /// - /// Whether finalizing the invocation effect succeeded, and the failure reason when it did not. + /// Custom agents available to the session after reloading definitions from disk. /// ///
    /// @@ -4325,34 +6115,31 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn finalize_invocation_effect( - &self, - params: CommandsFinalizeInvocationEffectRequest, - ) -> 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 reload(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_COMMANDS_FINALIZEINVOCATIONEFFECT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_AGENT_RELOAD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Reports completion of a pending client-handled slash command. - /// - /// Wire method: `session.commands.handlePendingCommand`. - /// - /// # Parameters +/// `session.autopilotObjective.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcAutopilotObjective<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcAutopilotObjective<'a> { + /// Reads the current canonical autopilot objective state for this session. /// - /// * `params` - Pending command request ID and an optional error if the client handler failed. + /// Wire method: `session.autopilotObjective.getState`. /// /// # Returns /// - /// Indicates whether the pending client-handled command was completed successfully. + /// Canonical runtime state for the session's current autopilot objective. /// ///
    /// @@ -4361,34 +6148,48 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn handle_pending_command( - &self, - params: CommandsHandlePendingCommandRequest, - ) -> 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_state(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_COMMANDS_HANDLEPENDINGCOMMAND, + rpc_methods::SESSION_AUTOPILOTOBJECTIVE_GETSTATE, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Executes a slash command synchronously and returns any error. - /// - /// Wire method: `session.commands.execute`. - /// - /// # Parameters +/// `session.canvas.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcCanvas<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcCanvas<'a> { + /// `session.canvas.action.*` sub-namespace. + pub fn action(&self) -> SessionRpcCanvasAction<'a> { + SessionRpcCanvasAction { + session: self.session, + } + } + + /// `session.canvas.provider.*` sub-namespace. + pub fn provider(&self) -> SessionRpcCanvasProvider<'a> { + SessionRpcCanvasProvider { + session: self.session, + } + } + + /// Lists canvases declared for the session. /// - /// * `params` - Slash command name and argument string to execute synchronously. + /// Wire method: `session.canvas.list`. /// /// # Returns /// - /// Error message produced while executing the command, if any. + /// Declared canvases available in this session. /// ///
    /// @@ -4397,31 +6198,23 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn execute( - &self, - params: ExecuteCommandParams, - ) -> 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 list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_COMMANDS_EXECUTE, Some(wire_params)) + .call(rpc_methods::SESSION_CANVAS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Enqueues a slash command for FIFO processing on the local session. - /// - /// Wire method: `session.commands.enqueue`. - /// - /// # Parameters + /// Lists currently open canvas instances for the live session. /// - /// * `params` - Slash-prefixed command string to enqueue for FIFO processing. + /// Wire method: `session.canvas.listOpen`. /// /// # Returns /// - /// Indicates whether the command was accepted into the local execution queue. + /// Live open-canvas snapshot. /// ///
    /// @@ -4430,31 +6223,27 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn enqueue( - &self, - params: EnqueueCommandParams, - ) -> 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 list_open(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_COMMANDS_ENQUEUE, Some(wire_params)) + .call(rpc_methods::SESSION_CANVAS_LISTOPEN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Reports whether the host actually executed a queued command and whether to continue processing. + /// Opens or focuses a canvas instance. /// - /// Wire method: `session.commands.respondToQueuedCommand`. + /// Wire method: `session.canvas.open`. /// /// # Parameters /// - /// * `params` - Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). + /// * `params` - Canvas open parameters. /// /// # Returns /// - /// Indicates whether the queued-command response was matched to a pending request. + /// Open canvas instance snapshot. /// ///
    /// @@ -4463,38 +6252,24 @@ impl<'a> SessionRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn respond_to_queued_command( - &self, - params: CommandsRespondToQueuedCommandRequest, - ) -> Result { + pub async fn open(&self, params: CanvasOpenRequest) -> 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_COMMANDS_RESPONDTOQUEUEDCOMMAND, - Some(wire_params), - ) + .call(rpc_methods::SESSION_CANVAS_OPEN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.completions.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcCompletions<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcCompletions<'a> { - /// 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). + /// Closes an open canvas instance. /// - /// Wire method: `session.completions.getTriggerCharacters`. + /// Wire method: `session.canvas.close`. /// - /// # Returns + /// # Parameters /// - /// 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`). + /// * `params` - Canvas close parameters. /// ///
    /// @@ -4503,32 +6278,36 @@ impl<'a> SessionRpcCompletions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_trigger_characters( - &self, - ) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn close(&self, params: CanvasCloseRequest) -> 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_COMPLETIONS_GETTRIGGERCHARACTERS, - Some(wire_params), - ) + .call(rpc_methods::SESSION_CANVAS_CLOSE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } +} - /// 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. +/// `session.canvas.action.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcCanvasAction<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcCanvasAction<'a> { + /// Invokes an action on an open canvas instance. /// - /// Wire method: `session.completions.request`. + /// Wire method: `session.canvas.action.invoke`. /// /// # Parameters /// - /// * `params` - Request host-driven completions for the current composer input. + /// * `params` - Canvas action invocation parameters. /// /// # Returns /// - /// Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. + /// Canvas action invocation result. /// ///
    /// @@ -4537,35 +6316,35 @@ impl<'a> SessionRpcCompletions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn request( + pub async fn invoke( &self, - params: CompletionsRequestRequest, - ) -> Result { + params: CanvasActionInvokeRequest, + ) -> 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_COMPLETIONS_REQUEST, Some(wire_params)) + .call(rpc_methods::SESSION_CANVAS_ACTION_INVOKE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.connectors.*` RPCs. +/// `session.canvas.provider.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcConnectors<'a> { +pub struct SessionRpcCanvasProvider<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcConnectors<'a> { - /// Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. +impl<'a> SessionRpcCanvasProvider<'a> { + /// Registers an internal canvas provider connection and its contributions. /// - /// Wire method: `session.connectors.getCapabilities`. + /// Wire method: `session.canvas.provider.register`. /// - /// # Returns + /// # Parameters /// - /// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. + /// * `params` - Internal canvas provider registration parameters. /// ///
    /// @@ -4574,26 +6353,30 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_capabilities(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn register( + &self, + params: CanvasProviderRegisterRequest, + ) -> 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_CONNECTORS_GETCAPABILITIES, + rpc_methods::SESSION_CANVAS_PROVIDER_REGISTER, Some(wire_params), ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. + /// Unregisters an internal canvas provider connection. /// - /// Wire method: `session.connectors.getStatus`. + /// Wire method: `session.canvas.provider.unregister`. /// - /// # Returns + /// # Parameters /// - /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + /// * `params` - Internal canvas provider unregistration parameters. /// ///
    /// @@ -4602,27 +6385,38 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_status(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn unregister( + &self, + params: CanvasProviderUnregisterRequest, + ) -> 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_CONNECTORS_GETSTATUS, Some(wire_params)) + .call( + rpc_methods::SESSION_CANVAS_PROVIDER_UNREGISTER, + Some(wire_params), + ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } +} - /// Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. - /// - /// Wire method: `session.connectors.list`. - /// - /// # Parameters +/// `session.commands.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcCommands<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcCommands<'a> { + /// Lists slash commands available in the session. /// - /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + /// Wire method: `session.commands.list`. /// /// # Returns /// - /// Validated Connector catalog snapshot cached by the session. + /// Slash commands available in the session, after applying any include/exclude filters. /// ///
    /// @@ -4631,31 +6425,27 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list( - &self, - params: ConnectorAccountRequest, - ) -> 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 list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_CONNECTORS_LIST, Some(wire_params)) + .call(rpc_methods::SESSION_COMMANDS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Refreshes and validates the Connector catalog for the pinned opaque account selection. + /// Lists slash commands available in the session. /// - /// Wire method: `session.connectors.refresh`. + /// Wire method: `session.commands.list`. /// /// # Parameters /// - /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + /// * `params` - Optional filters controlling which command sources to include in the listing. /// /// # Returns /// - /// Validated Connector catalog snapshot cached by the session. + /// Slash commands available in the session, after applying any include/exclude filters. /// ///
    /// @@ -4664,31 +6454,31 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn refresh( + pub async fn list_with_params( &self, - params: ConnectorAccountRequest, - ) -> Result { + params: CommandsListRequest, + ) -> 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_REFRESH, Some(wire_params)) + .call(rpc_methods::SESSION_COMMANDS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// Invokes a slash command in the session. /// - /// Wire method: `session.connectors.connect`. + /// Wire method: `session.commands.invoke`. /// /// # Parameters /// - /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + /// * `params` - Slash command name and optional raw input string to invoke. /// /// # Returns /// - /// Typed result of initiating or continuing a Connector connection. + /// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). /// ///
    /// @@ -4697,31 +6487,31 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn connect( + pub async fn invoke( &self, - params: ConnectorConnectRequest, - ) -> Result { + params: CommandsInvokeRequest, + ) -> 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_CONNECT, Some(wire_params)) + .call(rpc_methods::SESSION_COMMANDS_INVOKE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. + /// Finalizes persistence associated with a client-applied slash-command effect. /// - /// Wire method: `session.connectors.reconnect`. + /// Wire method: `session.commands.finalizeInvocationEffect`. /// /// # Parameters /// - /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + /// * `params` - The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. /// /// # Returns /// - /// Typed result of initiating or continuing a Connector connection. + /// Whether finalizing the invocation effect succeeded, and the failure reason when it did not. /// ///
    /// @@ -4730,31 +6520,34 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reconnect( + pub(crate) async fn finalize_invocation_effect( &self, - params: ConnectorConnectRequest, - ) -> Result { + params: CommandsFinalizeInvocationEffectRequest, + ) -> 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_RECONNECT, Some(wire_params)) + .call( + rpc_methods::SESSION_COMMANDS_FINALIZEINVOCATIONEFFECT, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. + /// Reports completion of a pending client-handled slash command. /// - /// Wire method: `session.connectors.continueConnection`. + /// Wire method: `session.commands.handlePendingCommand`. /// /// # Parameters /// - /// * `params` - Explicitly bounded continuation of a pending Connector connection. + /// * `params` - Pending command request ID and an optional error if the client handler failed. /// /// # Returns /// - /// Typed result of initiating or continuing a Connector connection. + /// Indicates whether the pending client-handled command was completed successfully. /// ///
    /// @@ -4763,34 +6556,34 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn continue_connection( + pub async fn handle_pending_command( &self, - params: ConnectorContinueRequest, - ) -> Result { + params: CommandsHandlePendingCommandRequest, + ) -> 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_CONTINUECONNECTION, + rpc_methods::SESSION_COMMANDS_HANDLEPENDINGCOMMAND, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. + /// Executes a slash command synchronously and returns any error. /// - /// Wire method: `session.connectors.disconnect`. + /// Wire method: `session.commands.execute`. /// /// # Parameters /// - /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + /// * `params` - Slash command name and argument string to execute synchronously. /// /// # Returns /// - /// Authoritative result after disconnect and MCP reconciliation. + /// Error message produced while executing the command, if any. /// ///
    /// @@ -4799,34 +6592,31 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn disconnect( + pub async fn execute( &self, - params: ConnectorConnectRequest, - ) -> Result { + params: ExecuteCommandParams, + ) -> 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_DISCONNECT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_COMMANDS_EXECUTE, Some(wire_params)) .await?; 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. + /// Enqueues a slash command for FIFO processing on the local session. /// - /// Wire method: `session.connectors.reconcile`. + /// Wire method: `session.commands.enqueue`. /// /// # Parameters /// - /// * `params` - Requests authoritative Connector-to-MCP reconciliation for the pinned account. + /// * `params` - Slash-prefixed command string to enqueue for FIFO processing. /// /// # Returns /// - /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + /// Indicates whether the command was accepted into the local execution queue. /// ///
    /// @@ -4835,31 +6625,31 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reconcile( + pub async fn enqueue( &self, - params: ConnectorReconcileRequest, - ) -> Result { + params: EnqueueCommandParams, + ) -> 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)) + .call(rpc_methods::SESSION_COMMANDS_ENQUEUE, 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. + /// Reports whether the host actually executed a queued command and whether to continue processing. /// - /// Wire method: `session.connectors.reconcileForStartup`. + /// Wire method: `session.commands.respondToQueuedCommand`. /// /// # Parameters /// - /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + /// * `params` - Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). /// /// # Returns /// - /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + /// Indicates whether the queued-command response was matched to a pending request. /// ///
    /// @@ -4868,30 +6658,38 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn reconcile_for_startup( + pub async fn respond_to_queued_command( &self, - params: ConnectorAccountRequest, - ) -> Result { + params: CommandsRespondToQueuedCommandRequest, + ) -> 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_RECONCILEFORSTARTUP, + rpc_methods::SESSION_COMMANDS_RESPONDTOQUEUEDCOMMAND, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Removes the runtime-owned Connector MCP projection without changing service-side connections. +/// `session.completions.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcCompletions<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcCompletions<'a> { + /// 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). /// - /// Wire method: `session.connectors.withdrawProjection`. + /// Wire method: `session.completions.getTriggerCharacters`. /// /// # Returns /// - /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + /// 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`). /// ///
    /// @@ -4900,38 +6698,32 @@ impl<'a> SessionRpcConnectors<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn withdraw_projection(&self) -> Result { + pub async fn get_trigger_characters( + &self, + ) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_CONNECTORS_WITHDRAWPROJECTION, + rpc_methods::SESSION_COMPLETIONS_GETTRIGGERCHARACTERS, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.contentExclusion.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcContentExclusion<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcContentExclusion<'a> { - /// 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. + /// 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. /// - /// Wire method: `session.contentExclusion.checkPaths`. + /// Wire method: `session.completions.request`. /// /// # Parameters /// - /// * `params` - Local file system absolute paths within the session working directory to check against its content-exclusion policy. + /// * `params` - Request host-driven completions for the current composer input. /// /// # Returns /// - /// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. + /// Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. /// ///
    /// @@ -4940,38 +6732,35 @@ impl<'a> SessionRpcContentExclusion<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn check_paths( + pub async fn request( &self, - params: ContentExclusionCheckPathsRequest, - ) -> Result { + params: CompletionsRequestRequest, + ) -> 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_CONTENTEXCLUSION_CHECKPATHS, - Some(wire_params), - ) + .call(rpc_methods::SESSION_COMPLETIONS_REQUEST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.customizations.*` RPCs. +/// `session.connectors.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcCustomizations<'a> { +pub struct SessionRpcConnectors<'a> { pub(crate) session: &'a Session, } -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. +impl<'a> SessionRpcConnectors<'a> { + /// Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. /// - /// Wire method: `session.customizations.reload`. + /// Wire method: `session.connectors.getCapabilities`. /// /// # Returns /// - /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + /// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. /// ///
    /// @@ -4980,38 +6769,26 @@ impl<'a> SessionRpcCustomizations<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reload(&self) -> Result { + pub async fn get_capabilities(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_CUSTOMIZATIONS_RELOAD, + rpc_methods::SESSION_CONNECTORS_GETCAPABILITIES, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.debug.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcDebug<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcDebug<'a> { - /// 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. - /// - /// Wire method: `session.debug.collectLogs`. - /// - /// # Parameters + /// Returns the session account selection, or null. /// - /// * `params` - Options for collecting a session debug bundle with configurable redaction. + /// Wire method: `session.connectors.getAccount`. /// /// # Returns /// - /// Result of collecting a session debug bundle. + /// Session account selection, or null. /// ///
    /// @@ -5020,39 +6797,26 @@ impl<'a> SessionRpcDebug<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn collect_logs( - &self, - params: DebugCollectLogsRequest, - ) -> 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_account(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_DEBUG_COLLECTLOGS, Some(wire_params)) + .call( + rpc_methods::SESSION_CONNECTORS_GETACCOUNT, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.diagnostics.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcDiagnostics<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcDiagnostics<'a> { - /// 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. - /// - /// Wire method: `session.diagnostics.configure`. - /// - /// # Parameters + /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. /// - /// * `params` - Patch session diagnostic thresholds for explicitly supplied sources. + /// Wire method: `session.connectors.getStatus`. /// /// # Returns /// - /// Per-source session diagnostics configuration. + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. /// ///
    /// @@ -5061,34 +6825,27 @@ impl<'a> SessionRpcDiagnostics<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn configure( - &self, - params: DiagnosticsConfigureRequest, - ) -> 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_status(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_DIAGNOSTICS_CONFIGURE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_CONNECTORS_GETSTATUS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// 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. + /// Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. /// - /// Wire method: `session.diagnostics.read`. + /// Wire method: `session.connectors.list`. /// /// # Parameters /// - /// * `params` - Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). + /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. /// /// # Returns /// - /// One cursor-addressed page of retained session diagnostics. + /// Validated Connector catalog snapshot cached by the session. /// ///
    /// @@ -5097,39 +6854,31 @@ impl<'a> SessionRpcDiagnostics<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn read( + pub async fn list( &self, - params: DiagnosticsReadRequest, - ) -> Result { + params: ConnectorAccountRequest, + ) -> 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_DIAGNOSTICS_READ, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.eventLog.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcEventLog<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcEventLog<'a> { - /// Reads a batch of session events from a cursor, optionally waiting for new events. Supports tail-first reads via `direction: backward`. + /// Refreshes and validates the Connector catalog for the pinned opaque account selection. /// - /// Wire method: `session.eventLog.read`. + /// Wire method: `session.connectors.refresh`. /// /// # Parameters /// - /// * `params` - Cursor, batch size, and optional long-poll/filter parameters for reading session events. + /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. /// /// # Returns /// - /// Batch of session events returned by a read, with cursor and continuation metadata. + /// Validated Connector catalog snapshot cached by the session. /// ///
    /// @@ -5138,24 +6887,31 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn read(&self, params: EventLogReadRequest) -> Result { + pub async fn refresh( + &self, + params: ConnectorAccountRequest, + ) -> 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_EVENTLOG_READ, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_REFRESH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns a snapshot of the current tail cursor without consuming events. + /// 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. /// - /// Wire method: `session.eventLog.tail`. + /// Wire method: `session.connectors.connect`. + /// + /// # Parameters + /// + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. /// /// # Returns /// - /// 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). + /// Typed result of initiating or continuing a Connector connection. /// ///
    /// @@ -5164,27 +6920,31 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn tail(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn connect( + &self, + params: ConnectorConnectRequest, + ) -> 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_EVENTLOG_TAIL, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_CONNECT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Registers consumer interest in an event type for runtime gating purposes. + /// Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. /// - /// Wire method: `session.eventLog.registerInterest`. + /// Wire method: `session.connectors.reconnect`. /// /// # Parameters /// - /// * `params` - Event type to register consumer interest for, used by runtime gating logic. + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. /// /// # Returns /// - /// Opaque handle representing an event-type interest registration. + /// Typed result of initiating or continuing a Connector connection. /// ///
    /// @@ -5193,34 +6953,31 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn register_interest( + pub async fn reconnect( &self, - params: RegisterEventInterestParams, - ) -> Result { + params: ConnectorConnectRequest, + ) -> 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_EVENTLOG_REGISTERINTEREST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_CONNECTORS_RECONNECT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Releases a consumer's previously-registered interest in an event type. + /// Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. /// - /// Wire method: `session.eventLog.releaseInterest`. + /// Wire method: `session.connectors.continueConnection`. /// /// # Parameters /// - /// * `params` - Opaque handle previously returned by `registerInterest` to release. + /// * `params` - Explicitly bounded continuation of a pending Connector connection. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Typed result of initiating or continuing a Connector connection. /// ///
    /// @@ -5229,38 +6986,34 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn release_interest( + pub async fn continue_connection( &self, - params: ReleaseEventInterestParams, - ) -> Result { + params: ConnectorContinueRequest, + ) -> 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_EVENTLOG_RELEASEINTEREST, + rpc_methods::SESSION_CONNECTORS_CONTINUECONNECTION, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.extensions.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcExtensions<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcExtensions<'a> { - /// Lists extensions discovered for the session and their current status. + /// Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. /// - /// Wire method: `session.extensions.list`. + /// Wire method: `session.connectors.disconnect`. + /// + /// # Parameters + /// + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. /// /// # Returns /// - /// Extensions discovered for the session, with their current status. + /// Authoritative result after disconnect and MCP reconciliation. /// ///
    /// @@ -5269,23 +7022,34 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn disconnect( + &self, + params: ConnectorConnectRequest, + ) -> 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_EXTENSIONS_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_CONNECTORS_DISCONNECT, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Enables an extension for the session. + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. /// - /// Wire method: `session.extensions.enable`. + /// Wire method: `session.connectors.reconcile`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifier to enable for the session. + /// * `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. /// ///
    /// @@ -5294,24 +7058,31 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn enable(&self, params: ExtensionsEnableRequest) -> Result<(), Error> { + pub async fn reconcile( + &self, + params: ConnectorReconcileRequest, + ) -> 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_EXTENSIONS_ENABLE, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_RECONCILE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Disables an extension for the session. + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. /// - /// Wire method: `session.extensions.disable`. + /// Wire method: `session.connectors.reconcile`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifier to disable for the session. + /// * `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. /// ///
    /// @@ -5320,20 +7091,33 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn disable(&self, params: ExtensionsDisableRequest) -> Result<(), Error> { + /// + /// 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_EXTENSIONS_DISABLE, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_RECONCILE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Reloads extension definitions and processes for the session. + /// Reconciles the authoritative Connector catalog into the session MCP projection during startup with a bounded deadline and fail-closed cleanup. /// - /// Wire method: `session.extensions.reload`. + /// Wire method: `session.connectors.reconcileForStartup`. + /// + /// # Parameters + /// + /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + /// + /// # Returns + /// + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. /// ///
    /// @@ -5342,23 +7126,30 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reload(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn reconcile_for_startup( + &self, + params: ConnectorAccountRequest, + ) -> 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_EXTENSIONS_RELOAD, Some(wire_params)) + .call( + rpc_methods::SESSION_CONNECTORS_RECONCILEFORSTARTUP, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// 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. + /// Removes the runtime-owned Connector MCP projection without changing service-side connections. /// - /// Wire method: `session.extensions.sendAttachmentsToMessage`. + /// Wire method: `session.connectors.withdrawProjection`. /// - /// # Parameters + /// # Returns /// - /// * `params` - Parameters for session.extensions.sendAttachmentsToMessage. + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. /// ///
    /// @@ -5367,49 +7158,38 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn send_attachments_to_message( - &self, - params: SendAttachmentsToMessageParams, - ) -> Result<(), Error> { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub(crate) async fn withdraw_projection(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_EXTENSIONS_SENDATTACHMENTSTOMESSAGE, + rpc_methods::SESSION_CONNECTORS_WITHDRAWPROJECTION, Some(wire_params), ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } } -/// `session.factory.*` RPCs. +/// `session.contentExclusion.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcFactory<'a> { +pub struct SessionRpcContentExclusion<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcFactory<'a> { - /// `session.factory.journal.*` sub-namespace. - pub fn journal(&self) -> SessionRpcFactoryJournal<'a> { - SessionRpcFactoryJournal { - session: self.session, - } - } - - /// Runs a registered factory by name at the top level. +impl<'a> SessionRpcContentExclusion<'a> { + /// 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. /// - /// Wire method: `session.factory.run`. + /// Wire method: `session.contentExclusion.checkPaths`. /// /// # Parameters /// - /// * `params` - Parameters for invoking a registered factory. + /// * `params` - Local file system absolute paths within the session working directory to check against its content-exclusion policy. /// /// # Returns /// - /// Complete current or terminal factory run envelope. + /// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. /// ///
    /// @@ -5418,28 +7198,38 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn run(&self, params: FactoryRunRequest) -> Result { + pub async fn check_paths( + &self, + params: ContentExclusionCheckPathsRequest, + ) -> 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_FACTORY_RUN, Some(wire_params)) + .call( + rpc_methods::SESSION_CONTENTEXCLUSION_CHECKPATHS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Resumes a factory run using its persisted name, arguments, journal, and accounting. - /// - /// Wire method: `session.factory.resume`. - /// - /// # Parameters +/// `session.customizations.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcCustomizations<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcCustomizations<'a> { + /// 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. /// - /// * `params` - Parameters for resuming a factory run from its persisted identity. + /// Wire method: `session.customizations.reload`. /// /// # Returns /// - /// Resolved persisted factory identity and resumed run envelope. + /// 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. /// ///
    /// @@ -5448,28 +7238,38 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn resume(&self, params: FactoryResumeRequest) -> 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 reload(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_RESUME, Some(wire_params)) + .call( + rpc_methods::SESSION_CUSTOMIZATIONS_RELOAD, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} + +/// `session.debug.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcDebug<'a> { + pub(crate) session: &'a Session, +} - /// Internal tool-originated factory invocation. +impl<'a> SessionRpcDebug<'a> { + /// 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. /// - /// Wire method: `session.factory.runFromTool`. + /// Wire method: `session.debug.collectLogs`. /// /// # Parameters /// - /// * `params` - Internal parameters for invoking a registered factory from a tool. + /// * `params` - Options for collecting a session debug bundle with configurable redaction. /// /// # Returns /// - /// Complete current or terminal factory run envelope. + /// Result of collecting a session debug bundle. /// ///
    /// @@ -5478,31 +7278,39 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn run_from_tool( + pub async fn collect_logs( &self, - params: FactoryToolRunRequest, - ) -> Result { + params: DebugCollectLogsRequest, + ) -> 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_FACTORY_RUNFROMTOOL, Some(wire_params)) + .call(rpc_methods::SESSION_DEBUG_COLLECTLOGS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} + +/// `session.diagnostics.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcDiagnostics<'a> { + pub(crate) session: &'a Session, +} - /// Internal tool-originated factory resume. +impl<'a> SessionRpcDiagnostics<'a> { + /// 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. /// - /// Wire method: `session.factory.resumeFromTool`. + /// Wire method: `session.diagnostics.configure`. /// /// # Parameters /// - /// * `params` - Internal parameters for resuming a factory run from a tool. + /// * `params` - Patch session diagnostic thresholds for explicitly supplied sources. /// /// # Returns /// - /// Resolved persisted factory identity and resumed run envelope. + /// Per-source session diagnostics configuration. /// ///
    /// @@ -5511,34 +7319,34 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn resume_from_tool( + pub async fn configure( &self, - params: FactoryToolResumeRequest, - ) -> Result { + params: DiagnosticsConfigureRequest, + ) -> 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_FACTORY_RESUMEFROMTOOL, + rpc_methods::SESSION_DIAGNOSTICS_CONFIGURE, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets the current or settled envelope for a factory run. + /// 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. /// - /// Wire method: `session.factory.getRun`. + /// Wire method: `session.diagnostics.read`. /// /// # Parameters /// - /// * `params` - Parameters for retrieving a factory run. + /// * `params` - Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). /// /// # Returns /// - /// Complete current or terminal factory run envelope. + /// One cursor-addressed page of retained session diagnostics. /// ///
    /// @@ -5547,28 +7355,39 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_run(&self, params: FactoryGetRunRequest) -> Result { + pub async fn read( + &self, + params: DiagnosticsReadRequest, + ) -> 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_FACTORY_GETRUN, Some(wire_params)) + .call(rpc_methods::SESSION_DIAGNOSTICS_READ, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} + +/// `session.eventLog.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcEventLog<'a> { + pub(crate) session: &'a Session, +} - /// Lists durable factory runs for this session in creation order. +impl<'a> SessionRpcEventLog<'a> { + /// Reads a batch of session events from a cursor, optionally waiting for new events. Supports tail-first reads via `direction: backward`. /// - /// Wire method: `session.factory.listRuns`. + /// Wire method: `session.eventLog.read`. /// /// # Parameters /// - /// * `params` - Parameters for paging factory runs. + /// * `params` - Cursor, batch size, and optional long-poll/filter parameters for reading session events. /// /// # Returns /// - /// A page of factory runs in durable creation order. + /// Batch of session events returned by a read, with cursor and continuation metadata. /// ///
    /// @@ -5577,31 +7396,24 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list_runs( - &self, - params: FactoryListRunsRequest, - ) -> Result { + pub async fn read(&self, params: EventLogReadRequest) -> 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_FACTORY_LISTRUNS, Some(wire_params)) + .call(rpc_methods::SESSION_EVENTLOG_READ, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets durable and live observability detail for one factory run. - /// - /// Wire method: `session.factory.getRunDetail`. - /// - /// # Parameters + /// Returns a snapshot of the current tail cursor without consuming events. /// - /// * `params` - Parameters for retrieving a factory run. + /// Wire method: `session.eventLog.tail`. /// /// # Returns /// - /// Full factory run observability detail. + /// 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). /// ///
    /// @@ -5610,31 +7422,27 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_run_detail( - &self, - params: FactoryGetRunRequest, - ) -> 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 tail(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_GETRUNDETAIL, Some(wire_params)) + .call(rpc_methods::SESSION_EVENTLOG_TAIL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Pages durable progress for one factory run. + /// Registers consumer interest in an event type for runtime gating purposes. /// - /// Wire method: `session.factory.getRunProgress`. + /// Wire method: `session.eventLog.registerInterest`. /// /// # Parameters /// - /// * `params` - Parameters for paging factory progress. + /// * `params` - Event type to register consumer interest for, used by runtime gating logic. /// /// # Returns /// - /// A bidirectional page of factory progress. + /// Opaque handle representing an event-type interest registration. /// ///
    /// @@ -5643,34 +7451,34 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_run_progress( + pub async fn register_interest( &self, - params: FactoryGetRunProgressRequest, - ) -> Result { + params: RegisterEventInterestParams, + ) -> 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_FACTORY_GETRUNPROGRESS, + rpc_methods::SESSION_EVENTLOG_REGISTERINTEREST, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Requests cancellation of a factory run and returns its run envelope. + /// Releases a consumer's previously-registered interest in an event type. /// - /// Wire method: `session.factory.cancel`. + /// Wire method: `session.eventLog.releaseInterest`. /// /// # Parameters /// - /// * `params` - Parameters for cancelling a factory run. + /// * `params` - Opaque handle previously returned by `registerInterest` to release. /// /// # Returns /// - /// Complete current or terminal factory run envelope. + /// Indicates whether the operation succeeded. /// ///
    /// @@ -5679,54 +7487,38 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn cancel(&self, params: FactoryCancelRequest) -> Result { + pub async fn release_interest( + &self, + params: ReleaseEventInterestParams, + ) -> 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_FACTORY_CANCEL, Some(wire_params)) + .call( + rpc_methods::SESSION_EVENTLOG_RELEASEINTEREST, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Pauses a running factory and returns its settled run envelope. - /// - /// Wire method: `session.factory.pause`. - /// - /// # Parameters - /// - /// * `params` - Parameters for pausing a running factory. - /// - /// # Returns - /// - /// Complete current or terminal factory run envelope. - /// - ///
    - /// - /// **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 pause(&self, params: FactoryPauseRequest) -> 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_FACTORY_PAUSE, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } +/// `session.extensions.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcExtensions<'a> { + pub(crate) session: &'a Session, +} - /// Atomically pauses an owned factory attempt at a durable checkpoint. +impl<'a> SessionRpcExtensions<'a> { + /// Lists extensions discovered for the session and their current status. /// - /// Wire method: `session.factory.pauseAtCheckpoint`. + /// Wire method: `session.extensions.list`. /// - /// # Parameters + /// # Returns /// - /// * `params` - Parameters for an owned durable pause checkpoint. + /// Extensions discovered for the session, with their current status. /// ///
    /// @@ -5735,34 +7527,23 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn pause_at_checkpoint( - &self, - params: FactoryPauseCheckpointRequest, - ) -> 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 list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_FACTORY_PAUSEATCHECKPOINT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_EXTENSIONS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Records a batch of ordered factory progress lines. + /// Enables an extension for the session. /// - /// Wire method: `session.factory.log`. + /// Wire method: `session.extensions.enable`. /// /// # Parameters /// - /// * `params` - Parameters for recording factory progress. - /// - /// # Returns - /// - /// Acknowledgement that a factory request was accepted. + /// * `params` - Source-qualified extension identifier to enable for the session. /// ///
    /// @@ -5771,28 +7552,24 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn log(&self, params: FactoryLogRequest) -> Result { + pub async fn enable(&self, params: ExtensionsEnableRequest) -> 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_FACTORY_LOG, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_ENABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Runs one factory-scoped subagent and returns its result. + /// Disables an extension for the session. /// - /// Wire method: `session.factory.agent`. + /// Wire method: `session.extensions.disable`. /// /// # Parameters /// - /// * `params` - Parameters for one factory-scoped subagent call. - /// - /// # Returns - /// - /// Result of one factory-scoped subagent call. + /// * `params` - Source-qualified extension identifier to disable for the session. /// ///
    /// @@ -5801,36 +7578,20 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn agent(&self, params: FactoryAgentRequest) -> Result { + pub async fn disable(&self, params: ExtensionsDisableRequest) -> 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_FACTORY_AGENT, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_DISABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } -} - -/// `session.factory.journal.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcFactoryJournal<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcFactoryJournal<'a> { - /// Reads a memoized factory journal entry. - /// - /// Wire method: `session.factory.journal.get`. - /// - /// # Parameters - /// - /// * `params` - Parameters for reading a factory journal entry. - /// - /// # Returns + /// Reloads extension definitions and processes for the session. /// - /// Result of reading a factory journal entry. + /// Wire method: `session.extensions.reload`. /// ///
    /// @@ -5839,31 +7600,23 @@ impl<'a> SessionRpcFactoryJournal<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get( - &self, - params: FactoryJournalGetRequest, - ) -> 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 reload(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_JOURNAL_GET, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_RELOAD, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Stores a memoized factory journal entry. + /// 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. /// - /// Wire method: `session.factory.journal.put`. + /// Wire method: `session.extensions.sendAttachmentsToMessage`. /// /// # Parameters /// - /// * `params` - Parameters for storing a factory journal entry. - /// - /// # Returns - /// - /// Acknowledgement that a factory request was accepted. + /// * `params` - Parameters for session.extensions.sendAttachmentsToMessage. /// ///
    /// @@ -5872,15 +7625,21 @@ impl<'a> SessionRpcFactoryJournal<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn put(&self, params: FactoryJournalPutRequest) -> Result { + pub async fn send_attachments_to_message( + &self, + params: SendAttachmentsToMessageParams, + ) -> 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_FACTORY_JOURNAL_PUT, Some(wire_params)) + .call( + rpc_methods::SESSION_EXTENSIONS_SENDATTACHMENTSTOMESSAGE, + Some(wire_params), + ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } } @@ -6577,10 +8336,14 @@ impl<'a> SessionRpcInstructions<'a> { Ok(serde_json::from_value(_value)?) } - /// Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk. + /// For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. /// /// Wire method: `session.instructions.reload`. /// + /// # Returns + /// + /// Instruction sources loaded for the session, in merge order. + /// ///
    /// /// **Experimental.** This API is part of an experimental wire-protocol surface @@ -6588,14 +8351,14 @@ impl<'a> SessionRpcInstructions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reload(&self) -> Result<(), Error> { + pub async fn reload(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call(rpc_methods::SESSION_INSTRUCTIONS_RELOAD, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } } @@ -6718,7 +8481,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. /// ///
    /// @@ -6766,6 +8529,13 @@ impl<'a> SessionRpcMcp<'a> { } } + /// `session.mcp.prompts.*` sub-namespace. + pub fn prompts(&self) -> SessionRpcMcpPrompts<'a> { + SessionRpcMcpPrompts { + session: self.session, + } + } + /// `session.mcp.resources.*` sub-namespace. pub fn resources(&self) -> SessionRpcMcpResources<'a> { SessionRpcMcpResources { @@ -6773,7 +8543,39 @@ impl<'a> SessionRpcMcp<'a> { } } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + /// + /// Wire method: `session.mcp.setConnectedIdeInfo`. + /// + /// # Parameters + /// + /// * `params` - Records which IDE the host is connected to, or clears it. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn set_connected_ide_info( + &self, + params: SessionMcpSetConnectedIdeInfoParams, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_SETCONNECTEDIDEINFO, + Some(wire_params), + ) + .await?; + Ok(()) + } + + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// /// Wire method: `session.mcp.list`. /// @@ -6798,6 +8600,31 @@ impl<'a> SessionRpcMcp<'a> { Ok(serde_json::from_value(_value)?) } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// + /// Wire method: `session.mcp.listConfigured`. + /// + /// # Returns + /// + /// Effective MCP configuration with optional live observations from matching already materialized servers. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn list_configured(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_LISTCONFIGURED, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// /// Wire method: `session.mcp.listTools`. @@ -6857,6 +8684,34 @@ impl<'a> SessionRpcMcp<'a> { Ok(()) } + /// Enables an MCP server for the session. + /// + /// Wire method: `session.mcp.enable`. + /// + /// # Parameters + /// + /// * `params` - Name of the MCP server to enable for the session. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + /// + /// Accepts [`McpEnableOptions`], including inputs added after [`McpEnableRequest`]. + pub async fn enable_with_options(&self, params: McpEnableOptions) -> 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_ENABLE, Some(wire_params)) + .await?; + Ok(()) + } + /// Disables an MCP server for the session. /// /// Wire method: `session.mcp.disable`. @@ -6883,6 +8738,34 @@ impl<'a> SessionRpcMcp<'a> { Ok(()) } + /// Disables an MCP server for the session. + /// + /// Wire method: `session.mcp.disable`. + /// + /// # Parameters + /// + /// * `params` - Name of the MCP server to disable for the session. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + /// + /// Accepts [`McpDisableOptions`], including inputs added after [`McpDisableRequest`]. + pub async fn disable_with_options(&self, params: McpDisableOptions) -> 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_DISABLE, Some(wire_params)) + .await?; + Ok(()) + } + /// Reloads MCP server connections for the session. /// /// Wire method: `session.mcp.reload`. @@ -7102,9 +8985,95 @@ impl<'a> SessionRpcMcp<'a> { /// /// * `params` - Credential-free authentication identity used to configure GitHub MCP. /// - /// # Returns + /// # Returns + /// + /// Result of configuring GitHub MCP. + /// + ///
    + /// + /// **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 configure_git_hub( + &self, + params: McpConfigureGitHubRequest, + ) -> 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_CONFIGUREGITHUB, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `session.mcp.startServer`. + /// + /// # Parameters + /// + /// * `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. + /// + ///
    + /// + /// **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 start_server(&self, params: McpStartServerRequest) -> 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_STARTSERVER, Some(wire_params)) + .await?; + Ok(()) + } + + /// 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. + /// + /// Wire method: `session.mcp.startServer`. + /// + /// # Parameters + /// + /// * `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. + /// + ///
    + /// + /// **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 [`McpStartServerOptions`], including inputs added after [`McpStartServerRequest`]. + pub async fn start_server_with_options( + &self, + params: McpStartServerOptions, + ) -> 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_STARTSERVER, Some(wire_params)) + .await?; + Ok(()) + } + + /// 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.*`). + /// + /// Wire method: `session.mcp.restartServer`. + /// + /// # Parameters /// - /// Result of configuring GitHub MCP. + /// * `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. /// ///
    /// @@ -7113,27 +9082,24 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub(crate) async fn configure_git_hub( - &self, - params: McpConfigureGitHubRequest, - ) -> Result { + pub async fn restart_server(&self, params: McpRestartServerRequest) -> 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_CONFIGUREGITHUB, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_RESTARTSERVER, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// 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. + /// 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.*`). /// - /// Wire method: `session.mcp.startServer`. + /// Wire method: `session.mcp.restartServer`. /// /// # Parameters /// - /// * `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. + /// * `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. /// ///
    /// @@ -7142,24 +9108,29 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn start_server(&self, params: McpStartServerRequest) -> Result<(), Error> { + /// + /// Accepts [`McpRestartServerOptions`], including inputs added after [`McpRestartServerRequest`]. + pub async fn restart_server_with_options( + &self, + params: McpRestartServerOptions, + ) -> 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_STARTSERVER, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_RESTARTSERVER, Some(wire_params)) .await?; Ok(()) } - /// 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.*`). + /// Stops an individual MCP server on the session's host. /// - /// Wire method: `session.mcp.restartServer`. + /// Wire method: `session.mcp.stopServer`. /// /// # Parameters /// - /// * `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. + /// * `params` - Server name for an individual MCP server stop. /// ///
    /// @@ -7168,13 +9139,13 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn restart_server(&self, params: McpRestartServerRequest) -> Result<(), Error> { + pub async fn stop_server(&self, params: McpStopServerRequest) -> 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_RESTARTSERVER, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_STOPSERVER, Some(wire_params)) .await?; Ok(()) } @@ -7194,7 +9165,12 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn stop_server(&self, params: McpStopServerRequest) -> Result<(), Error> { + /// + /// Accepts [`McpStopServerOptions`], including inputs added after [`McpStopServerRequest`]. + pub async fn stop_server_with_options( + &self, + params: McpStopServerOptions, + ) -> 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 @@ -7515,15 +9491,254 @@ pub struct SessionRpcMcpHeaders<'a> { impl<'a> SessionRpcMcpHeaders<'a> { /// 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. /// - /// Wire method: `session.mcp.headers.handlePendingHeadersRefreshRequest`. + /// Wire method: `session.mcp.headers.handlePendingHeadersRefreshRequest`. + /// + /// # Parameters + /// + /// * `params` - MCP headers refresh request id and the host response. + /// + /// # Returns + /// + /// Indicates whether the pending MCP headers refresh response was accepted. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn handle_pending_headers_refresh_request( + &self, + params: McpHeadersHandlePendingHeadersRefreshRequestRequest, + ) -> 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_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.mcp.oauth.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpOauth<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpOauth<'a> { + /// 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. + /// + /// Wire method: `session.mcp.oauth.handlePendingRequest`. + /// + /// # Parameters + /// + /// * `params` - Pending MCP OAuth request ID and host-provided token or cancellation response. + /// + /// # Returns + /// + /// Indicates whether the pending MCP OAuth response was accepted. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn handle_pending_request( + &self, + params: McpOauthHandlePendingRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_OAUTH_HANDLEPENDINGREQUEST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. + /// + /// Wire method: `session.mcp.oauth.authenticationStateChanged`. + /// + /// # Parameters + /// + /// * `params` - Identifies the MCP server whose persisted OAuth credentials were updated. + /// + ///
    + /// + /// **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 authentication_state_changed( + &self, + params: McpOauthAuthenticationStateChangedRequest, + ) -> 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_AUTHENTICATIONSTATECHANGED, + Some(wire_params), + ) + .await?; + Ok(()) + } + + /// 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. + /// + /// Wire method: `session.mcp.oauth.prepareLogin`. + /// + /// # Parameters + /// + /// * `params` - Effect-free preparation bound to the existing local session, requester and installation, with frozen options. + /// + /// # Returns + /// + /// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn prepare_login( + &self, + params: SessionMcpOauthPrepareLoginParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_OAUTH_PREPARELOGIN, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `session.mcp.oauth.login`. + /// + /// # Parameters + /// + /// * `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. + /// + ///
    + /// + /// **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 login(&self, params: McpOauthLoginRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_OAUTH_LOGIN, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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. + /// + /// Wire method: `session.mcp.oauth.login`. + /// + /// # Parameters + /// + /// * `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. + /// + ///
    + /// + /// **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 [`McpOauthLoginOptions`], including inputs added after [`McpOauthLoginRequest`]. + pub async fn login_with_options( + &self, + params: McpOauthLoginOptions, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_OAUTH_LOGIN, Some(wire_params)) + .await?; + 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`. /// /// # Parameters /// - /// * `params` - MCP headers refresh request id and the host response. + /// * `params` - Remote MCP server name for a passive OAuth status probe. /// /// # Returns /// - /// Indicates whether the pending MCP headers refresh response was accepted. + /// 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. /// ///
    /// @@ -7532,42 +9747,28 @@ impl<'a> SessionRpcMcpHeaders<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn handle_pending_headers_refresh_request( - &self, - params: McpHeadersHandlePendingHeadersRefreshRequestRequest, - ) -> Result { + pub async fn probe(&self, params: McpOauthProbeRequest) -> 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_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_OAUTH_PROBE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.mcp.oauth.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcMcpOauth<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcMcpOauth<'a> { - /// 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. + /// 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.handlePendingRequest`. + /// Wire method: `session.mcp.oauth.probe`. /// /// # Parameters /// - /// * `params` - Pending MCP OAuth request ID and host-provided token or cancellation response. + /// * `params` - Remote MCP server name for a passive OAuth status probe. /// /// # Returns /// - /// Indicates whether the pending MCP OAuth response was accepted. + /// 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. /// ///
    /// @@ -7576,30 +9777,33 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn handle_pending_request( + /// + /// Accepts [`McpOauthProbeOptions`], including inputs added after [`McpOauthProbeRequest`]. + pub async fn probe_with_options( &self, - params: McpOauthHandlePendingRequest, - ) -> Result { + params: McpOauthProbeOptions, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_OAUTH_HANDLEPENDINGREQUEST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_OAUTH_PROBE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. + /// Cancels the exact owned OAuth login issued to this original session requester, without clearing shared credentials. /// - /// Wire method: `session.mcp.oauth.authenticationStateChanged`. + /// Wire method: `session.mcp.oauth.cancelLogin`. /// /// # Parameters /// - /// * `params` - Identifies the MCP server whose persisted OAuth credentials were updated. + /// * `params` - Targets only the original prepared/applying owned login on this exact session requester. + /// + /// # Returns + /// + /// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. /// ///
    /// @@ -7608,34 +9812,34 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn authentication_state_changed( + pub async fn cancel_login( &self, - params: McpOauthAuthenticationStateChangedRequest, - ) -> Result<(), Error> { + params: SessionMcpOauthCancelLoginParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_MCP_OAUTH_AUTHENTICATIONSTATECHANGED, + rpc_methods::SESSION_MCP_OAUTH_CANCELLOGIN, Some(wire_params), ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Starts OAuth authentication for a remote MCP server. + /// Responds to a pending MCP OAuth authorization request by its request id. /// - /// Wire method: `session.mcp.oauth.login`. + /// Wire method: `session.mcp.oauth.respond`. /// /// # 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` - Pending MCP OAuth request id to respond to. /// /// # Returns /// - /// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. + /// Indicates whether the pending MCP OAuth response was accepted. /// ///
    /// @@ -7644,28 +9848,39 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn login(&self, params: McpOauthLoginRequest) -> Result { + pub async fn respond( + &self, + params: McpOauthRespondRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_OAUTH_LOGIN, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_OAUTH_RESPOND, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// 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. +/// `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.oauth.probe`. + /// Wire method: `session.mcp.prompts.list`. /// /// # Parameters /// - /// * `params` - Remote MCP server name for a passive OAuth status probe. + /// * `params` - MCP server whose prompts to enumerate. /// /// # 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. + /// One page of prompts advertised by the named MCP server. /// ///
    /// @@ -7674,28 +9889,28 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn probe(&self, params: McpOauthProbeRequest) -> Result { + pub async fn list(&self, params: McpPromptsListRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_OAUTH_PROBE, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_PROMPTS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Responds to a pending MCP OAuth authorization request by its request id. + /// Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch referenced resources. /// - /// Wire method: `session.mcp.oauth.respond`. + /// Wire method: `session.mcp.prompts.get`. /// /// # Parameters /// - /// * `params` - Pending MCP OAuth request id to respond to. + /// * `params` - MCP server, prompt name, and optional string-valued arguments. /// /// # Returns /// - /// Indicates whether the pending MCP OAuth response was accepted. + /// Prompt messages returned by the MCP server without sending them to the model. /// ///
    /// @@ -7704,16 +9919,13 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn respond( - &self, - params: McpOauthRespondRequest, - ) -> Result { + pub async fn get(&self, params: McpPromptsGetRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_OAUTH_RESPOND, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_PROMPTS_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } @@ -9889,34 +12101,200 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn remove( + 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_PROVIDER_GETENDPOINT, 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. + /// + /// Wire method: `session.provider.getEndpoint`. + /// + /// # Parameters + /// + /// * `params` - Optional model identifier to scope the endpoint snapshot to. + /// + /// # 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_with_params( &self, - params: PluginsMarketplacesRemoveRequest, - ) -> 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_REMOVE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_GETENDPOINT, 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. + /// 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.browse`. + /// Wire method: `session.provider.add`. /// /// # Parameters /// - /// * `params` - Name of the marketplace whose plugin catalog to fetch. + /// * `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 /// - /// Plugins advertised by the marketplace. + /// The selectable model entries synthesized for the models added by this call. /// ///
    /// @@ -9925,30 +12303,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 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_BROWSE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_ADD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Refreshes marketplaces 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.refresh`. + /// Wire method: `session.provider.sync`. + /// + /// # Parameters + /// + /// * `params` - Authoritative BYOK provider and model registry snapshot to apply atomically to the session. /// /// # Returns /// - /// Result of refreshing one or more marketplace catalogs. + /// The selectable model entries and selection ids synthesized for the synchronized BYOK models. /// ///
    /// @@ -9957,30 +12333,28 @@ 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 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_REFRESH, - 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` - Optional marketplace name; omit to refresh all. + /// * `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. /// ///
    /// @@ -9989,38 +12363,42 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn refresh_with_params( + pub async fn withdraw( &self, - params: PluginsMarketplacesRefreshRequest, - ) -> Result { + 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)?) } } -/// `session.provider.*` RPCs. +/// `session.providers.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcProvider<'a> { +pub struct SessionRpcProviders<'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. +impl<'a> SessionRpcProviders<'a> { + /// `session.providers.models.*` sub-namespace. + pub fn models(&self) -> SessionRpcProvidersModels<'a> { + SessionRpcProvidersModels { + session: self.session, + } + } + + /// Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. /// - /// Wire method: `session.provider.getEndpoint`. + /// Wire method: `session.providers.getCatalog`. /// /// # Returns /// - /// A snapshot of the provider endpoint the session is currently configured to talk to. + /// Normalized model-provider adapter definitions available to the session, not discovered instances. /// ///
    /// @@ -10029,27 +12407,27 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_endpoint(&self) -> Result { + 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_PROVIDER_GETENDPOINT, Some(wire_params)) + .call(rpc_methods::SESSION_PROVIDERS_GETCATALOG, 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. + /// 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` - Optional model identifier to scope the endpoint snapshot to. + /// * `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. /// ///
    /// @@ -10058,31 +12436,31 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_endpoint_with_params( + pub async fn discover( &self, - params: ProviderGetEndpointRequest, - ) -> Result { + 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)?) } - /// 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. + /// Gets current health and version information for a discovered model-provider instance. /// - /// Wire method: `session.provider.add`. + /// Wire method: `session.providers.getStatus`. /// /// # 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 status request parameters. /// /// # Returns /// - /// The selectable model entries synthesized for the models added by this call. + /// Current health information for a provider instance. /// ///
    /// @@ -10091,28 +12469,39 @@ 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 get_status( + &self, + params: ModelProviderGetStatusRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PROVIDER_ADD, Some(wire_params)) + .call(rpc_methods::SESSION_PROVIDERS_GETSTATUS, 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. +/// `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.sync`. + /// Wire method: `session.providers.models.list`. /// /// # Parameters /// - /// * `params` - Authoritative BYOK provider and model registry snapshot to apply atomically to the session. + /// * `params` - Provider model inventory request parameters. /// /// # Returns /// - /// The selectable model entries and selection ids synthesized for the synchronized BYOK models. + /// 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. /// ///
    /// @@ -10121,13 +12510,55 @@ 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 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_SYNC, Some(wire_params)) + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_LIST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. + /// + /// Wire method: `session.providers.models.prepareConfiguration`. + /// + /// # Parameters + /// + /// * `params` - A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + /// + /// # Returns + /// + /// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn prepare_configuration( + &self, + params: ModelProviderPrepareConfigurationRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_PREPARECONFIGURATION, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } @@ -10911,6 +13342,42 @@ impl<'a> SessionRpcSandbox<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// 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. + /// + /// Wire method: `session.sandbox.grantPathForRequest`. + /// + /// # Parameters + /// + /// * `params` - Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. + /// + /// # Returns + /// + /// Result of accepting a sandbox path grant. + /// + ///
    + /// + /// **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 grant_path_for_request( + &self, + params: SandboxGrantPathForRequestRequest, + ) -> 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_SANDBOX_GRANTPATHFORREQUEST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } } /// `session.schedule.*` RPCs. @@ -11269,17 +13736,17 @@ pub struct SessionRpcShell<'a> { } impl<'a> SessionRpcShell<'a> { - /// Starts a shell command and streams output through session notifications. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. + /// Starts a shell command, returning an RPC error if it cannot be spawned. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. /// /// Wire method: `session.shell.exec`. /// /// # Parameters /// - /// * `params` - Shell command to run, with optional working directory and timeout in milliseconds. + /// * `params` - Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. /// /// # Returns /// - /// Identifier of the spawned process, used to correlate streamed output and exit notifications. + /// Identifier of the spawned shell process, usable with shell.kill while the process is running. /// ///
    /// @@ -12402,6 +14869,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. + /// + /// Wire method: `session.ui.handleHumanAskUser`. + /// + /// # Parameters + /// + /// * `params` - Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + /// + /// # Returns + /// + /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn handle_human_ask_user( + &self, + params: UIHandlePendingElicitationRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANASKUSER, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves a pending `user_input.requested` event with the user's response. /// /// Wire method: `session.ui.handlePendingUserInput`. @@ -12438,6 +14941,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + /// + /// Wire method: `session.ui.handleHumanUserInput`. + /// + /// # Parameters + /// + /// * `params` - Request ID of a pending `user_input.requested` event and the user's response. + /// + /// # Returns + /// + /// Indicates whether the pending UI request was resolved by this call. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn handle_human_user_input( + &self, + params: UIHandlePendingUserInputRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANUSERINPUT, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. /// /// Wire method: `session.ui.handlePendingSampling`. @@ -12582,6 +15121,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + /// + /// Wire method: `session.ui.handleHumanExitPlanMode`. + /// + /// # Parameters + /// + /// * `params` - Request ID of a pending `exit_plan_mode.requested` event and the user's response. + /// + /// # Returns + /// + /// Indicates whether the pending UI request was resolved by this call. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn handle_human_exit_plan_mode( + &self, + params: UIHandlePendingExitPlanModeRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANEXITPLANMODE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. /// /// Wire method: `session.ui.registerDirectAutoModeSwitchHandler`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 6cc0534f8b..b9f6e0ca73 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")] @@ -449,8 +457,8 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
    - #[serde(rename = "factory.run_updated")] - FactoryRunUpdated, + #[serde(rename = "workflow.run_updated")] + WorkflowRunUpdated, /// ///
    /// @@ -458,8 +466,8 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
    - #[serde(rename = "factory.run_started")] - FactoryRunStarted, + #[serde(rename = "workflow.run_started")] + WorkflowRunStarted, /// ///
    /// @@ -467,8 +475,8 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
    - #[serde(rename = "factory.run_settled")] - FactoryRunSettled, + #[serde(rename = "workflow.run_settled")] + WorkflowRunSettled, #[serde(rename = "session.skills_loaded")] SessionSkillsLoaded, #[serde(rename = "session.custom_agents_updated")] @@ -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")] @@ -969,8 +985,8 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
    - #[serde(rename = "factory.run_updated")] - FactoryRunUpdated(FactoryRunUpdatedData), + #[serde(rename = "workflow.run_updated")] + WorkflowRunUpdated(WorkflowRunUpdatedData), /// ///
    /// @@ -978,8 +994,8 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
    - #[serde(rename = "factory.run_started")] - FactoryRunStarted(FactoryRunStartedData), + #[serde(rename = "workflow.run_started")] + WorkflowRunStarted(WorkflowRunStartedData), /// ///
    /// @@ -987,8 +1003,8 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
    - #[serde(rename = "factory.run_settled")] - FactoryRunSettled(FactoryRunSettledData), + #[serde(rename = "workflow.run_settled")] + WorkflowRunSettled(WorkflowRunSettledData), #[serde(rename = "session.skills_loaded")] SessionSkillsLoaded(SessionSkillsLoadedData), #[serde(rename = "session.custom_agents_updated")] @@ -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, @@ -2121,10 +2146,14 @@ pub struct ResponsesReasoning { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionCompactionCompleteData { - /// Authoritative active-factory reminder appended to the compacted context + /// Legacy active-workflow reminder retained for replay compatibility #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) active_factory_summary: Option, + /// Authoritative active-workflow reminder appended to the compacted context + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) active_workflow_summary: Option, /// Canonical model identifier used for model-specific behavior when replaying compaction #[serde(skip_serializing_if = "Option::is_none")] pub behavior_model_id: Option, @@ -2385,6 +2414,46 @@ pub struct SessionFusionRouteFailedData { pub synthetic_model: String, } +/// +///
    +/// +/// **Experimental.** This type 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 FusionCritic { + /// Concrete model selected for this critic. + /// + ///
    + /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
    + pub model: String, + /// Unique execution phase identifier for this critic. + /// + ///
    + /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
    + pub phase_id: String, + /// Explicit reasoning effort selected for this critic, if supplied. + /// + ///
    + /// + /// **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 reasoning_effort: Option, +} + /// Durable server recommendation for subsequent HydraFusion turns. /// ///
    @@ -2457,6 +2526,16 @@ pub struct FusionScores { pub struct SessionFusionResolvedData { /// Version of the validated HydraFusion event contract. pub contract_version: i64, + /// 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. + /// + ///
    + /// + /// **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 critics: Option>, /// Concrete model used when the planned primary model cannot execute. pub fallback_model: String, /// Router recommendation controlling reuse or rerouting on later turns. @@ -2476,6 +2555,16 @@ pub struct SessionFusionResolvedData { ///
    #[serde(skip_serializing_if = "Option::is_none")] pub hint: Option, + /// Concrete model selected for Cascade escalation-gate calls, when required. + /// + ///
    + /// + /// **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 judge_model: Option, /// Version of the executable model universe used for selection. #[serde(skip_serializing_if = "Option::is_none")] pub model_universe_version: Option, @@ -2501,6 +2590,16 @@ pub struct SessionFusionResolvedData { pub policy_version: Option, /// Concrete model selected for the primary solver phase. pub primary_model: String, + /// Concrete model selected for Cascade repair, when required. + /// + ///
    + /// + /// **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 repair_model: Option, /// Router implementation that supplied the plan. #[serde(skip_serializing_if = "Option::is_none")] pub route_source: Option, @@ -2519,7 +2618,7 @@ pub struct SessionFusionResolvedData { /// Validated capability scores used to select the route. #[serde(skip_serializing_if = "Option::is_none")] pub scores: Option, - /// Concrete model selected for the review or judge phase, when required. + /// Concrete model selected for Critique review, or the legacy Cascade judge/repair model when role-specific fields are absent. pub secondary_model: Option, /// Synthetic HydraFusion model selected for the session. pub synthetic_model: String, @@ -2657,6 +2756,9 @@ pub struct AssistantTurnStartData { /// Model identifier used for this turn, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Parent task tool call ID when this turn belongs to a sub-agent + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_tool_call_id: Option, /// Identifier for this turn within the agentic loop, typically a stringified turn number pub turn_id: String, } @@ -2748,6 +2850,16 @@ pub struct AssistantFusionPhaseStartedData { pub phase_id: String, /// Kind of phase being executed. pub phase_kind: FusionPhaseKind, + /// Explicit reasoning effort selected for this phase, if supplied. + /// + ///
    + /// + /// **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 reasoning_effort: Option, /// Semantic role assigned to the phase. pub role: String, } @@ -2862,6 +2974,16 @@ pub struct AssistantFusionPhaseCompletedData { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) projection_mode: Option, + /// Explicit reasoning effort selected for this phase, if supplied. + /// + ///
    + /// + /// **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 reasoning_effort: Option, /// Semantic role assigned to the completed phase. pub role: String, /// Terminal request held outside canonical state until selected by the final commit. @@ -2907,6 +3029,16 @@ pub struct AssistantFusionPhaseFailedData { pub phase_kind: FusionPhaseKind, /// Stable machine-readable reason for the phase failure. pub reason: String, + /// Explicit reasoning effort selected for this phase, if supplied. + /// + ///
    + /// + /// **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 reasoning_effort: Option, /// Semantic role assigned to the failed phase. pub role: String, /// Durable outcome status of the phase. @@ -3119,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, } @@ -3319,6 +3454,9 @@ pub struct AssistantTurnEndData { /// Model identifier used for this turn, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Parent task tool call ID when this turn belongs to a sub-agent + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_tool_call_id: Option, /// Identifier of the turn that has ended, matching the corresponding assistant.turn_start event pub turn_id: String, } @@ -3424,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")] @@ -3503,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")] @@ -3635,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")] @@ -3687,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, @@ -3733,6 +3884,12 @@ 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, /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, @@ -3762,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")] @@ -3799,6 +3969,9 @@ pub struct ModelCallStartData { /// Model identifier used for this API call, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Parent task tool call ID when this model call belongs to a sub-agent + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_tool_call_id: Option, /// Previous response or interaction identifier included in the model request, when present #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] @@ -3947,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, } @@ -3981,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")] @@ -4444,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. /// ///
    @@ -4782,6 +5009,35 @@ pub struct SandboxDecisionDataAccessDenied { pub tool_call_id: Option, } +/// Permissive learning mode (record and allow) recorded an access that the enforced policy would have refused, and allowed it. Emitted once per distinct recorded access of a record-and-allow run, bounded per command. The only per-access record of such a run: nothing was refused, so no `access_denied` is raised for it. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxDecisionDataAccessRecorded { + /// Command that made the access. Populated only when content capture is enabled. + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + /// Sandbox control the access belongs to. Follows from `denialClass`. + pub control: SandboxControl, + /// Bounded class of the access the enforced policy would have refused. + pub denial_class: SandboxDenialClass, + /// Resource the enforced policy would have refused. Populated only when content capture is enabled. + #[serde(skip_serializing_if = "Option::is_none")] + pub denied_resource: Option, + /// Runtime subsystem that ran the command + pub enforcement_point: SandboxEnforcementPoint, + /// Sandbox decision variant discriminator. + pub kind: SandboxDecisionDataAccessRecordedKind, + /// Always `allowed`. + pub outcome: SandboxOutcome, + /// Why the run recorded and allowed instead of enforcing. + pub permissive_source: SandboxPermissiveSource, + /// Host operating-system family + pub platform: SandboxPlatform, + /// Tool call the access belongs to, for span correlation only. Never exported as a telemetry attribute or metric dimension. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + /// A request to run outside the process sandbox was resolved. This is what makes an `inactive` `enforcement_state` readable: without it, a command that ran unsandboxed because a person approved a bypass looks identical to one that ran unsandboxed because the session never had a sandbox. Reported only when a sandbox was in force, since bypassing a disabled sandbox bypasses nothing. Carries neither backend nor attestation: the verdict comes from the runtime's own permission flow or the local escalation prompt, not from a containment backend and not from the built-in policy check, so `source` is what records where it came from. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4904,7 +5160,7 @@ pub struct SubagentStartedData { /// Whether the sub-agent runs synchronously or in the background. #[serde(skip_serializing_if = "Option::is_none")] pub execution_mode: Option, - /// Root id of the factory run that spawned this sub-agent, when it was spawned by one. + /// Legacy root id of the workflow run that spawned this sub-agent. New consumers should use workflowRunId. #[serde(skip_serializing_if = "Option::is_none")] pub factory_run_id: Option, /// Model the sub-agent will run with, when known at start. @@ -4919,11 +5175,14 @@ pub struct SubagentStartedData { /// Whether this sub-agent can be resumed. Currently always false. #[serde(skip_serializing_if = "Option::is_none")] pub resumable: Option, - /// Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and factory agents); absent for sub-agents created through other runtime paths. + /// Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and workflow agents); absent for sub-agents created through other runtime paths. #[serde(skip_serializing_if = "Option::is_none")] pub task_model_source: Option, /// Tool call ID of the parent tool invocation that spawned this sub-agent pub tool_call_id: String, + /// Root id of the workflow run that spawned this sub-agent, when it was spawned by one. + #[serde(skip_serializing_if = "Option::is_none")] + pub workflow_run_id: Option, } /// Session event "subagent.configured". Resolved runtime configuration for a configured sub-agent @@ -5227,6 +5486,29 @@ pub struct PermissionRequestShellPossibleUrl { pub url: String, } +/// A sandbox filesystem policy edit that would let a blocked operation run inside the sandbox instead of outside it. Offered only on a sandbox escalation request whose denial adding this path lifts, and only when managed policy permits the grant. A host accepts it with session.sandbox.grantPathForRequest, which adds the path to the session's sandbox policy and re-runs the operation sandboxed; a host that persists sandbox settings may also save the path there. +/// +///
    +/// +/// **Experimental.** This type 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 PermissionSandboxPathGrant { + /// Which access the grant confers, and so which policy list the path is added to + pub access: PermissionSandboxPathGrantAccess, + /// The path the sandbox refused, present only when it differs from path. That happens when a write under a read-only folder moves the folder to the read-write paths, when a path that does not exist yet is granted through its nearest existing folder, because the OS sandbox cannot grant a path before it exists, and when either is spelled through a symlink, because a grant covers its path as written, so path is then the resolved location. Hosts should then name path in the offer, since the denial names this one. + #[serde(skip_serializing_if = "Option::is_none")] + pub denied_path: Option, + /// Absolute path to add to the sandbox filesystem policy + pub path: String, + /// readonlyPaths entries the grant removes, exactly as written in the policy, because a read-only entry for the same location would otherwise keep the path read-only. A host that persists the path must remove these entries from its stored readonlyPaths too. + #[serde(skip_serializing_if = "Option::is_none")] + pub removed_readonly_paths: Option>, +} + /// Shell command permission request #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -5282,6 +5564,16 @@ pub struct PermissionRequestShell { ///
    #[serde(skip_serializing_if = "Option::is_none")] pub resolved_working_directory: Option, + /// Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + /// + ///
    + /// + /// **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 sandbox_path_grant: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -5326,6 +5618,16 @@ pub struct PermissionRequestWrite { ///
    #[serde(skip_serializing_if = "Option::is_none")] pub resolved_path: Option, + /// Sandbox policy edit that would let the write run inside the sandbox. Only present when requestSandboxBypass is true. + /// + ///
    + /// + /// **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 sandbox_path_grant: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -5360,6 +5662,16 @@ pub struct PermissionRequestRead { ///
    #[serde(skip_serializing_if = "Option::is_none")] pub resolved_path: Option, + /// Sandbox policy edit that would let the read run inside the sandbox. Only present when requestSandboxBypass is true. + /// + ///
    + /// + /// **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 sandbox_path_grant: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -5581,10 +5893,10 @@ pub struct PermissionRequestExtensionManagement { pub tool_call_id: Option, } -/// A declared phase shown in a factory permission prompt. +/// A declared phase shown in a workflow permission prompt. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryPermissionPhase { +pub struct WorkflowPermissionPhase { /// Optional phase detail #[serde(skip_serializing_if = "Option::is_none")] pub detail: Option, @@ -5592,31 +5904,31 @@ pub struct FactoryPermissionPhase { pub title: String, } -/// Factory run or authoring permission request +/// Workflow run or authoring permission request #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionRequestFactory { - /// Canonical key used for scoped factory approvals +pub struct PermissionRequestWorkflow { + /// Canonical key used for scoped workflow approvals pub approval_key: String, - /// Whether this factory is eligible for persistent approval + /// Whether this workflow is eligible for persistent approval pub can_persist_approval: bool, - /// Factory-declared AI-credit limit before any run/resume caller override is applied. + /// Workflow-declared AI-credit limit before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_max_ai_credits: Option, - /// Factory-declared concurrent-subagent limit before any run/resume caller override is applied. + /// Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_max_concurrent_subagents: Option, - /// Factory-declared total-subagent limit before any run/resume caller override is applied. + /// Workflow-declared total-subagent limit before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_max_total_subagents: Option, - /// Factory-declared active-time limit in seconds before any run/resume caller override is applied. + /// Workflow-declared active-time limit in seconds before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_timeout_seconds: Option, - /// Factory description + /// Workflow description pub description: String, /// Permission kind discriminator - pub kind: PermissionRequestFactoryKind, - /// When true, managed policy requires an explicit user decision and automatic approval must be bypassed. + pub kind: PermissionRequestWorkflowKind, + /// Whether managed policy requires a human response and forbids host auto-approval #[serde(skip_serializing_if = "Option::is_none")] pub managed_approval_required: Option, /// Effective AI-credit limit; omitted means unlimited @@ -5628,12 +5940,12 @@ pub struct PermissionRequestFactory { /// Effective total-subagent limit; omitted means unlimited #[serde(skip_serializing_if = "Option::is_none")] pub max_total_subagents: Option, - /// Factory name + /// Workflow name pub name: String, - /// Factory operation, either run or author - pub operation: FactoryPermissionOperation, - /// Declared factory phases - pub phases: Vec, + /// Workflow operation, either run or author + pub operation: WorkflowPermissionOperation, + /// Declared workflow phases + pub phases: Vec, /// Effective active-time limit in seconds; omitted means unlimited #[serde(skip_serializing_if = "Option::is_none")] pub timeout_seconds: Option, @@ -5714,6 +6026,16 @@ pub struct PermissionPromptRequestCommands { /// True when the escalation is a permissive retry that keeps the sandbox and network policy attached while recording file and process accesses instead of blocking them. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_permissive: Option, + /// Sandbox policy edit that would let the command run inside the sandbox. Only present when requestSandboxBypass is true. + /// + ///
    + /// + /// **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 sandbox_path_grant: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -5971,6 +6293,16 @@ pub struct PermissionPromptRequestPath { pub kind: PermissionPromptRequestPathKind, /// File paths that require explicit approval pub paths: Vec, + /// Canonical directory candidates that can be granted for file-tool read access in this logical session. Present only for read path prompts. + /// + ///
    + /// + /// **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 read_only_directories: Option>, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -6031,11 +6363,11 @@ pub struct PermissionPromptRequestExtensionManagement { pub tool_call_id: Option, } -/// Factory run or authoring permission prompt +/// Workflow run or authoring permission prompt #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionPromptRequestFactory { - /// Canonical key used for scoped factory approvals +pub struct PermissionPromptRequestWorkflow { + /// Canonical key used for scoped workflow approvals pub approval_key: String, /// Assisted-approval judge information for this request; present only in assisted mode. /// @@ -6047,24 +6379,24 @@ pub struct PermissionPromptRequestFactory { ///
    #[serde(skip_serializing_if = "Option::is_none")] pub assisted_approval: Option, - /// Whether this factory is eligible for persistent approval + /// Whether this workflow is eligible for persistent approval pub can_persist_approval: bool, - /// Factory-declared AI-credit limit before any run/resume caller override is applied. + /// Workflow-declared AI-credit limit before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_max_ai_credits: Option, - /// Factory-declared concurrent-subagent limit before any run/resume caller override is applied. + /// Workflow-declared concurrent-subagent limit before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_max_concurrent_subagents: Option, - /// Factory-declared total-subagent limit before any run/resume caller override is applied. + /// Workflow-declared total-subagent limit before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_max_total_subagents: Option, - /// Factory-declared active-time limit in seconds before any run/resume caller override is applied. + /// Workflow-declared active-time limit in seconds before any run/resume caller override is applied. #[serde(skip_serializing_if = "Option::is_none")] pub declared_timeout_seconds: Option, - /// Factory description + /// Workflow description pub description: String, /// Prompt kind discriminator - pub kind: PermissionPromptRequestFactoryKind, + pub kind: PermissionPromptRequestWorkflowKind, /// Whether managed policy requires a human response and forbids host auto-approval #[serde(skip_serializing_if = "Option::is_none")] pub managed_approval_required: Option, @@ -6077,12 +6409,12 @@ pub struct PermissionPromptRequestFactory { /// Effective total-subagent limit; omitted means unlimited #[serde(skip_serializing_if = "Option::is_none")] pub max_total_subagents: Option, - /// Factory name + /// Workflow name pub name: String, - /// Factory operation, either run or author - pub operation: FactoryPermissionOperation, - /// Declared factory phases - pub phases: Vec, + /// Workflow operation, either run or author + pub operation: WorkflowPermissionOperation, + /// Declared workflow phases + pub phases: Vec, /// Effective active-time limit in seconds; omitted means unlimited #[serde(skip_serializing_if = "Option::is_none")] pub timeout_seconds: Option, @@ -6180,6 +6512,16 @@ pub struct PermissionApproved { pub managed_approval_handled: Option, } +/// Permission response variant that approves a request and records file-tool read authority for specific directories in this logical session. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionApprovedReadOnlyForSession { + /// Canonical directories covered by the session read-only grant + pub directories: Vec, + /// Approved with read-only directory authority for this session + pub kind: PermissionApprovedReadOnlyForSessionKind, +} + /// Session-scoped tool-approval rule for specific shell command identifiers. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -6247,15 +6589,15 @@ pub struct UserToolSessionApprovalExtensionManagement { pub operation: Option, } -/// Session-scoped factory approval, optionally narrowed by approval key. +/// Session-scoped workflow approval, optionally narrowed by approval key. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct UserToolSessionApprovalFactory { - /// Optional factory operation name or canonical approval key +pub struct UserToolSessionApprovalWorkflow { + /// Optional workflow operation name or canonical approval key #[serde(skip_serializing_if = "Option::is_none")] pub approval_key: Option, - /// Factory approval kind - pub kind: UserToolSessionApprovalFactoryKind, + /// Workflow approval kind + pub kind: UserToolSessionApprovalWorkflowKind, } /// Session-scoped tool-approval rule for an extension's permission-gated capability access, keyed by extension name. @@ -6416,7 +6758,7 @@ pub struct PermissionCompletedData { pub tool_call_id: Option, } -/// Session event "permission.carriedForward". Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. +/// Session event "permission.carriedForward". Historical decode-only receipt from the retired Assisted Permissions authorization extractor. Current runtimes ignore it for permission decisions. /// ///
    /// @@ -6465,7 +6807,7 @@ pub struct PermissionCarriedForwardData { pub tool_call_id: String, } -/// Session event "permission.messageAuthorization". Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. +/// Session event "permission.messageAuthorization". Historical decode-only claim from the retired Assisted Permissions authorization extractor. Current runtimes preserve the payload but do not establish authority from it. /// ///
    /// @@ -6562,7 +6904,7 @@ pub struct PermissionMessageAuthorizationData { pub world: Option, } -/// Session event "permission.messageAuthorizationRead". Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Also records whether that pass activates ongoing extraction; contextual-assent-only passes do not, so unrelated future messages remain outside extraction. +/// Session event "permission.messageAuthorizationRead". Historical decode-only extractor progress marker. Current runtimes do not run or resume extraction from it. /// ///
    /// @@ -6594,7 +6936,7 @@ pub struct PermissionMessageAuthorizationReadData { pub turn_index: i64, } -/// Session event "permission.messageAuthorizationDegraded". Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. +/// Session event "permission.messageAuthorizationDegraded". Historical decode-only degradation marker from the retired extractor. Current runtimes ignore it for permission decisions. /// ///
    /// @@ -6616,7 +6958,7 @@ pub struct PermissionMessageAuthorizationDegradedData { pub turn_index: i64, } -/// Session event "permission.assentDetected". Records that deterministic text recognition found likely assent in the human turn immediately following a root Autopilot permission request that was blocked because no interactive response was available. This event grants no authority; its model-facing projection only suggests retrying the unchanged operation. +/// Session event "permission.assentDetected". Historical decode-only contextual-assent marker. Current runtimes do not project it into the conversation or permission flow. /// ///
    /// @@ -6647,7 +6989,7 @@ pub struct PermissionAssentDetectedData { pub turn_index: i64, } -/// Session event "permission.contextualAuthorization". Freezes a blinded contextual authorization proposal whose verbatim human span was deterministically bound to the immediately preceding blocked permission request. The event carries no action fields; replay re-derives the exact action from the earlier permission request and mints a one-shot message grant only when the binding and span still verify. +/// Session event "permission.contextualAuthorization". Historical decode-only contextual authorization claim. Current runtimes preserve the payload but do not establish authority from it. /// ///
    /// @@ -7162,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. /// ///
    /// @@ -7313,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")] @@ -7326,7 +7744,7 @@ pub struct SessionToolsUpdatedData { #[serde(rename_all = "camelCase")] pub struct SessionBackgroundTasksChangedData {} -/// Session event "factory.run_updated". Ephemeral invalidation signal for a changed factory run. +/// Session event "workflow.run_updated". Ephemeral invalidation signal for a changed workflow run. /// ///
    /// @@ -7336,14 +7754,14 @@ pub struct SessionBackgroundTasksChangedData {} ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunUpdatedData { +pub struct WorkflowRunUpdatedData { /// Monotonic revision now available for the run. pub revision: i64, - /// Factory run identifier. + /// Workflow run identifier. pub run_id: String, } -/// Session event "factory.run_started". Ephemeral signal that a factory run attempt began executing. +/// Session event "workflow.run_started". Ephemeral signal that a workflow run attempt began executing. /// ///
    /// @@ -7353,16 +7771,16 @@ pub struct FactoryRunUpdatedData { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunStartedData { +pub struct WorkflowRunStartedData { /// Attempt number this start committed; a resumed run increments it. pub attempt: i64, - /// Name of the factory this run executes. Low cardinality by construction. - pub factory_name: String, - /// Identifier of the factory run that started. + /// Identifier of the workflow run that started. pub run_id: String, + /// Name of the workflow this run executes. Low cardinality by construction. + pub workflow_name: String, } -/// Session event "factory.run_settled". Ephemeral signal that a factory run reached a terminal status. +/// Session event "workflow.run_settled". Ephemeral signal that a workflow run reached a terminal status. /// ///
    /// @@ -7372,20 +7790,20 @@ pub struct FactoryRunStartedData { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct FactoryRunSettledData { +pub struct WorkflowRunSettledData { /// AI credits this run consumed, in nano-AIU. pub consumed_nano_aiu: i64, /// Subagents this run consumed against its limits. pub consumed_subagents: i64, /// Active milliseconds accumulated across every attempt of this run. pub elapsed_ms: i64, - /// Typed failure class recorded on the run, when it failed with one (e.g. `factory_limit_reached`). + /// Typed failure class recorded on the run, when it failed with one (e.g. `workflow_limit_reached`). #[serde(skip_serializing_if = "Option::is_none")] pub failure_type: Option, - /// Identifier of the factory run that settled. + /// Identifier of the workflow run that settled. pub run_id: String, /// Terminal status the run committed. - pub status: FactoryRunSettledStatus, + pub status: WorkflowRunSettledStatus, } /// A single resolved skill in `session.skills_loaded`, including source, invocability, enabled state, path, and argument hint. @@ -7516,9 +7934,15 @@ pub struct SessionMcpServersLoadedData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMcpServerStatusChangedData { + /// 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 #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, + /// Runtime-produced classification for the final failed connection; unclassified means no classification was supplied. Additional string values may be introduced. + #[serde(skip_serializing_if = "Option::is_none")] + pub error_classification: Option, /// Name of the MCP server whose status changed pub server_name: String, /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured @@ -8225,6 +8649,9 @@ pub enum ModelChangeSource { /// An SDK or RPC caller selected the model. #[serde(rename = "sdk")] Sdk, + /// The user accepted a CAPI-issued Auto tier recommendation. + #[serde(rename = "auto_tier_recommendation")] + AutoTierRecommendation, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -9035,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 { @@ -9077,7 +9537,7 @@ pub enum AbortReason { Unknown, } -/// Configuration source: user, workspace, plugin, builtin, or managed +/// Configuration source: user, workspace, plugin, builtin, managed, or account #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpServerSource { /// Server configured in the user's global MCP configuration. @@ -9095,6 +9555,9 @@ pub enum McpServerSource { /// Server supplied by a trusted host-managed catalog. #[serde(rename = "managed")] Managed, + /// Server contributed by a signed-in account; enablement and organization policy still apply. + #[serde(rename = "account")] + Account, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -9137,6 +9600,70 @@ pub enum ToolExecutionStartToolDescriptionMetaUIVisibility { Unknown, } +/// Shell output source. Terminal output has no separate stdout/stderr attribution. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ToolShellOutputStream { + /// Output from the shell command's standard output stream. This is the default when stream is omitted. + #[serde(rename = "stdout")] + Stdout, + /// Output from the shell command's standard error stream. + #[serde(rename = "stderr")] + Stderr, + /// Inherently merged output that cannot be attributed separately to stdout or stderr. + #[serde(rename = "terminal")] + Terminal, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Kind of file mutation committed by a built-in editing tool. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum ToolExecutionCompleteFileEditKind { + /// A file was created. + Create, + /// A file was written by an edit operation. + Edit, + /// A file was deleted. + Delete, + /// An unrecognized operation kind, retaining its wire value. + Unknown(String), +} +impl Default for ToolExecutionCompleteFileEditKind { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} +impl From for ToolExecutionCompleteFileEditKind { + fn from(value: String) -> Self { + match value.as_str() { + "create" => Self::Create, + "edit" => Self::Edit, + "delete" => Self::Delete, + _ => Self::Unknown(value), + } + } +} +impl From for String { + fn from(value: ToolExecutionCompleteFileEditKind) -> Self { + match value { + ToolExecutionCompleteFileEditKind::Create => "create".to_owned(), + ToolExecutionCompleteFileEditKind::Edit => "edit".to_owned(), + ToolExecutionCompleteFileEditKind::Delete => "delete".to_owned(), + ToolExecutionCompleteFileEditKind::Unknown(value) => value, + } + } +} + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PersistedBinaryImageType { @@ -9439,7 +9966,7 @@ pub enum SandboxDecisionDataPolicyResolvedKind { PolicyResolved, } -/// Finite result of a sandbox decision. Each `SandboxDecisionData` variant uses a disjoint subset: `policy_resolved` is `resolved | degraded`, `spawn_completed` and `permissive_retry_completed` are `succeeded | failed`, `enforcement_state` is `engaged | inactive | failed`, `access_denied` is `denied`, and escalation decisions are `approved | declined`. +/// Finite result of a sandbox decision. Each `SandboxDecisionData` variant uses a disjoint subset: `policy_resolved` is `resolved | degraded`, `spawn_completed` and `permissive_retry_completed` are `succeeded | failed`, `enforcement_state` is `engaged | inactive | failed`, `access_denied` is `denied`, `access_recorded` is `allowed`, and escalation decisions are `approved | declined`. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SandboxOutcome { /// The sandbox policy resolved successfully. Describes configuration only and makes no claim that a backend engaged. @@ -9469,6 +9996,9 @@ pub enum SandboxOutcome { /// A request to run outside the process sandbox was not granted. #[serde(rename = "declined")] Declined, + /// Permissive learning mode recorded an access the enforced policy would have refused, and allowed it. + #[serde(rename = "allowed")] + Allowed, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -9640,6 +10170,29 @@ pub enum SandboxDecisionDataAccessDeniedKind { AccessDenied, } +/// Sandbox decision variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SandboxDecisionDataAccessRecordedKind { + #[serde(rename = "access_recorded")] + #[default] + AccessRecorded, +} + +/// Why a sandboxed run recorded and allowed its process-container access checks instead of enforcing them. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SandboxPermissiveSource { + /// A person approved the permissive retry for this command after a sandboxed attempt was blocked. + #[serde(rename = "approved_retry")] + ApprovedRetry, + /// Device-managed policy (`sandbox.learningMode: "allow"`) starts sandboxed shell commands in permissive learning mode. + #[serde(rename = "policy")] + Policy, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Sandbox decision variant discriminator. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SandboxDecisionDataBypassDecidedKind { @@ -9690,6 +10243,7 @@ pub enum SandboxDecisionData { SpawnCompleted(SandboxDecisionDataSpawnCompleted), EnforcementState(SandboxDecisionDataEnforcementState), AccessDenied(SandboxDecisionDataAccessDenied), + AccessRecorded(SandboxDecisionDataAccessRecorded), BypassDecided(SandboxDecisionDataBypassDecided), PermissiveRetryDecided(SandboxDecisionDataPermissiveRetryDecided), PermissiveRetryCompleted(SandboxDecisionDataPermissiveRetryCompleted), @@ -9784,6 +10338,21 @@ pub enum PermissionRequestShellKind { Shell, } +/// Access a sandbox path grant confers +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionSandboxPathGrantAccess { + /// Read access: the path is added to readonlyPaths. + #[serde(rename = "read")] + Read, + /// Read and write access: the path is added to readwritePaths. + #[serde(rename = "readWrite")] + ReadWrite, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Permission kind discriminator #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PermissionRequestWriteKind { @@ -9928,6 +10497,9 @@ pub enum PermissionApprovalEvaluationReasonCode { /// The script path was not authorized for inspection. #[serde(rename = "path-not-authorized")] PathNotAuthorized, + /// A code source was excluded from review by content exclusion policy. + #[serde(rename = "content-excluded")] + ContentExcluded, /// The script working directory was invalid. #[serde(rename = "invalid-working-directory")] InvalidWorkingDirectory, @@ -9964,6 +10536,24 @@ pub enum PermissionApprovalEvaluationReasonCode { /// The script argument binding could not be reviewed. #[serde(rename = "argument-binding-unreviewable")] ArgumentBindingUnreviewable, + /// The shell command could not be analyzed for execution evidence. + #[serde(rename = "unsupported-command-shape")] + UnsupportedCommandShape, + /// The shell command used a code source that cannot be bound for review. + #[serde(rename = "unsupported-source")] + UnsupportedSource, + /// The shell command used a code source computed at run time. + #[serde(rename = "dynamic-source")] + DynamicSource, + /// The shell command referenced more code sources than can be reviewed. + #[serde(rename = "too-many-sources")] + TooManySources, + /// A code-bearing executable could not be inspected. + #[serde(rename = "executable-unavailable")] + ExecutableUnavailable, + /// A code-bearing executable exceeded the binding size limit. + #[serde(rename = "executable-too-large")] + ExecutableTooLarge, /// The script review metadata was malformed. #[serde(rename = "malformed-script-action-review")] MalformedScriptActionReview, @@ -10111,19 +10701,19 @@ pub enum PermissionRequestExtensionManagementKind { /// Permission kind discriminator #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionRequestFactoryKind { - #[serde(rename = "factory")] +pub enum PermissionRequestWorkflowKind { + #[serde(rename = "workflow")] #[default] - Factory, + Workflow, } -/// Operation gated by a factory permission request. +/// Operation gated by a workflow permission request. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FactoryPermissionOperation { - /// Running a registered factory, which spends subagents, active time, and AI credits under the approved limits. +pub enum WorkflowPermissionOperation { + /// Running a registered workflow, which spends subagents, active time, and AI credits under the approved limits. #[serde(rename = "run")] Run, - /// Authoring a factory, which writes JavaScript into a session-scoped extension and loads it. + /// Authoring a workflow, which writes JavaScript into a session-scoped extension and loads it. #[serde(rename = "author")] Author, /// Unknown variant for forward compatibility. @@ -10161,7 +10751,7 @@ pub enum PermissionRequest { CustomTool(PermissionRequestCustomTool), Hook(PermissionRequestHook), ExtensionManagement(PermissionRequestExtensionManagement), - Factory(PermissionRequestFactory), + Workflow(PermissionRequestWorkflow), ExtensionPermissionAccess(PermissionRequestExtensionPermissionAccess), ExtensionEnvAccess(PermissionRequestExtensionEnvAccess), } @@ -10266,10 +10856,10 @@ pub enum PermissionPromptRequestExtensionManagementKind { /// Prompt kind discriminator #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionPromptRequestFactoryKind { - #[serde(rename = "factory")] +pub enum PermissionPromptRequestWorkflowKind { + #[serde(rename = "workflow")] #[default] - Factory, + Workflow, } /// Prompt kind discriminator @@ -10302,7 +10892,7 @@ pub enum PermissionPromptRequest { Path(PermissionPromptRequestPath), Hook(PermissionPromptRequestHook), ExtensionManagement(PermissionPromptRequestExtensionManagement), - Factory(PermissionPromptRequestFactory), + Workflow(PermissionPromptRequestWorkflow), ExtensionPermissionAccess(PermissionPromptRequestExtensionPermissionAccess), ExtensionEnvAccess(PermissionPromptRequestExtensionEnvAccess), } @@ -10322,7 +10912,7 @@ pub enum PermissionDecisionSource { /// The host denied the request because no interactive user response was available. #[serde(rename = "unattended_fallback")] UnattendedFallback, - /// A live authorization record from an earlier human decision in this session contained the proposal, so it ran without another prompt. This is not a new human decision and never mints authority of its own. + /// Historical compatibility value for sessions created while authorization carry-forward was executable. Current runtimes do not produce this source. #[serde(rename = "authorization_carry_forward")] AuthorizationCarryForward, /// Unknown variant for forward compatibility. @@ -10339,6 +10929,14 @@ pub enum PermissionApprovedKind { Approved, } +/// Approved with read-only directory authority for this session +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionApprovedReadOnlyForSessionKind { + #[serde(rename = "approved-read-only-for-session")] + #[default] + ApprovedReadOnlyForSession, +} + /// Command approval kind #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UserToolSessionApprovalCommandsKind { @@ -10395,12 +10993,12 @@ pub enum UserToolSessionApprovalExtensionManagementKind { ExtensionManagement, } -/// Factory approval kind +/// Workflow approval kind #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum UserToolSessionApprovalFactoryKind { - #[serde(rename = "factory")] +pub enum UserToolSessionApprovalWorkflowKind { + #[serde(rename = "workflow")] #[default] - Factory, + Workflow, } /// Extension permission access approval kind @@ -10430,7 +11028,7 @@ pub enum UserToolSessionApproval { Memory(UserToolSessionApprovalMemory), CustomTool(UserToolSessionApprovalCustomTool), ExtensionManagement(UserToolSessionApprovalExtensionManagement), - Factory(UserToolSessionApprovalFactory), + Workflow(UserToolSessionApprovalWorkflow), ExtensionPermissionAccess(UserToolSessionApprovalExtensionPermissionAccess), ExtensionEnvAccess(UserToolSessionApprovalExtensionEnvAccess), } @@ -10504,6 +11102,7 @@ pub enum PermissionDeniedByPermissionRequestHookKind { #[serde(untagged)] pub enum PermissionResult { Approved(PermissionApproved), + ApprovedReadOnlyForSession(PermissionApprovedReadOnlyForSession), ApprovedForSession(PermissionApprovedForSession), ApprovedForLocation(PermissionApprovedForLocation), Cancelled(PermissionCancelled), @@ -10516,7 +11115,7 @@ pub enum PermissionResult { DeniedByPermissionRequestHook(PermissionDeniedByPermissionRequestHook), } -/// Which direction a message-backed authorization claim moves authority in. +/// Direction stored in a historical extractor claim. Current runtimes do not apply it. /// ///
    /// @@ -10526,10 +11125,10 @@ pub enum PermissionResult { ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PermissionMessageAuthorizationPolarity { - /// The human's words authorized an effect. + /// Historical claim recorded as a grant. #[serde(rename = "grant")] Grant, - /// The human's words refused an effect. + /// Historical claim recorded as a denial. #[serde(rename = "denial")] Denial, /// Unknown variant for forward compatibility. @@ -10837,10 +11436,61 @@ pub enum ExitPlanModeAction { Unknown, } -/// Terminal status a factory run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. +/// 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 FactoryRunSettledStatus { - /// The factory body resolved and its result was committed. +pub enum WorkflowRunSettledStatus { + /// The workflow body resolved and its result was committed. #[serde(rename = "completed")] Completed, /// The run was stopped by a limit, an approval refusal or another policy decision. @@ -10927,7 +11577,7 @@ pub enum McpServerStatus { /// The server is configured but disabled. #[serde(rename = "disabled")] Disabled, - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. #[serde(rename = "stopped")] Stopped, /// The server is not configured for this session. diff --git a/rust/src/github_token.rs b/rust/src/github_token.rs index e5cd201b83..db77cce96c 100644 --- a/rust/src/github_token.rs +++ b/rust/src/github_token.rs @@ -418,33 +418,4 @@ async fn send_error(client: &Weak, request_id: u64, code: i32, mess } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn token_debug_is_redacted() { - let token = GitHubToken::new("do-not-print", 28_800); - assert!(!format!("{token:?}").contains("do-not-print")); - } - - #[test] - fn retiring_session_removes_its_provider() { - let registry = GitHubTokenRegistry::new(); - let provider = Arc::new(|_args: GitHubTokenProviderArgs| async { - Ok(GitHubTokenProviderResult::Cancelled) - }); - let registration_id = registry.register(provider); - let session_id = crate::SessionId::from("session-1"); - registry.claim(®istration_id, session_id.clone()); - - registry.retire_session(&session_id); - - assert!( - !registry - .state - .lock() - .providers - .contains_key(®istration_id) - ); - } -} +mod tests; diff --git a/rust/src/github_token/tests.rs b/rust/src/github_token/tests.rs new file mode 100644 index 0000000000..d1376f5360 --- /dev/null +++ b/rust/src/github_token/tests.rs @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn token_debug_is_redacted() { + let token = GitHubToken::new("do-not-print", 28_800); + assert!(!format!("{token:?}").contains("do-not-print")); +} + +#[test] +fn retiring_session_removes_its_provider() { + let registry = GitHubTokenRegistry::new(); + let provider = Arc::new(|_args: GitHubTokenProviderArgs| async { + Ok(GitHubTokenProviderResult::Cancelled) + }); + let registration_id = registry.register(provider); + let session_id = crate::SessionId::from("session-1"); + registry.claim(®istration_id, session_id.clone()); + + registry.retire_session(&session_id); + + assert!( + !registry + .state + .lock() + .providers + .contains_key(®istration_id) + ); +} diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 61d9b192cb..a21dee9f87 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -381,97 +381,4 @@ impl PermissionHandler for DenyAllHandler { } #[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn approve_all_handler_returns_approved() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData::default(), - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_handler_fails_when_managed_settings_enabled() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData { - managed_settings_enabled: true, - ..Default::default() - }, - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::UserNotAvailable(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_handler_leaves_managed_approval_pending() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData { - managed_approval_required: Some(true), - ..Default::default() - }, - ) - .await; - assert!(matches!(result, PermissionResult::NoResult)); - } - - #[tokio::test] - async fn deny_all_handler_returns_denied() { - let result = DenyAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData::default(), - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[test] - fn mcp_auth_result_token_converts_to_wire_response() { - let wire = McpAuthResult::Token { - access_token: "host-token".to_string(), - token_type: Some("Bearer".to_string()), - expires_in: Some(3600), - } - .into_wire(); - - match wire { - McpOauthPendingRequestResponse::Token(token) => { - assert_eq!(token.access_token, "host-token"); - assert_eq!(token.token_type.as_deref(), Some("Bearer")); - assert_eq!(token.expires_in, Some(3600)); - } - McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"), - } - } -} +mod tests; diff --git a/rust/src/handler/tests.rs b/rust/src/handler/tests.rs new file mode 100644 index 0000000000..1d1df233bc --- /dev/null +++ b/rust/src/handler/tests.rs @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[tokio::test] +async fn approve_all_handler_returns_approved() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_handler_fails_when_managed_settings_enabled() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData { + managed_settings_enabled: true, + ..Default::default() + }, + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::UserNotAvailable(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_handler_leaves_managed_approval_pending() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData { + managed_approval_required: Some(true), + ..Default::default() + }, + ) + .await; + assert!(matches!(result, PermissionResult::NoResult)); +} + +#[tokio::test] +async fn deny_all_handler_returns_denied() { + let result = DenyAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[test] +fn mcp_auth_result_token_converts_to_wire_response() { + let wire = McpAuthResult::Token { + access_token: "host-token".to_string(), + token_type: Some("Bearer".to_string()), + expires_in: Some(3600), + } + .into_wire(); + + match wire { + McpOauthPendingRequestResponse::Token(token) => { + assert_eq!(token.access_token, "host-token"); + assert_eq!(token.token_type.as_deref(), Some("Bearer")); + assert_eq!(token.expires_in, Some(3600)); + } + McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"), + } +} diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs index 4986d6cb18..3b2af37cb1 100644 --- a/rust/src/hooks.rs +++ b/rust/src/hooks.rs @@ -362,6 +362,86 @@ pub struct AgentStopOutput { pub reason: Option, } +/// Input for `subagentStart`, received before a subagent's first turn. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStartInput { + /// The parent session ID. + pub session_id: String, + /// Unix timestamp in ms. + pub timestamp: f64, + /// Working directory of the parent session. + #[serde(rename = "cwd")] + pub working_directory: PathBuf, + /// Path to the parent session transcript. + pub transcript_path: PathBuf, + /// Name of the subagent definition. + pub agent_name: String, + /// Display name, when the definition provides one. + #[serde(default)] + pub agent_display_name: Option, + /// Description, when the definition provides one. + #[serde(default)] + pub agent_description: Option, +} + +/// Output for `subagentStart`. +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStartOutput { + /// Context prepended to the subagent's first prompt. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_context: Option, +} + +/// Input for `subagentStop`, received after a subagent's turn. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStopInput { + /// The parent session ID. + pub session_id: String, + /// Unix timestamp in ms. + pub timestamp: f64, + /// Working directory of the parent session. + #[serde(rename = "cwd")] + pub working_directory: PathBuf, + /// Path to the parent session transcript. + pub transcript_path: PathBuf, + /// Name of the subagent definition. + pub agent_name: String, + /// Type of the subagent. + pub agent_type: String, + /// Agent ID, when available. + #[serde(default)] + pub agent_id: Option, + /// Display name, when the definition provides one. + #[serde(default)] + pub agent_display_name: Option, + /// Description, when the definition provides one. + #[serde(default)] + pub agent_description: Option, + /// Reason the subagent stopped (normally `"end_turn"`). + pub stop_reason: String, + /// The subagent's last assistant response. + pub response: String, +} + +/// Output for `subagentStop`. +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStopOutput { + /// Set to `"block"` to run another subagent turn; only `"allow"` and `"block"` are valid. + #[serde(skip_serializing_if = "Option::is_none")] + pub decision: Option, + /// Nonempty follow-up instruction required when blocking; without a block + /// decision this is an error, not an instruction to the child. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Replacement final response when the stop is allowed. + #[serde(skip_serializing_if = "Option::is_none")] + pub modified_response: Option, +} + /// Events dispatched to [`SessionHooks::on_hook`] at CLI lifecycle points. /// /// Each variant carries the typed input for that hook plus the shared @@ -442,6 +522,20 @@ pub enum HookEvent { /// Session context. ctx: HookContext, }, + /// Fired before a subagent's first turn. + SubagentStart { + /// Typed input data. + input: SubagentStartInput, + /// Session context. + ctx: HookContext, + }, + /// Fired after a subagent's turn. + SubagentStop { + /// Typed input data. + input: SubagentStopInput, + /// Session context. + ctx: HookContext, + }, } /// Response from [`SessionHooks::on_hook`] back to the SDK. @@ -473,6 +567,10 @@ pub enum HookOutput { ErrorOccurred(ErrorOccurredOutput), /// Response for an agent-stop hook. AgentStop(AgentStopOutput), + /// Response for a subagent-start hook. + SubagentStart(SubagentStartOutput), + /// Response for a subagent-stop hook. + SubagentStop(SubagentStopOutput), } impl HookOutput { @@ -489,6 +587,8 @@ impl HookOutput { Self::SessionEnd(_) => "SessionEnd", Self::ErrorOccurred(_) => "ErrorOccurred", Self::AgentStop(_) => "AgentStop", + Self::SubagentStart(_) => "SubagentStart", + Self::SubagentStop(_) => "SubagentStop", } } } @@ -567,6 +667,16 @@ pub trait SessionHooks: Send + Sync + 'static { .await .map(HookOutput::AgentStop) .unwrap_or(HookOutput::None), + HookEvent::SubagentStart { input, ctx } => self + .on_subagent_start(input, ctx) + .await + .map(HookOutput::SubagentStart) + .unwrap_or(HookOutput::None), + HookEvent::SubagentStop { input, ctx } => self + .on_subagent_stop(input, ctx) + .await + .map(HookOutput::SubagentStop) + .unwrap_or(HookOutput::None), } } @@ -673,6 +783,25 @@ pub trait SessionHooks: Send + Sync + 'static { ) -> Option { None } + + /// Called before a subagent runs. Return context to prepend to its prompt. + async fn on_subagent_start( + &self, + _input: SubagentStartInput, + _ctx: HookContext, + ) -> Option { + None + } + + /// Called when a subagent stops. Return a block decision with a reason + /// to continue it, or a replacement for its final response. + async fn on_subagent_stop( + &self, + _input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + None + } } /// Dispatches a `hooks.invoke` request to [`SessionHooks::on_hook`]. @@ -731,6 +860,14 @@ pub(crate) async fn dispatch_hook( let input: AgentStopInput = serde_json::from_value(raw_input)?; HookEvent::AgentStop { input, ctx } } + "subagentStart" => { + let input: SubagentStartInput = serde_json::from_value(raw_input)?; + HookEvent::SubagentStart { input, ctx } + } + "subagentStop" => { + let input: SubagentStopInput = serde_json::from_value(raw_input)?; + HookEvent::SubagentStop { input, ctx } + } _ => { tracing::warn!( hook_type = hook_type, @@ -770,6 +907,8 @@ pub(crate) async fn dispatch_hook( ("sessionEnd", HookOutput::SessionEnd(o)) => Some(serde_json::to_value(o)?), ("errorOccurred", HookOutput::ErrorOccurred(o)) => Some(serde_json::to_value(o)?), ("agentStop", HookOutput::AgentStop(o)) => Some(serde_json::to_value(o)?), + ("subagentStart", HookOutput::SubagentStart(o)) => Some(serde_json::to_value(o)?), + ("subagentStop", HookOutput::SubagentStop(o)) => Some(serde_json::to_value(o)?), _ => { tracing::warn!( hook_type = hook_type, @@ -785,400 +924,4 @@ pub(crate) async fn dispatch_hook( } #[cfg(test)] -mod tests { - use super::*; - - struct TestHooks; - - #[async_trait] - impl SessionHooks for TestHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::PreToolUse { input, .. } => { - if input.tool_name == "dangerous_tool" { - HookOutput::PreToolUse(PreToolUseOutput { - permission_decision: Some("deny".to_string()), - permission_decision_reason: Some("blocked by policy".to_string()), - ..Default::default() - }) - } else { - HookOutput::None - } - } - HookEvent::UserPromptSubmitted { input, .. } => { - HookOutput::UserPromptSubmitted(UserPromptSubmittedOutput { - modified_prompt: Some(format!("[prefixed] {}", input.prompt)), - ..Default::default() - }) - } - HookEvent::UserPromptTransformed { input, .. } => { - HookOutput::UserPromptTransformed(UserPromptTransformedOutput { - modified_transformed_prompt: Some(format!( - "[transformed] {}", - input.transformed_prompt - )), - }) - } - _ => HookOutput::None, - } - } - } - - #[tokio::test] - async fn dispatch_pre_tool_use_deny() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "dangerous_tool", - "toolArgs": {} - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - let output = &result["output"]; - assert_eq!(output["permissionDecision"], "deny"); - assert_eq!(output["permissionDecisionReason"], "blocked by policy"); - } - - #[tokio::test] - async fn dispatch_pre_tool_use_passthrough() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "safe_tool", - "toolArgs": {"key": "value"} - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - // No hook registered for this tool — output should be empty object - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_user_prompt_submitted() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "prompt": "hello world" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "userPromptSubmitted", - input, - ) - .await - .unwrap(); - assert_eq!(result["output"]["modifiedPrompt"], "[prefixed] hello world"); - } - - #[tokio::test] - async fn dispatch_user_prompt_transformed() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "prompt": "hello world", - "transformedPrompt": "now\nhello world" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "userPromptTransformed", - input, - ) - .await - .unwrap(); - assert_eq!( - result["output"]["modifiedTransformedPrompt"], - "[transformed] now\nhello world" - ); - } - - #[tokio::test] - async fn dispatch_unregistered_hook_returns_empty() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "reason": "complete" - }); - // TestHooks doesn't handle SessionEnd - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionEnd", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_unknown_hook_type() { - let hooks = TestHooks; - let input = serde_json::json!({}); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "unknownHook", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_mismatched_output_returns_empty() { - struct MismatchHooks; - #[async_trait] - impl SessionHooks for MismatchHooks { - async fn on_hook(&self, _event: HookEvent) -> HookOutput { - // Always return SessionEnd output regardless of event type - HookOutput::SessionEnd(SessionEndOutput { - session_summary: Some("oops".to_string()), - ..Default::default() - }) - } - } - - let hooks = MismatchHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {} - }); - // preToolUse event gets a SessionEnd output — should be treated as empty - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_default() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {}, - "toolResult": "success" - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "postToolUse", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_default() { - // No handler override — should return an empty output object. - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {"key": "value"}, - "error": "boom" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_returns_additional_context() { - struct FailureHooks; - #[async_trait] - impl SessionHooks for FailureHooks { - async fn on_post_tool_use_failure( - &self, - input: PostToolUseFailureInput, - _ctx: HookContext, - ) -> Option { - assert_eq!(input.session_id, "sess-1"); - assert_eq!(input.tool_name, "some_tool"); - assert_eq!(input.error, "boom"); - assert_eq!(input.working_directory, PathBuf::from("/tmp")); - Some(PostToolUseFailureOutput { - additional_context: Some(format!( - "tool {} failed: {}", - input.tool_name, input.error - )), - }) - } - } - - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {}, - "error": "boom" - }); - let result = dispatch_hook( - &FailureHooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap(); - assert_eq!( - result["output"]["additionalContext"], - "tool some_tool failed: boom" - ); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_invalid_input_errors() { - // Missing required `error` field — dispatcher should surface the - // deserialization error rather than dispatching with empty input. - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {} - }); - let err = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap_err(); - let msg = err.to_string().to_ascii_lowercase(); - assert!( - msg.contains("error") || msg.contains("missing field"), - "unexpected error: {msg}" - ); - } - - #[tokio::test] - async fn dispatch_session_start() { - struct StartHooks; - #[async_trait] - impl SessionHooks for StartHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::SessionStart { .. } => { - HookOutput::SessionStart(SessionStartOutput { - additional_context: Some("extra context".to_string()), - ..Default::default() - }) - } - _ => HookOutput::None, - } - } - } - - let hooks = StartHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "source": "new" - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionStart", input) - .await - .unwrap(); - assert_eq!(result["output"]["additionalContext"], "extra context"); - } - - #[tokio::test] - async fn dispatch_error_occurred() { - struct ErrorHooks; - #[async_trait] - impl SessionHooks for ErrorHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::ErrorOccurred { .. } => { - HookOutput::ErrorOccurred(ErrorOccurredOutput { - error_handling: Some("retry".to_string()), - retry_count: Some(3), - ..Default::default() - }) - } - _ => HookOutput::None, - } - } - } - - let hooks = ErrorHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "error": "model timeout", - "errorContext": "model_call", - "recoverable": true - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "errorOccurred", input) - .await - .unwrap(); - assert_eq!(result["output"]["errorHandling"], "retry"); - assert_eq!(result["output"]["retryCount"], 3); - } - - #[tokio::test] - async fn dispatch_agent_stop_block() { - struct AgentStopHooks; - #[async_trait] - impl SessionHooks for AgentStopHooks { - async fn on_agent_stop( - &self, - input: AgentStopInput, - ctx: HookContext, - ) -> Option { - assert_eq!(ctx.session_id, SessionId::new("sess-1")); - assert_eq!(input.session_id, "sess-1"); - assert_eq!(input.stop_reason.as_deref(), Some("end_turn")); - assert_eq!( - input.transcript_path, - Some(PathBuf::from("/tmp/transcript.jsonl")) - ); - assert_eq!(input.stop_hook_active, Some(true)); - Some(AgentStopOutput { - decision: Some("block".to_string()), - reason: Some("finish the remaining work".to_string()), - }) - } - } - - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "stopReason": "end_turn", - "transcriptPath": "/tmp/transcript.jsonl", - "stop_hook_active": true - }); - let result = dispatch_hook( - &AgentStopHooks, - &SessionId::new("sess-1"), - "agentStop", - input, - ) - .await - .unwrap(); - - assert_eq!(result["output"]["decision"], "block"); - assert_eq!(result["output"]["reason"], "finish the remaining work"); - } -} +mod tests; diff --git a/rust/src/hooks/tests.rs b/rust/src/hooks/tests.rs new file mode 100644 index 0000000000..e8a8179c41 --- /dev/null +++ b/rust/src/hooks/tests.rs @@ -0,0 +1,530 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +struct TestHooks; + +#[async_trait] +impl SessionHooks for TestHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::PreToolUse { input, .. } => { + if input.tool_name == "dangerous_tool" { + HookOutput::PreToolUse(PreToolUseOutput { + permission_decision: Some("deny".to_string()), + permission_decision_reason: Some("blocked by policy".to_string()), + ..Default::default() + }) + } else { + HookOutput::None + } + } + HookEvent::UserPromptSubmitted { input, .. } => { + HookOutput::UserPromptSubmitted(UserPromptSubmittedOutput { + modified_prompt: Some(format!("[prefixed] {}", input.prompt)), + ..Default::default() + }) + } + HookEvent::UserPromptTransformed { input, .. } => { + HookOutput::UserPromptTransformed(UserPromptTransformedOutput { + modified_transformed_prompt: Some(format!( + "[transformed] {}", + input.transformed_prompt + )), + }) + } + _ => HookOutput::None, + } + } +} + +#[tokio::test] +async fn dispatch_pre_tool_use_deny() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "dangerous_tool", + "toolArgs": {} + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + let output = &result["output"]; + assert_eq!(output["permissionDecision"], "deny"); + assert_eq!(output["permissionDecisionReason"], "blocked by policy"); +} + +#[tokio::test] +async fn dispatch_pre_tool_use_passthrough() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "safe_tool", + "toolArgs": {"key": "value"} + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + // No hook registered for this tool — output should be empty object + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_user_prompt_submitted() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "prompt": "hello world" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "userPromptSubmitted", + input, + ) + .await + .unwrap(); + assert_eq!(result["output"]["modifiedPrompt"], "[prefixed] hello world"); +} + +#[tokio::test] +async fn dispatch_user_prompt_transformed() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "prompt": "hello world", + "transformedPrompt": "now\nhello world" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "userPromptTransformed", + input, + ) + .await + .unwrap(); + assert_eq!( + result["output"]["modifiedTransformedPrompt"], + "[transformed] now\nhello world" + ); +} + +#[tokio::test] +async fn dispatch_unregistered_hook_returns_empty() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "reason": "complete" + }); + // TestHooks doesn't handle SessionEnd + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionEnd", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_unknown_hook_type() { + let hooks = TestHooks; + let input = serde_json::json!({}); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "unknownHook", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_subagent_hooks_with_typed_input_and_output() { + struct SubagentHooks; + #[async_trait] + impl SessionHooks for SubagentHooks { + async fn on_subagent_start( + &self, + input: SubagentStartInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.timestamp, 1234567890.0); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + assert_eq!( + input.transcript_path, + PathBuf::from("/tmp/transcript.jsonl") + ); + assert_eq!(input.agent_name, "explore"); + assert_eq!(input.agent_display_name.as_deref(), Some("Explore Agent")); + assert_eq!(input.agent_description.as_deref(), Some("Read code")); + Some(SubagentStartOutput { + additional_context: Some("Follow the file".to_string()), + }) + } + + async fn on_subagent_stop( + &self, + input: SubagentStopInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.timestamp, 1234567891.0); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + assert_eq!( + input.transcript_path, + PathBuf::from("/tmp/transcript.jsonl") + ); + assert_eq!(input.agent_name, "explore"); + assert_eq!(input.agent_type, "explore"); + assert_eq!(input.agent_id.as_deref(), Some("read-file")); + assert_eq!(input.agent_display_name.as_deref(), Some("Explore Agent")); + assert_eq!(input.agent_description.as_deref(), Some("Read code")); + assert_eq!(input.stop_reason, "end_turn"); + assert_eq!(input.response, "original answer"); + Some(SubagentStopOutput { + modified_response: Some("rewritten answer".to_string()), + ..Default::default() + }) + } + } + + let common = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "explore", + "agentDisplayName": "Explore Agent", + "agentDescription": "Read code" + }); + let start = dispatch_hook( + &SubagentHooks, + &SessionId::new("sess-1"), + "subagentStart", + common.clone(), + ) + .await + .unwrap(); + assert_eq!( + start, + serde_json::json!({ "output": { "additionalContext": "Follow the file" } }) + ); + + let mut stop = common; + let object = stop.as_object_mut().unwrap(); + object.insert("timestamp".to_string(), serde_json::json!(1234567891)); + object.insert("agentType".to_string(), serde_json::json!("explore")); + object.insert("agentId".to_string(), serde_json::json!("read-file")); + object.insert("stopReason".to_string(), serde_json::json!("end_turn")); + object.insert("response".to_string(), serde_json::json!("original answer")); + let stopped = dispatch_hook( + &SubagentHooks, + &SessionId::new("sess-1"), + "subagentStop", + stop, + ) + .await + .unwrap(); + assert_eq!( + stopped, + serde_json::json!({ "output": { "modifiedResponse": "rewritten answer" } }) + ); +} + +#[tokio::test] +async fn dispatch_subagent_stop_block_output() { + struct BlockHook; + #[async_trait] + impl SessionHooks for BlockHook { + async fn on_subagent_stop( + &self, + _input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + Some(SubagentStopOutput { + decision: Some("block".to_string()), + reason: Some("Keep researching".to_string()), + ..Default::default() + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "explore", + "agentType": "explore", + "stopReason": "end_turn", + "response": "draft answer" + }); + let result = dispatch_hook(&BlockHook, &SessionId::new("sess-1"), "subagentStop", input) + .await + .unwrap(); + assert_eq!( + result, + serde_json::json!({ "output": { "decision": "block", "reason": "Keep researching" } }) + ); +} + +#[tokio::test] +async fn dispatch_mismatched_output_returns_empty() { + struct MismatchHooks; + #[async_trait] + impl SessionHooks for MismatchHooks { + async fn on_hook(&self, _event: HookEvent) -> HookOutput { + // Always return SessionEnd output regardless of event type + HookOutput::SessionEnd(SessionEndOutput { + session_summary: Some("oops".to_string()), + ..Default::default() + }) + } + } + + let hooks = MismatchHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {} + }); + // preToolUse event gets a SessionEnd output — should be treated as empty + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_default() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {}, + "toolResult": "success" + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "postToolUse", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_default() { + // No handler override — should return an empty output object. + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {"key": "value"}, + "error": "boom" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_returns_additional_context() { + struct FailureHooks; + #[async_trait] + impl SessionHooks for FailureHooks { + async fn on_post_tool_use_failure( + &self, + input: PostToolUseFailureInput, + _ctx: HookContext, + ) -> Option { + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.tool_name, "some_tool"); + assert_eq!(input.error, "boom"); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + Some(PostToolUseFailureOutput { + additional_context: Some(format!( + "tool {} failed: {}", + input.tool_name, input.error + )), + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {}, + "error": "boom" + }); + let result = dispatch_hook( + &FailureHooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap(); + assert_eq!( + result["output"]["additionalContext"], + "tool some_tool failed: boom" + ); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_invalid_input_errors() { + // Missing required `error` field — dispatcher should surface the + // deserialization error rather than dispatching with empty input. + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {} + }); + let err = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap_err(); + let msg = err.to_string().to_ascii_lowercase(); + assert!( + msg.contains("error") || msg.contains("missing field"), + "unexpected error: {msg}" + ); +} + +#[tokio::test] +async fn dispatch_session_start() { + struct StartHooks; + #[async_trait] + impl SessionHooks for StartHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::SessionStart { .. } => HookOutput::SessionStart(SessionStartOutput { + additional_context: Some("extra context".to_string()), + ..Default::default() + }), + _ => HookOutput::None, + } + } + } + + let hooks = StartHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "source": "new" + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionStart", input) + .await + .unwrap(); + assert_eq!(result["output"]["additionalContext"], "extra context"); +} + +#[tokio::test] +async fn dispatch_error_occurred() { + struct ErrorHooks; + #[async_trait] + impl SessionHooks for ErrorHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::ErrorOccurred { .. } => HookOutput::ErrorOccurred(ErrorOccurredOutput { + error_handling: Some("retry".to_string()), + retry_count: Some(3), + ..Default::default() + }), + _ => HookOutput::None, + } + } + } + + let hooks = ErrorHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "error": "model timeout", + "errorContext": "model_call", + "recoverable": true + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "errorOccurred", input) + .await + .unwrap(); + assert_eq!(result["output"]["errorHandling"], "retry"); + assert_eq!(result["output"]["retryCount"], 3); +} + +#[tokio::test] +async fn dispatch_agent_stop_block() { + struct AgentStopHooks; + #[async_trait] + impl SessionHooks for AgentStopHooks { + async fn on_agent_stop( + &self, + input: AgentStopInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.stop_reason.as_deref(), Some("end_turn")); + assert_eq!( + input.transcript_path, + Some(PathBuf::from("/tmp/transcript.jsonl")) + ); + assert_eq!(input.stop_hook_active, Some(true)); + Some(AgentStopOutput { + decision: Some("block".to_string()), + reason: Some("finish the remaining work".to_string()), + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "stopReason": "end_turn", + "transcriptPath": "/tmp/transcript.jsonl", + "stop_hook_active": true + }); + let result = dispatch_hook( + &AgentStopHooks, + &SessionId::new("sess-1"), + "agentStop", + input, + ) + .await + .unwrap(); + + assert_eq!(result["output"]["decision"], "block"); + assert_eq!(result["output"]["reason"], "finish the remaining work"); +} diff --git a/rust/src/installation_confirmation.rs b/rust/src/installation_confirmation.rs new file mode 100644 index 0000000000..ccb9b46b01 --- /dev/null +++ b/rust/src/installation_confirmation.rs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +//! Connection-global human confirmation for experimental installation operations. + +use std::panic::AssertUnwindSafe; +use std::sync::{Arc, OnceLock, Weak}; + +use async_trait::async_trait; +use futures_util::FutureExt; +use parking_lot::RwLock; +use tokio_util::sync::CancellationToken; +use tracing::warn; + +pub use crate::rpc::{ + InstallationConfirmationRequest, InstallationConfirmationResponse, InstallationDecision, + InstallationReview, McpInstallationReview, +}; +use crate::{ + Client, ClientInner, JsonRpcError, JsonRpcRequest, JsonRpcResponse, Result, error_codes, +}; + +pub(crate) const CONFIRM_METHOD: &str = "installations.confirm"; + +/// The cancellation lifetime of one confirmation. +/// +/// This signal retires a human review when the runtime cancels the request or +/// the original connection closes. It does not cancel outbound installation or +/// OAuth RPCs. The runtime remains authoritative for the operation's expiry. +#[derive(Clone)] +pub struct InstallationConfirmationContext { + cancellation: CancellationToken, +} + +impl InstallationConfirmationContext { + /// Cancelled when the runtime retires this request or the original connection closes. + /// + /// Cancelling the returned child token cannot cancel another review. + pub fn cancellation(&self) -> CancellationToken { + self.cancellation.child_token() + } +} + +/// Collects a fresh human decision for the complete review on its original connection. +/// +/// Match the request's `operation_id` and `policy_session_id` against the exact +/// action registered by the host before displaying it. Never infer the originating +/// action from a current session, server name or global pending slot. Refuse +/// unknown actions or incomplete reviews. +/// +/// The SDK echoes the original challenge and fingerprint; the handler returns +/// only the decision. Cancellation drops the handler future, so any separately +/// spawned UI work must also observe the supplied context. +/// +/// This experimental callback does not imply that the connected runtime supports +/// installation, removal or activation. +#[async_trait] +pub trait InstallationConfirmationHandler: Send + Sync + 'static { + /// Review this operation and return an explicit decision, or an error to refuse it. + async fn confirm( + &self, + request: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ) -> Result; +} + +pub(crate) struct InstallationConfirmationDispatcher { + handler: RwLock>>, + client: OnceLock>, +} + +impl InstallationConfirmationDispatcher { + pub(crate) fn new() -> Self { + Self { + handler: RwLock::new(None), + client: OnceLock::new(), + } + } + + pub(crate) fn set_client(&self, client: Weak) { + 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; + } + + pub(crate) fn clear(&self) { + self.handler.write().take(); + } + + pub(crate) fn dispatch(self: &Arc, request: JsonRpcRequest) { + let Some(client) = self.client.get().and_then(Weak::upgrade) else { + return; + }; + let Some(pending) = client.rpc.cancellable_requests.claim(request.id) else { + warn!("confirmation request retired before dispatch"); + return; + }; + let request_cancelled = pending.cancellation().clone(); + let connection_closed = client.rpc.connection_closed_token(); + let context = InstallationConfirmationContext { + cancellation: connection_closed.child_token(), + }; + let handler = self.handler.read().clone(); + let dispatcher = self.clone(); + tokio::spawn(async move { + let outcome = tokio::select! { + biased; + _ = connection_closed.cancelled() => return, + _ = request_cancelled.cancelled() => { + context.cancellation.cancel(); + Err((error_codes::REQUEST_CANCELLED, "Installation confirmation request cancelled")) + } + outcome = Self::handle(handler, request.params, context.clone()) => outcome, + }; + if connection_closed.is_cancelled() { + return; + } + let outcome = if request_cancelled.is_cancelled() { + context.cancellation.cancel(); + Err(( + error_codes::REQUEST_CANCELLED, + "Installation confirmation request cancelled", + )) + } else { + outcome + }; + dispatcher.respond(request.id, outcome).await; + drop(pending); + }); + } + + async fn handle( + handler: Option>, + params: Option, + context: InstallationConfirmationContext, + ) -> std::result::Result { + let Some(handler) = handler else { + return Err(( + error_codes::METHOD_NOT_FOUND, + "No installations client-global handler registered", + )); + }; + let request: InstallationConfirmationRequest = + serde_json::from_value(params.unwrap_or(serde_json::Value::Null)).map_err(|_| { + ( + error_codes::INVALID_PARAMS, + "Invalid installation confirmation review", + ) + })?; + let confirmation_id = request.confirmation_id.clone(); + let review_fingerprint = request.review_fingerprint.clone(); + let outcome = AssertUnwindSafe(handler.confirm(request, context)) + .catch_unwind() + .await; + let decision = match outcome { + Ok(Ok( + decision @ (InstallationDecision::Confirm + | InstallationDecision::Decline + | InstallationDecision::Cancel), + )) => decision, + Ok(Ok(InstallationDecision::Unknown)) => { + return Err(( + error_codes::INTERNAL_ERROR, + "Invalid installation confirmation decision", + )); + } + Ok(Err(_)) => { + return Err(( + error_codes::INTERNAL_ERROR, + "Installation confirmation handler failed", + )); + } + Err(_) => { + return Err(( + error_codes::INTERNAL_ERROR, + "Installation confirmation handler panicked", + )); + } + }; + Ok(InstallationConfirmationResponse { + confirmation_id, + review_fingerprint, + decision, + }) + } + + async fn respond( + &self, + id: u64, + outcome: std::result::Result, + ) { + let Some(client) = self.client.get().and_then(Weak::upgrade) else { + return; + }; + let (result, error) = match outcome { + Ok(response) => match serde_json::to_value(response) { + Ok(value) => (Some(value), None), + Err(_) => { + warn!("failed to serialise installation confirmation response"); + ( + None, + Some(JsonRpcError { + code: error_codes::INTERNAL_ERROR, + message: "Installation confirmation serialisation failed".to_string(), + data: None, + }), + ) + } + }, + Err((code, message)) => ( + None, + Some(JsonRpcError { + code, + message: message.to_string(), + data: None, + }), + ), + }; + if Client::from_inner(client) + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result, + error, + }) + .await + .is_err() + { + warn!("failed to send installation confirmation response"); + } + } +} diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 1a590c555b..2f69da5ba9 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::hash_map::Entry; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Instant; @@ -26,6 +27,19 @@ use crate::{Error, ErrorKind, ProtocolErrorKind}; pub(crate) type InlineResponseCallback = Box Result<(), Error> + Send + Sync>; +pub(crate) type RequestHandler = Arc< + dyn Fn(Value) -> futures_util::future::BoxFuture<'static, Result> + Send + Sync, +>; +type RequestHandlers = Arc>>; + +fn remote_error_log_message<'a>(method: &str, message: &'a str) -> &'a str { + // Listener negotiation carries a token that a remote error may echo. + match method { + "host.getConfiguration" | "host.ready" => "listener negotiation request rejected", + _ => message, + } +} + /// Internal pairing of the response delivery channel with an optional /// inline callback that the read loop runs synchronously before delivery. struct PendingRequest { @@ -85,6 +99,8 @@ pub mod error_codes { /// Internal server error (-32603). #[allow(dead_code, reason = "standard JSON-RPC code, reserved for future use")] pub const INTERNAL_ERROR: i32 = -32603; + /// Request cancelled by the peer's `$/cancelRequest` (-32800). + pub const REQUEST_CANCELLED: i32 = -32800; } /// A JSON-RPC 2.0 notification (no `id`, no response expected). @@ -263,6 +279,85 @@ struct WriteCommand { ack: oneshot::Sender>, } +/// Inbound requests that honor `$/cancelRequest`. +/// +/// The read loop registers each one synchronously before forwarding it. This +/// preserves request/cancellation ordering across the router's separate queues. +#[derive(Default)] +pub(crate) struct CancellableRequests { + pending: Mutex>>, +} + +impl CancellableRequests { + fn honors_cancellation(method: &str) -> bool { + use crate::generated::api_types::rpc_methods; + + matches!( + method, + crate::installation_confirmation::CONFIRM_METHOD + | rpc_methods::SKILLPROVIDER_LIST + | rpc_methods::SKILLPROVIDER_READ + ) + } + + fn register(&self, id: u64) -> bool { + let mut pending = self.pending.lock(); + match pending.entry(id) { + Entry::Vacant(entry) => { + entry.insert(Arc::new(CancellationToken::new())); + true + } + Entry::Occupied(_) => false, + } + } + + fn cancel(&self, id: u64) { + if let Some(token) = self.pending.lock().get(&id) { + token.cancel(); + } + } + + fn clear(&self) { + self.pending.lock().clear(); + } + + /// Take ownership of a registered request's cancellation until the + /// returned guard drops, or `None` if the connection already retired it. + pub(crate) fn claim(self: &Arc, id: u64) -> Option { + let cancellation = self.pending.lock().get(&id)?.clone(); + Some(PendingCancellation { + requests: self.clone(), + id, + cancellation, + }) + } +} + +pub(crate) struct PendingCancellation { + requests: Arc, + id: u64, + cancellation: Arc, +} + +impl PendingCancellation { + /// Cancelled when the runtime sends `$/cancelRequest` for this request. + pub(crate) fn cancellation(&self) -> &CancellationToken { + &self.cancellation + } +} + +impl Drop for PendingCancellation { + fn drop(&mut self) { + let mut pending = self.requests.pending.lock(); + if pending + .get(&self.id) + .is_some_and(|token| Arc::ptr_eq(token, &self.cancellation)) + { + pending.remove(&self.id); + } + } +} + /// Low-level JSON-RPC 2.0 client over Content-Length-framed streams. /// /// # Cancel safety @@ -285,7 +380,9 @@ pub struct JsonRpcClient { pending_requests: Arc>>, notification_tx: broadcast::Sender, request_tx: mpsc::UnboundedSender, + request_handlers: RequestHandlers, connection_closed: CancellationToken, + pub(crate) cancellable_requests: Arc, read_task: Mutex>>, write_task: Mutex>>, } @@ -297,11 +394,22 @@ impl JsonRpcClient { /// messages to pending request channels, the notification broadcast, /// or the request-forwarding channel; and a writer actor that owns the /// underlying `AsyncWrite` and serializes frames atomically. + #[cfg(any(test, feature = "test-support"))] pub fn new( writer: impl AsyncWrite + Unpin + Send + 'static, reader: impl AsyncRead + Unpin + Send + 'static, notification_tx: broadcast::Sender, request_tx: mpsc::UnboundedSender, + ) -> Self { + Self::new_with_host_notifications(writer, reader, notification_tx, request_tx, None) + } + + pub(crate) fn new_with_host_notifications( + writer: impl AsyncWrite + Unpin + Send + 'static, + reader: impl AsyncRead + Unpin + Send + 'static, + notification_tx: broadcast::Sender, + request_tx: mpsc::UnboundedSender, + host_notifications: Option>, ) -> Self { let (write_tx, write_rx) = mpsc::unbounded_channel::(); @@ -314,7 +422,9 @@ impl JsonRpcClient { pending_requests: Arc::new(RwLock::new(HashMap::new())), notification_tx, request_tx, + request_handlers: Arc::new(RwLock::new(HashMap::new())), connection_closed: CancellationToken::new(), + cancellable_requests: Arc::new(CancellableRequests::default()), read_task: Mutex::new(None), write_task: Mutex::new(Some(write_task)), }; @@ -323,6 +433,9 @@ impl JsonRpcClient { let notification_tx_clone = client.notification_tx.clone(); let request_tx_clone = client.request_tx.clone(); let connection_closed = client.connection_closed.clone(); + let cancellable_requests = client.cancellable_requests.clone(); + let request_handlers = client.request_handlers.clone(); + let write_tx = client.write_tx.clone(); let reader_span = tracing::error_span!("jsonrpc_read_loop"); let read_task = tokio::spawn( @@ -330,11 +443,13 @@ impl JsonRpcClient { Self::read_loop( reader, pending_requests, - notification_tx_clone, + (notification_tx_clone, host_notifications), request_tx_clone, + request_handlers, + write_tx, + (connection_closed, cancellable_requests), ) .await; - connection_closed.cancel(); } .instrument(reader_span), ); @@ -345,19 +460,49 @@ impl JsonRpcClient { pub(crate) fn force_close(&self) { self.connection_closed.cancel(); + self.cancellable_requests.clear(); + let handlers = std::mem::take(&mut *self.request_handlers.write()); + drop(handlers); if let Some(task) = self.read_task.lock().take() { task.abort(); } + self.close_writer(); + self.pending_requests.write().clear(); + } + + /// Release stdin while continuing to drain the owned child's final stdout. + pub(crate) fn close_writer(&self) { if let Some(task) = self.write_task.lock().take() { task.abort(); } - self.pending_requests.write().clear(); } pub(crate) fn connection_closed_token(&self) -> CancellationToken { self.connection_closed.child_token() } + pub(crate) fn register_request_handler( + &self, + method: &str, + handler: RequestHandler, + ) -> Result<(), Error> { + let mut handlers = self.request_handlers.write(); + if method.is_empty() || self.connection_closed.is_cancelled() { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "Request handlers require a nonempty method and an open connection", + )); + } + if handlers.contains_key(method) { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + format!("A request handler is already registered for {method}"), + )); + } + handlers.insert(method.to_owned(), handler); + Ok(()) + } + /// Writer-actor task. Owns the `AsyncWrite`, drains the command queue, /// and writes each frame atomically (header + body + flush) before /// signaling the ack. @@ -392,10 +537,17 @@ impl JsonRpcClient { async fn read_loop( reader: impl AsyncRead + Unpin + Send, pending_requests: Arc>>, - notification_tx: broadcast::Sender, + notifications: ( + broadcast::Sender, + Option>, + ), request_tx: mpsc::UnboundedSender, + request_handlers: RequestHandlers, + write_tx: mpsc::UnboundedSender, + connection: (CancellationToken, Arc), ) { let mut reader = BufReader::new(reader); + let (connection_closed, cancellable_requests) = connection; loop { match Self::read_message(&mut reader).await { @@ -455,10 +607,70 @@ impl JsonRpcClient { } } JsonRpcMessage::Notification(notification) => { - let _ = notification_tx.send(notification); + if notification.method == "$/cancelRequest" { + if let Some(id) = notification + .params + .as_ref() + .and_then(|params| params.get("id")) + .and_then(Value::as_u64) + { + cancellable_requests.cancel(id); + } else { + warn!("invalid numeric request cancellation"); + } + } + if matches!( + notification.method.as_str(), + "host.exited" | "host.sessionReleased" + ) && let Some(hosts) = ¬ifications.1 + { + let _ = hosts.send(notification.clone()); + } + let _ = notifications.0.send(notification); } JsonRpcMessage::Request(request) => { - if request_tx.send(request).is_err() { + if CancellableRequests::honors_cancellation(&request.method) + && !cancellable_requests.register(request.id) + { + warn!(method = %request.method, "duplicate pending cancellable request ID"); + break; + } + let handler = request_handlers.read().get(&request.method).cloned(); + if let Some(handler) = handler { + let write_tx = write_tx.clone(); + let closed = connection_closed.clone(); + // Internal handlers may register request state before + // the reader dispatches a following notification. + let response = handler(request.params.unwrap_or(Value::Null)); + tokio::spawn(async move { + let result = tokio::select! { + biased; + _ = closed.cancelled() => return, + result = response => result, + }; + let (result, error) = match result { + Ok(value) => (Some(value), None), + Err(error) => ( + None, + Some(JsonRpcError { + code: error_codes::INTERNAL_ERROR, + message: error.to_string(), + data: None, + }), + ), + }; + let response = JsonRpcResponse { + jsonrpc: "2.0".into(), + id: request.id, + result, + error, + }; + if let Err(error) = Self::write_message(&write_tx, &response).await + { + warn!(%error, "failed to send connection request response"); + } + }); + } else if request_tx.send(request).is_err() { warn!("failed to forward JSON-RPC request, channel closed"); } } @@ -472,6 +684,12 @@ impl JsonRpcClient { } } } + connection_closed.cancel(); + cancellable_requests.clear(); + // A handler may own the last Client clone, whose drop closes the RPC. + // Release the registry lock before dropping those captured values. + let handlers = std::mem::take(&mut *request_handlers.write()); + drop(handlers); // Drain in-flight requests so callers observe cancellation // instead of hanging on a oneshot receiver. @@ -576,18 +794,34 @@ impl JsonRpcClient { /// response payload is discarded and an internal-error JSON-RPC /// error is delivered instead). The error is never propagated back /// to the server and does not crash the read loop. - pub(crate) async fn send_request_with_inline_callback( + pub(crate) fn send_request_with_inline_callback( &self, method: &str, params: Option, inline_callback: Option, - ) -> Result { - let request_start = Instant::now(); + ) -> impl std::future::Future> + Send + 'static { let id = self.request_id.fetch_add(1, Ordering::SeqCst); - let request = JsonRpcRequest::new(id, method, params); + Self::send_owned_request( + self.pending_requests.clone(), + self.write_tx.clone(), + JsonRpcRequest::new(id, method, params), + inline_callback, + ) + } + // Own the request bookkeeping, not the connection. A pending response may + // outlive its caller without preventing the last Client from closing I/O. + async fn send_owned_request( + pending_requests: Arc>>, + write_tx: mpsc::UnboundedSender, + request: JsonRpcRequest, + inline_callback: Option, + ) -> Result { + let request_start = Instant::now(); + let id = request.id; + let method = request.method.as_str(); let (tx, rx) = oneshot::channel(); - self.pending_requests.write().insert( + pending_requests.write().insert( id, PendingRequest { sender: tx, @@ -600,7 +834,7 @@ impl JsonRpcClient { // success return so the read loop owns the cleanup on the happy // path. let mut guard = PendingGuard { - map: &self.pending_requests, + map: &pending_requests, id, armed: true, }; @@ -608,7 +842,7 @@ impl JsonRpcClient { // The PendingGuard's drop removes the entry on every error path // and on cancellation; disarmed below before the success return so // the read loop owns the cleanup on the happy path. - if let Err(error) = self.write(&request).await { + if let Err(error) = Self::write_message(&write_tx, &request).await { warn!( elapsed_ms = request_start.elapsed().as_millis(), method = %method, @@ -643,7 +877,7 @@ impl JsonRpcClient { request_id = id, status = "failed", code = error.code, - error = %error.message, + error = %remote_error_log_message(method, &error.message), "JsonRpcClient::send_request JSON-RPC request finished" ); } else { @@ -667,6 +901,13 @@ impl JsonRpcClient { /// drops the ack receiver; the actor still completes the frame and /// flushes. A partial frame can never appear on the wire. pub async fn write(&self, message: &T) -> Result<(), Error> { + Self::write_message(&self.write_tx, message).await + } + + async fn write_message( + write_tx: &mpsc::UnboundedSender, + message: &T, + ) -> Result<(), Error> { let body = serde_json::to_vec(message)?; let mut frame = Vec::with_capacity(CONTENT_LENGTH_HEADER.len() + 16 + body.len() + 4); frame.extend_from_slice(CONTENT_LENGTH_HEADER.as_bytes()); @@ -675,7 +916,7 @@ impl JsonRpcClient { frame.extend_from_slice(&body); let (ack_tx, ack_rx) = oneshot::channel(); - self.write_tx + write_tx .send(WriteCommand { frame, ack: ack_tx }) .map_err(|_| { Error::from(std::io::Error::new( @@ -695,6 +936,12 @@ impl JsonRpcClient { } } +impl Drop for JsonRpcClient { + fn drop(&mut self) { + self.force_close(); + } +} + /// RAII guard that removes a pending-request entry from the map if the /// owning future is dropped before the response arrives. Disarmed on the /// happy path so the read loop's response handling owns the cleanup. @@ -719,149 +966,4 @@ impl Drop for PendingGuard<'_> { } #[cfg(test)] -mod tests { - use super::*; - - #[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 d4c3510839..93790a94b8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,7 +3,26 @@ #![deny(rustdoc::broken_intra_doc_links)] #![cfg_attr(test, allow(clippy::unwrap_used))] -#[cfg(all(not(feature = "bundled-cli"), not(feature = "local-runtime")))] +/// Experimental runtime-owned Agent Host Protocol listeners. +pub mod ahp_host; +pub use ahp_host::{ + AhpHost, AhpHostExit, AhpHostExitCallback, AhpHostExitReason, AhpHostOptions, + AhpSessionFactory, AhpSessionReleasedCallback, AhpSessionRequest, AhpSessionResumeFactory, + 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"), + any(test, has_extracted_cli) +))] mod cache_paths; /// Canvas declarations, provider callbacks, and host-side canvas RPC types. pub mod canvas; @@ -32,6 +51,8 @@ pub mod github_token; pub mod handler; /// Lifecycle hook callbacks (pre/post tool use, prompt submission, session start/end). pub mod hooks; +/// Connection-global human confirmation for experimental installation operations. +pub mod installation_confirmation; mod jsonrpc; /// Permission-policy helpers that produce a [`handler::PermissionHandler`]. pub mod permission; @@ -40,6 +61,7 @@ mod process_tree; pub mod provider_token; mod provider_token_dispatch; /// GitHub Copilot CLI binary resolution (env var, embedded, dev cache). +#[cfg(feature = "runtime")] pub(crate) mod resolve; mod router; /// Session management — create, resume, send messages, and interact with the agent. @@ -47,6 +69,8 @@ pub mod session; /// Custom session filesystem provider (virtualizable filesystem layer). pub mod session_fs; mod session_fs_dispatch; +/// Session-scoped skill provider callbacks. +pub mod skill_provider; /// Per-phase timing breakdown for [`Client::start`]. pub mod startup_timings; /// Event subscription handles returned by `subscribe()` methods. @@ -85,7 +109,10 @@ pub(crate) mod generated; pub mod mode; use std::ffi::OsString; -use std::path::{Path, PathBuf}; +#[cfg(feature = "runtime")] +use std::path::Path; +use std::path::PathBuf; +#[cfg(feature = "runtime")] use std::process::Stdio; use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; @@ -106,6 +133,7 @@ pub(crate) use jsonrpc::{ }; pub use mode::{BUILTIN_TOOLS_ISOLATED, ClientMode, ToolSet}; pub use provider_token::{BearerTokenError, BearerTokenProvider, ProviderTokenArgs}; +pub use skill_provider::{SkillProvider, SkillProviderDescriptor}; /// Re-exported JSON-RPC internals for integration tests (requires `test-support` feature). #[cfg(feature = "test-support")] @@ -116,11 +144,20 @@ pub mod test_support { }; } use serde::{Deserialize, Serialize}; -use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader}; +#[cfg(feature = "runtime")] +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::io::{AsyncRead, AsyncWrite}; +#[cfg(feature = "runtime")] use tokio::net::TcpStream; -use tokio::process::{Child, Command}; -use tokio::sync::{broadcast, mpsc, oneshot}; -use tracing::{Instrument, debug, error, info, warn}; +use tokio::process::Child; +#[cfg(feature = "runtime")] +use tokio::process::Command; +#[cfg(feature = "runtime")] +use tokio::sync::oneshot; +use tokio::sync::{broadcast, mpsc}; +#[cfg(feature = "runtime")] +use tracing::Instrument; +use tracing::{debug, error, info, warn}; pub use types::*; mod sdk_protocol_version; @@ -132,6 +169,7 @@ pub use subscription::{EventSubscription, LifecycleSubscription}; const MIN_PROTOCOL_VERSION: u32 = 3; const RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); +#[cfg(feature = "runtime")] fn record_optional_millis(span: &tracing::Span, field: &'static str, value: Option) { match value { Some(value) => { @@ -353,6 +391,12 @@ pub struct ClientOptions { /// of sessions. pub extension_launch_provider: Option>, + /// Connection-global human review handler for `installations.confirm` (experimental). + /// + /// This receives requests without session routing or inferred operation authority. + /// It does not register or enable installation capabilities on the runtime. + pub installation_confirmation_handler: + Option>, /// Connection-level GitHub telemetry forwarding callback (experimental). /// /// When set, every session created or resumed on this client opts into @@ -545,6 +589,13 @@ impl std::fmt::Debug for ClientOptions { "extension_launch_provider", &self.extension_launch_provider.as_ref().map(|_| ""), ) + .field( + "installation_confirmation_handler", + &self + .installation_confirmation_handler + .as_ref() + .map(|_| ""), + ) .field( "on_github_telemetry", &self.on_github_telemetry.as_ref().map(|_| ""), @@ -800,6 +851,7 @@ impl Default for ClientOptions { session_fs: None, request_handler: None, extension_launch_provider: None, + installation_confirmation_handler: None, on_github_telemetry: None, on_get_trace_context: None, telemetry: None, @@ -971,6 +1023,15 @@ impl ClientOptions { self } + /// Install a connection-global human confirmation handler (experimental). + pub fn with_installation_confirmation_handler(mut self, handler: H) -> Self + where + H: crate::installation_confirmation::InstallationConfirmationHandler, + { + self.installation_confirmation_handler = Some(Arc::new(handler)); + self + } + /// Register a connection-level GitHub telemetry forwarding callback /// (internal/experimental). Registering a callback auto-enables telemetry /// forwarding on every session created or resumed on this client; the @@ -1049,6 +1110,7 @@ impl ClientOptions { } /// Validate a [`SessionFsConfig`] before sending `sessionFs.setProvider`. +#[cfg(feature = "runtime")] fn validate_session_fs_config(cfg: &SessionFsConfig) -> Result<()> { if cfg.initial_cwd.trim().is_empty() { return Err(Error::with_message( @@ -1071,6 +1133,7 @@ fn validate_session_fs_config(cfg: &SessionFsConfig) -> Result<()> { /// pre-1.0 review consensus, so adopting a `Uuid` type just for SDK- /// generated secrets would be inconsistent and semantically misleading; /// this is opaque random data, not an identifier). +#[cfg(any(feature = "runtime", test, feature = "test-support"))] fn generate_connection_token() -> String { let mut bytes = [0u8; 16]; getrandom::fill(&mut bytes) @@ -1087,9 +1150,11 @@ fn generate_connection_token() -> String { /// leaves [`ClientOptions::transport`] at [`Transport::Default`]. /// Accepts `"inprocess"` or `"stdio"` (case-insensitive); unset preserves /// stdio. Any other value is an error. +#[cfg(feature = "runtime")] const DEFAULT_CONNECTION_ENV_VAR: &str = "COPILOT_SDK_DEFAULT_CONNECTION"; /// Resolve a transport override from [`DEFAULT_CONNECTION_ENV_VAR`]. +#[cfg(feature = "runtime")] fn resolve_default_transport(options: &ClientOptions) -> Result { let configured = options .env @@ -1103,6 +1168,7 @@ fn resolve_default_transport(options: &ClientOptions) -> Result { resolve_default_transport_value(configured.as_deref().or(process.as_deref())) } +#[cfg(feature = "runtime")] fn resolve_default_transport_value(value: Option<&str>) -> Result { match value { None => Ok(Transport::Stdio), @@ -1168,6 +1234,8 @@ fn validate_inprocess_options(options: &ClientOptions) -> Result<()> { /// The child process (if any) is killed when the last clone drops. #[derive(Clone)] pub struct Client { + // Session-internal clients share the connection, not its AHP retention root. + ahp_host_sessions: Option>, inner: Arc, } @@ -1181,7 +1249,11 @@ impl std::fmt::Debug for Client { } struct ClientInner { + ahp_host_callbacks: Arc, + ahp_host_sessions: std::sync::Weak, child: parking_lot::Mutex>, + owns_stdio: bool, + force_stop_requested: tokio_util::sync::CancellationToken, process_tree: parking_lot::Mutex>, #[cfg(feature = "in-process")] /// In-process FFI runtime host, set only for [`Transport::InProcess`]. @@ -1200,10 +1272,12 @@ 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>, extension_launch_provider: Arc, + installation_confirmation: Arc, /// Connection-level GitHub telemetry forwarding callback, set from /// [`ClientOptions::on_github_telemetry`]. Drives the /// `enableGitHubTelemetryForwarding` wire flag and the @@ -1228,6 +1302,19 @@ struct ClientInner { startup_timings: OnceLock, } +struct StdioShutdownGuard<'a> { + client: &'a Client, + armed: bool, +} + +impl Drop for StdioShutdownGuard<'_> { + fn drop(&mut self) { + if self.armed { + self.client.force_stop(); + } + } +} + impl Client { /// Start a CLI server process with the given options. /// @@ -1241,6 +1328,7 @@ impl Client { /// When [`ClientOptions::session_fs`] is set, also calls /// `sessionFs.setProvider` to register the SDK as the filesystem /// backend. + #[cfg(feature = "runtime")] pub async fn start(options: ClientOptions) -> Result { let start_time = Instant::now(); let mut timings = StartupTimings::default(); @@ -1354,10 +1442,15 @@ impl Client { let session_fs_config = options.session_fs.clone(); let request_handler = options.request_handler.clone(); let extension_launch_provider = options.extension_launch_provider.clone(); + let installation_confirmation_handler = options.installation_confirmation_handler.clone(); let session_fs_sqlite_declared = session_fs_config .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"); @@ -1429,11 +1522,13 @@ 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(), options.mode, options.client_info, + false, )? } Transport::Tcp { @@ -1463,11 +1558,13 @@ 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(), options.mode, options.client_info, + false, )? } Transport::Stdio => { @@ -1487,11 +1584,13 @@ 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(), options.mode, options.client_info, + true, )? } Transport::InProcess => { @@ -1555,11 +1654,13 @@ 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(), options.mode, options.client_info, + false, )?; *client.inner.ffi_host.lock() = Some(shared); client @@ -1569,6 +1670,10 @@ impl Client { } }; timings.transport_setup_ms = StartupTimings::millis(transport_setup_start.elapsed()); + client + .inner + .installation_confirmation + .set_handler(installation_confirmation_handler); debug!( elapsed_ms = start_time.elapsed().as_millis(), "Client::start transport setup complete" @@ -1588,15 +1693,8 @@ impl Client { let _ = client.inner.llm_inference.set(dispatcher.clone()); dispatcher }); + client.inner.router.ensure_started(&client.inner); if client.inner.extension_launch_provider.is_configured() { - client.inner.router.ensure_started( - &client.inner.notification_tx, - &client.inner.request_rx, - client.inner.extension_launch_provider.clone(), - request_dispatcher.clone(), - client.inner.on_github_telemetry.clone(), - client.inner.github_token_registry.clone(), - ); client.rpc().register_extension_launch_provider().await?; } if !builtin_plugin_directories.is_empty() { @@ -1612,6 +1710,7 @@ impl Client { let capabilities = cfg.capabilities.as_ref().map(|c| { crate::generated::api_types::SessionFsSetProviderCapabilities { sqlite: Some(c.sqlite), + binary: Some(c.binary), } }); let request = crate::generated::api_types::SessionFsSetProviderRequest { @@ -1628,18 +1727,8 @@ impl Client { "Client::start session filesystem setup complete" ); } - if let Some(dispatcher) = request_dispatcher { + if request_dispatcher.is_some() { let llm_inference_start = Instant::now(); - // Start the router early (before any session is registered) so the - // startup model catalog request is dispatched to the handler. - client.inner.router.ensure_started( - &client.inner.notification_tx, - &client.inner.request_rx, - client.inner.extension_launch_provider.clone(), - Some(dispatcher.clone()), - client.inner.on_github_telemetry.clone(), - client.inner.github_token_registry.clone(), - ); client.rpc().llm_inference().set_provider().await?; let llm_inference_elapsed = llm_inference_start.elapsed(); timings.llm_handler_ms = Some(StartupTimings::millis(llm_inference_elapsed)); @@ -1680,6 +1769,53 @@ impl Client { Ok(client) } + /// Runtime startup is unavailable in an external-stream-only build. + /// + /// Enable the `runtime` feature to launch or discover a runtime, or use + /// [`Client::from_streams`] to attach an externally supplied connection. + #[cfg(not(feature = "runtime"))] + pub async fn start(_options: ClientOptions) -> Result { + Err(Error::with_message( + ErrorKind::InvalidConfig, + "Client::start requires the `runtime` Cargo feature; use Client::from_streams", + )) + } + + /// Register an inbound connection-level SDK JSON-RPC operation. + /// + /// The handler runs independently of the reader, so it may await ordinary + /// SDK requests on this connection. Its result is written using the normal + /// framed writer. Duplicate registrations are rejected. + /// Use the supplied request-scoped client for SDK calls instead of capturing + /// a client clone, which would create an ownership cycle in the registry. + /// + /// This low-level integration hook is used by runtime-supervised hosts. + #[doc(hidden)] + pub fn register_request_handler(&self, method: &str, handler: F) -> Result<()> + where + F: Fn(serde_json::Value, Client) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + let client = Arc::downgrade(&self.inner); + let handler = Arc::new(handler); + self.inner.rpc.register_request_handler( + method, + Arc::new(move |params| { + let client = client.clone(); + let handler = handler.clone(); + Box::pin(async move { + let inner = client.upgrade().ok_or_else(|| { + Error::with_message( + ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled), + "Request handler connection is closed", + ) + })?; + handler(params, Client::from_inner(inner)).await + }) + }), + ) + } + /// Create a Client from raw async streams (no child process). /// /// Useful for testing or connecting to a server over a custom transport. @@ -1698,11 +1834,45 @@ 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, + ClientMode::default(), + None, + false, ) } @@ -1726,14 +1896,33 @@ impl Client { Some(provider), false, false, + false, None, None, None, ClientMode::default(), None, + false, ) } + /// Construct a client with a confirmation handler for framed transport tests. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_installation_confirmation_handler( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + handler: Arc, + ) -> Result { + let client = Self::from_streams(reader, writer, cwd)?; + client + .inner + .installation_confirmation + .set_handler(Some(handler)); + Ok(client) + } + /// Construct a [`Client`] from raw streams with a /// [`TraceContextProvider`] preset, for integration testing. /// @@ -1758,11 +1947,13 @@ impl Client { None, false, false, + false, Some(provider), None, None, ClientMode::default(), None, + false, ) } @@ -1786,11 +1977,13 @@ impl Client { None, false, false, + false, None, None, token, ClientMode::default(), None, + false, ) } @@ -1814,11 +2007,13 @@ impl Client { None, false, false, + false, None, Some(on_github_telemetry), None, ClientMode::default(), None, + false, ) } @@ -1853,11 +2048,29 @@ impl Client { None, false, false, + false, None, None, None, ClientMode::default(), client_info, + false, + ) + } + + /// Construct a client from raw streams with a preset mode, for integration + /// tests that need mode-specific session defaults without spawning a CLI. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_mode_for_test( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + mode: ClientMode, + ) -> Result { + Self::from_transport( + reader, writer, None, None, cwd, None, None, false, false, false, None, None, None, + mode, None, false, ) } @@ -1874,20 +2087,24 @@ 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, mode: ClientMode, client_info: Option, + owns_stdio: bool, ) -> Result { let setup_start = Instant::now(); let (request_tx, request_rx) = mpsc::unbounded_channel::(); let (notification_broadcast_tx, _) = broadcast::channel::(1024); - let rpc = JsonRpcClient::new( + let (host_notification_tx, host_notification_rx) = mpsc::unbounded_channel(); + let rpc = JsonRpcClient::new_with_host_notifications( writer, reader, notification_broadcast_tx.clone(), request_tx, + Some(host_notification_tx), ); let pid = child.as_ref().and_then(|c| c.id()); @@ -1899,9 +2116,17 @@ impl Client { extension_launch_provider, ), ); + let installation_confirmation = + Arc::new(installation_confirmation::InstallationConfirmationDispatcher::new()); + let ahp_host_sessions = Arc::new(ahp_host::HostSessions::new()); let client = Self { + ahp_host_sessions: Some(ahp_host_sessions.clone()), inner: Arc::new(ClientInner { + ahp_host_callbacks: Arc::new(ahp_host::ExitCallbacks::default()), + ahp_host_sessions: Arc::downgrade(&ahp_host_sessions), child: parking_lot::Mutex::new(child), + owns_stdio, + force_stop_requested: tokio_util::sync::CancellationToken::new(), process_tree: parking_lot::Mutex::new(process_tree), #[cfg(feature = "in-process")] ffi_host: parking_lot::Mutex::new(None), @@ -1918,8 +2143,10 @@ 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(), on_github_telemetry, on_get_trace_context, effective_connection_token, @@ -1928,9 +2155,13 @@ impl Client { startup_timings: OnceLock::new(), }), }; + ahp_host_sessions.set_owner(Arc::downgrade(&client.inner)); github_token_registry.set_client(Arc::downgrade(&client.inner)); extension_launch_provider.set_client(Arc::downgrade(&client.inner)); + installation_confirmation.set_client(Arc::downgrade(&client.inner)); client.spawn_lifecycle_dispatcher(); + client.spawn_ahp_host_dispatcher(host_notification_rx); + client.register_ahp_session_factory()?; debug!( elapsed_ms = setup_start.elapsed().as_millis(), pid = ?pid, @@ -1979,6 +2210,7 @@ impl Client { }); } + #[cfg(feature = "runtime")] fn build_command(program: &Path, options: &ClientOptions, working_directory: &Path) -> Command { let mut command = Command::new(program); command.kill_on_drop(true); @@ -2037,6 +2269,9 @@ impl Client { for key in &options.env_remove { command.env_remove(key); } + if options.mode != ClientMode::Empty { + command.env("COPILOT_RUNTIME_PROCESS_FILE_LOGGING", "1"); + } command .current_dir(working_directory) .stdout(Stdio::piped()) @@ -2052,6 +2287,7 @@ impl Client { /// When the effective `use_logged_in_user` is `false` (either explicitly /// or because a token was provided without an override), adds /// `--no-auto-login`. + #[cfg(feature = "runtime")] fn auth_args(options: &ClientOptions) -> Vec<&'static str> { let mut args: Vec<&'static str> = Vec::new(); if options.github_token.is_some() { @@ -2070,6 +2306,7 @@ impl Client { /// Returns `--session-idle-timeout ` when /// [`ClientOptions::session_idle_timeout_seconds`] is `Some(n)` with /// `n > 0`. Otherwise returns an empty vector. + #[cfg(feature = "runtime")] fn session_idle_timeout_args(options: &ClientOptions) -> Vec { match options.session_idle_timeout_seconds { Some(secs) if secs > 0 => { @@ -2079,6 +2316,7 @@ impl Client { } } + #[cfg(feature = "runtime")] fn remote_args(options: &ClientOptions) -> Vec { if options.enable_remote_sessions { vec!["--remote".to_string()] @@ -2087,6 +2325,7 @@ impl Client { } } + #[cfg(feature = "runtime")] fn log_level_args(options: &ClientOptions) -> Vec<&'static str> { match options.log_level { Some(level) => vec!["--log-level", level.as_str()], @@ -2094,6 +2333,7 @@ impl Client { } } + #[cfg(feature = "runtime")] fn spawn_stdio( program: &Path, options: &ClientOptions, @@ -2119,6 +2359,7 @@ impl Client { Ok((child, tree, spawn_elapsed)) } + #[cfg(feature = "runtime")] async fn spawn_tcp( program: &Path, options: &ClientOptions, @@ -2192,6 +2433,7 @@ impl Client { Ok((child, tree, actual_port, spawn_elapsed, port_wait_elapsed)) } + #[cfg(feature = "runtime")] fn drain_stderr(child: &mut Child) { if let Some(stderr) = child.stderr.take() { let span = tracing::error_span!("copilot_cli"); @@ -2301,32 +2543,37 @@ impl Client { /// If the callback returns an error, that error is propagated to /// this awaiter in place of the response. The callback never causes /// the read loop to crash. - pub(crate) async fn call_with_inline_callback( + /// + /// The returned future owns the request but not this `Client`, so it + /// can be spawned without keeping the connection open. + pub(crate) fn call_with_inline_callback( &self, method: &str, params: Option, inline_callback: Option, - ) -> Result { + ) -> impl std::future::Future> + Send + 'static { let session_id: Option = params .as_ref() .and_then(|p| p.get("sessionId")) .and_then(|v| v.as_str()) .map(SessionId::from); - let response = self - .inner - .rpc - .send_request_with_inline_callback(method, params, inline_callback) - .await?; - if let Some(err) = response.error { - if err.message.contains("Session not found") { - return Err(ErrorKind::Session(SessionErrorKind::NotFound( - session_id.unwrap_or_else(|| "unknown".into()), - )) - .into()); + let request = + self.inner + .rpc + .send_request_with_inline_callback(method, params, inline_callback); + async move { + let response = request.await?; + if let Some(err) = response.error { + if err.message.contains("Session not found") { + return Err(ErrorKind::Session(SessionErrorKind::NotFound( + session_id.unwrap_or_else(|| "unknown".into()), + )) + .into()); + } + return Err(Error::from_rpc(err.code, err.message, err.data)); } - return Err(Error::from_rpc(err.code, err.message, err.data)); + Ok(response.result.unwrap_or(serde_json::Value::Null)) } - Ok(response.result.unwrap_or(serde_json::Value::Null)) } /// Send a JSON-RPC response back to the CLI (e.g. for permission or tool call requests). @@ -2336,7 +2583,10 @@ impl Client { /// Reconstruct a [`Client`] handle from a shared inner pointer. pub(crate) fn from_inner(inner: Arc) -> Self { - Self { inner } + Self { + inner, + ahp_host_sessions: None, + } } /// Take the receiver for incoming JSON-RPC requests from the CLI. @@ -2361,17 +2611,35 @@ impl Client { &self, session_id: &SessionId, ) -> crate::router::SessionRegistration { - self.inner.router.ensure_started( - &self.inner.notification_tx, - &self.inner.request_rx, - self.inner.extension_launch_provider.clone(), - self.inner.llm_inference.get().cloned(), - self.inner.on_github_telemetry.clone(), - self.inner.github_token_registry.clone(), - ); + self.inner.router.ensure_started(&self.inner); self.inner.router.register(session_id) } + /// Like [`register_session`](Self::register_session), but keeps the + /// registration it displaced so a failed resume can restore it with + /// [`restore_session_owned`](Self::restore_session_owned). + pub(crate) fn replace_session_registration( + &self, + session_id: &SessionId, + ) -> ( + crate::router::SessionRegistration, + Option, + ) { + self.inner.router.ensure_started(&self.inner); + self.inner.router.replace(session_id) + } + + /// Unregister the registration identified by `token`, handing the ID back + /// to `replaced` if that session is still running. + pub(crate) fn restore_session_owned( + &self, + session_id: &SessionId, + token: crate::router::RegistrationToken, + replaced: Option, + ) { + self.inner.router.restore_owned(session_id, token, replaced); + } + /// Unregister a session only if `token` still identifies the live /// registration. /// @@ -2402,14 +2670,7 @@ impl Client { &self, provider: Arc, ) -> github_token::GitHubTokenRegistration { - self.inner.router.ensure_started( - &self.inner.notification_tx, - &self.inner.request_rx, - self.inner.extension_launch_provider.clone(), - self.inner.llm_inference.get().cloned(), - self.inner.on_github_telemetry.clone(), - self.inner.github_token_registry.clone(), - ); + self.inner.router.ensure_started(&self.inner); let id = self.inner.github_token_registry.register(provider); github_token::GitHubTokenRegistration::new(self.inner.github_token_registry.clone(), id) } @@ -2635,14 +2896,7 @@ impl Client { #[cfg(feature = "test-support")] #[doc(hidden)] pub fn start_router_for_test(&self) { - self.inner.router.ensure_started( - &self.inner.notification_tx, - &self.inner.request_rx, - self.inner.extension_launch_provider.clone(), - self.inner.llm_inference.get().cloned(), - self.inner.on_github_telemetry.clone(), - self.inner.github_token_registry.clone(), - ); + self.inner.router.ensure_started(&self.inner); } #[cfg(feature = "test-support")] @@ -2798,8 +3052,10 @@ impl Client { /// Cooperatively shut down the client and the CLI child process. /// /// Walks every still-registered session and sends `session.detach` - /// for each one, asks SDK-owned runtimes to shut down, terminates the - /// Windows-owned CLI Job Object when present, and reaps the root process. + /// for each one and asks SDK-owned runtimes to shut down. For an owned stdio + /// child, closes stdin and waits up to 10 seconds for host cleanup and exit + /// before falling back to termination. Terminates the Windows-owned CLI + /// Job Object when present and bounds the final root-process reap to 10 seconds. /// Errors from per-session detaches, runtime shutdown, and final process /// termination are collected into [`StopErrors`] rather than /// short-circuiting on the first failure — so callers see the full picture @@ -2807,7 +3063,7 @@ impl Client { /// /// If you have already called [`Session::disconnect`] on every /// session this client created, the per-session destroy step is a - /// no-op (the router map is empty); only the child-kill remains. + /// no-op (the router map is empty); runtime and process shutdown still run. /// /// [`Session::disconnect`]: crate::session::Session::disconnect /// @@ -2822,11 +3078,14 @@ impl Client { /// or call `stop()` again with a fresh future. The documented /// `tokio::time::timeout(..., client.stop())` pattern in the example /// below uses `force_stop` as the fallback for exactly this case. + /// Once owned-stdio exit waiting begins, cancelling `stop()` forcibly + /// terminates that child. Concurrent `force_stop()` also interrupts the wait. pub async fn stop(&self) -> std::result::Result<(), StopErrors> { let pid = self.pid(); info!(pid = ?pid, "stopping CLI process"); let mut errors: Vec = Vec::new(); self.inner.extension_launch_provider.clear(); + self.inner.installation_confirmation.clear(); // Snapshot the registered session IDs without holding the router // lock across the detach RPCs. @@ -2885,10 +3144,43 @@ impl Client { } } - let child = self.inner.child.lock().take(); - let process_tree = self.inner.process_tree.lock().take(); *self.inner.state.lock() = ConnectionState::Disconnected; *self.inner.models_cache.lock() = Arc::new(tokio::sync::OnceCell::new()); + if self.inner.owns_stdio && self.inner.child.lock().is_some() { + let mut guard = StdioShutdownGuard { + client: self, + armed: true, + }; + // The host flushes telemetry after stdin EOF, not after the shutdown RPC. + // Drop ChildStdin but retain the reader and process tree until exit. + self.inner.rpc.close_writer(); + let wait = async { + tokio::select! { + result = std::future::poll_fn(|cx| { + let mut child = self.inner.child.lock(); + let Some(child) = child.as_mut() else { + return std::task::Poll::Ready(Ok(())); + }; + // Child::wait is cancel-safe; retain ownership between polls so + // synchronous force_stop can still terminate the child and its tree. + std::future::Future::poll(std::pin::pin!(child.wait()), cx) + .map(|result| result.map(|_| ())) + }) => result, + _ = self.inner.force_stop_requested.cancelled() => Ok(()), + } + }; + match tokio::time::timeout(RUNTIME_SHUTDOWN_TIMEOUT, wait).await { + Ok(Ok(_)) => {} + Ok(Err(error)) => errors.push(error.into()), + Err(_) => warn!( + timeout = ?RUNTIME_SHUTDOWN_TIMEOUT, + "CLI did not exit after stdin EOF; terminating" + ), + } + guard.armed = false; + } + let child = self.inner.child.lock().take(); + let process_tree = self.inner.process_tree.lock().take(); if let Some(process_tree) = process_tree && let Err(error) = process_tree.terminate() { @@ -2898,14 +3190,16 @@ impl Client { match child.try_wait() { Ok(Some(_status)) => {} Ok(None) => { - // The runtime completes all cleanup before responding to - // runtime.shutdown and then leaves termination to us; it - // deliberately keeps its JSON-RPC server alive to send the - // response and never self-exits. Waiting for a self-exit - // that will never come just wastes time, so terminate the - // child immediately. - if let Err(e) = child.kill().await { - errors.push(e.into()); + match tokio::time::timeout(RUNTIME_SHUTDOWN_TIMEOUT, child.kill()).await { + Ok(Ok(())) => {} + Ok(Err(error)) => errors.push(error.into()), + Err(_) => errors.push( + std::io::Error::new( + std::io::ErrorKind::TimedOut, + "CLI process reap timed out during Client::stop", + ) + .into(), + ), } } Err(e) => errors.push(e.into()), @@ -2917,8 +3211,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)); } } @@ -2962,6 +3259,7 @@ impl Client { let pid = self.pid(); info!(pid = ?pid, "force-stopping CLI process"); self.inner.extension_launch_provider.clear(); + self.inner.installation_confirmation.clear(); if let Some(process_tree) = self.inner.process_tree.lock().take() && let Err(error) = process_tree.terminate() { @@ -2972,6 +3270,7 @@ impl Client { { error!(pid = ?pid, error = %e, "failed to send kill signal"); } + self.inner.force_stop_requested.cancel(); self.inner.rpc.force_close(); #[cfg(feature = "in-process")] { @@ -3028,6 +3327,7 @@ impl Client { impl Drop for ClientInner { fn drop(&mut self) { + self.rpc.force_close(); let pid = self.child.lock().as_ref().and_then(Child::id); if let Some(process_tree) = self.process_tree.lock().take() && let Err(error) = process_tree.terminate() @@ -3044,785 +3344,11 @@ impl Drop for ClientInner { #[cfg(feature = "in-process")] { if let Some(host) = self.ffi_host.lock().take() { - self.rpc.force_close(); host.close(); } } } } -#[cfg(test)] -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 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, - ) - .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 { - inner: Arc::new(ClientInner { - child: parking_lot::Mutex::new(None), - 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), - ), - 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; - } - } -} +#[cfg(all(test, feature = "runtime"))] +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/process_tree.rs b/rust/src/process_tree.rs index 8dc7a451c5..ef9d3fc62e 100644 --- a/rust/src/process_tree.rs +++ b/rust/src/process_tree.rs @@ -7,8 +7,10 @@ use std::io; +#[cfg(feature = "runtime")] use tokio::process::{Child, Command}; +#[cfg(feature = "runtime")] pub(crate) fn spawn(command: &mut Command) -> io::Result<(Child, Option)> { #[cfg(windows)] { @@ -36,7 +38,7 @@ impl Drop for ProcessTree { } } -#[cfg(not(windows))] +#[cfg(not(all(windows, feature = "runtime")))] mod platform { pub(super) struct Tree; @@ -47,7 +49,7 @@ mod platform { } } -#[cfg(windows)] +#[cfg(all(windows, feature = "runtime"))] mod platform { use std::mem::size_of; use std::os::windows::process::CommandExt; 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 646f999ad3..9b0d68464b 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Weak}; use parking_lot::Mutex; use tokio::sync::{broadcast, mpsc}; use tracing::warn; -use crate::jsonrpc::{JsonRpcNotification, JsonRpcRequest}; +use crate::jsonrpc::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, error_codes}; use crate::types::{SessionEventNotification, SessionId}; /// Identity of one specific registration of a session ID. @@ -41,6 +41,13 @@ struct SessionSenders { token: RegistrationToken, } +/// A registration displaced by [`SessionRouter::replace`]. +/// +/// Holding it keeps the displaced session's channels open, so a failed +/// resume can hand routing back with [`SessionRouter::restore_owned`]. +/// Dropping it closes them, which ends that session's event loop. +pub(crate) struct ReplacedRegistration(SessionSenders); + /// Routes notifications and requests by sessionId to per-session channels. /// /// Internal to the SDK — consumers interact via `Client::register_session()`. @@ -64,10 +71,20 @@ impl SessionRouter { /// Replaces any existing registration for the same ID and returns a /// fresh [`RegistrationToken`] identifying this registration. pub(crate) fn register(&self, session_id: &SessionId) -> SessionRegistration { + self.replace(session_id).0 + } + + /// Like [`register`](Self::register), but also returns the registration + /// it displaced so the caller can [`restore_owned`](Self::restore_owned) + /// it if the new one is abandoned. + pub(crate) fn replace( + &self, + session_id: &SessionId, + ) -> (SessionRegistration, Option) { let (notif_tx, notif_rx) = mpsc::unbounded_channel(); let (req_tx, req_rx) = mpsc::unbounded_channel(); let token = RegistrationToken(self.next_token.fetch_add(1, Ordering::Relaxed)); - self.sessions.lock().insert( + let replaced = self.sessions.lock().insert( session_id.clone(), SessionSenders { notifications: notif_tx, @@ -75,13 +92,14 @@ impl SessionRouter { token, }, ); - SessionRegistration { + let registration = SessionRegistration { channels: SessionChannels { notifications: notif_rx, requests: req_rx, }, token, - } + }; + (registration, replaced.map(ReplacedRegistration)) } /// Unregister a session, dropping its channels. @@ -94,6 +112,17 @@ impl SessionRouter { self.sessions.lock().remove(session_id.as_str()); } + pub(crate) fn is_registered_owner( + &self, + session_id: &SessionId, + token: RegistrationToken, + ) -> bool { + self.sessions + .lock() + .get(session_id.as_str()) + .is_some_and(|senders| senders.token == token) + } + /// Unregister a session only if it is still the registration identified /// by `token`. /// @@ -104,17 +133,37 @@ impl SessionRouter { &self, session_id: &SessionId, token: RegistrationToken, + ) -> bool { + self.restore_owned(session_id, token, None) + } + + /// Unregister the registration identified by `token`, handing the ID + /// back to `replaced` when that session is still running. + /// + /// Returns `false`, leaving the router unchanged, when `token` no longer + /// identifies the live registration. + pub(crate) fn restore_owned( + &self, + session_id: &SessionId, + token: RegistrationToken, + replaced: Option, ) -> bool { let mut sessions = self.sessions.lock(); - if sessions + if !sessions .get(session_id.as_str()) .is_some_and(|senders| senders.token == token) { - sessions.remove(session_id.as_str()); - true - } else { - false + return false; + } + match replaced.filter(|replaced| !replaced.0.requests.is_closed()) { + Some(ReplacedRegistration(senders)) => { + sessions.insert(session_id.clone(), senders); + } + None => { + sessions.remove(session_id.as_str()); + } } + true } /// Snapshot every currently-registered session ID. @@ -142,29 +191,25 @@ impl SessionRouter { /// Start the router tasks if not already running. /// - /// Takes the notification broadcast and request channel from the Client. - /// If `request_rx` is `None` (already taken by `take_request_rx()`), + /// Takes the notification broadcast and request channel from the client. + /// If its request receiver was already taken by `take_request_rx()`, /// only notification routing is available. - pub(crate) fn ensure_started( - &self, - notification_tx: &broadcast::Sender, - request_rx: &Mutex>>, - extension_launch_provider: Arc< - crate::extension_launch_provider::ExtensionLaunchProviderDispatcher, - >, - llm_inference: Option>, - github_telemetry: Option, - github_token_registry: Arc, - ) { + pub(crate) fn ensure_started(&self, client: &Arc) { let mut started = self.started.lock(); if *started { return; } *started = true; + let weak_client = Arc::downgrade(client); + let extension_launch_provider = client.extension_launch_provider.clone(); + let llm_inference = client.llm_inference.get().cloned(); + let github_telemetry = client.on_github_telemetry.clone(); + let github_token_registry = client.github_token_registry.clone(); + let installation_confirmation = client.installation_confirmation.clone(); // Notification routing task let sessions = self.sessions.clone(); - let mut notif_rx = notification_tx.subscribe(); + let mut notif_rx = client.notification_tx.subscribe(); tokio::spawn(async move { loop { match notif_rx.recv().await { @@ -255,12 +300,19 @@ impl SessionRouter { }); // Request routing task (if request_rx is available) - if let Some(mut rx) = request_rx.lock().take() { + if let Some(mut rx) = client.request_rx.lock().take() { let sessions = self.sessions.clone(); tokio::spawn(async move { while let Some(request) = rx.recv().await { + if request.method == crate::installation_confirmation::CONFIRM_METHOD { + installation_confirmation.dispatch(request); + continue; + } if request.method == crate::extension_launch_provider::RESOLVE_METHOD { - extension_launch_provider.dispatch(request).await; + // The host's resolver may take arbitrarily long, so it must + // not hold up routing of later requests. + let provider = extension_launch_provider.clone(); + tokio::spawn(async move { provider.dispatch(request).await }); continue; } if request.method == "gitHubToken.getToken" { @@ -285,27 +337,44 @@ impl SessionRouter { .params .as_ref() .and_then(|p| p.get("sessionId")) - .and_then(|v| v.as_str()); + .and_then(|v| v.as_str()) + .map(str::to_owned); + let Some(session_id) = session_id else { + warn!(method = %request.method, "request missing sessionId"); + send_error( + &weak_client, + request.id, + error_codes::INVALID_PARAMS, + "missing required field: sessionId".to_string(), + ) + .await; + continue; + }; - if let Some(sid) = session_id { - let sender = { - let guard = sessions.lock(); - guard.get(sid).map(|s| s.requests.clone()) - }; - if let Some(sender) = sender { - let _ = sender.send(request); - } else { - warn!( - session_id = sid, - method = %request.method, - "request for unregistered session" - ); - } - } else { + let sender = sessions + .lock() + .get(session_id.as_str()) + .map(|s| s.requests.clone()); + let unrouted = match sender { + Some(sender) => sender.send(request).err().map(|error| error.0), + None => Some(request), + }; + // Every request owes the peer a response. Answer unroutable + // ones (for example, after a `Session` was dropped without + // detaching) so the runtime does not wait for its timeout. + if let Some(request) = unrouted { warn!( + session_id = %session_id, method = %request.method, - "request missing sessionId" + "request for unregistered session" ); + send_error( + &weak_client, + request.id, + error_codes::INTERNAL_ERROR, + format!("Session not found: {session_id}"), + ) + .await; } } }); @@ -313,36 +382,30 @@ impl SessionRouter { } } -#[cfg(test)] -mod tests { - use super::*; - - fn session_id() -> SessionId { - SessionId::new("router-ownership") - } - - #[test] - fn each_registration_gets_a_distinct_token() { - let router = SessionRouter::new(); - let first = router.register(&session_id()); - let second = router.register(&session_id()); - assert_ne!(first.token, second.token); - } - - #[test] - fn unregister_owned_removes_only_the_matching_registration() { - let router = SessionRouter::new(); - let stale = router.register(&session_id()); - let live = router.register(&session_id()); - - // The stale owner must not evict the registration that replaced it. - assert!(!router.unregister_owned(&session_id(), stale.token)); - assert_eq!(router.session_ids(), vec![session_id()]); - - assert!(router.unregister_owned(&session_id(), live.token)); - assert!(router.session_ids().is_empty()); - - // Removing twice is a no-op rather than evicting a future tenant. - assert!(!router.unregister_owned(&session_id(), live.token)); - } +async fn send_error( + client: &Weak, + request_id: u64, + code: i32, + message: String, +) { + let Some(inner) = client.upgrade() else { + return; + }; + // Retire any cancellation registered for a request that never reaches a handler. + drop(inner.rpc.cancellable_requests.claim(request_id)); + let _ = crate::Client::from_inner(inner) + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request_id, + result: None, + error: Some(JsonRpcError { + code, + message, + data: None, + }), + }) + .await; } + +#[cfg(test)] +mod tests; diff --git a/rust/src/router/tests.rs b/rust/src/router/tests.rs new file mode 100644 index 0000000000..f63ca005f7 --- /dev/null +++ b/rust/src/router/tests.rs @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +fn session_id() -> SessionId { + SessionId::new("router-ownership") +} + +#[test] +fn each_registration_gets_a_distinct_token() { + let router = SessionRouter::new(); + let first = router.register(&session_id()); + let second = router.register(&session_id()); + assert_ne!(first.token, second.token); +} + +#[test] +fn unregister_owned_removes_only_the_matching_registration() { + let router = SessionRouter::new(); + let stale = router.register(&session_id()); + let live = router.register(&session_id()); + + // The stale owner must not evict the registration that replaced it. + assert!(!router.unregister_owned(&session_id(), stale.token)); + assert_eq!(router.session_ids(), vec![session_id()]); + + assert!(router.unregister_owned(&session_id(), live.token)); + assert!(router.session_ids().is_empty()); + + // Removing twice is a no-op rather than evicting a future tenant. + assert!(!router.unregister_owned(&session_id(), live.token)); +} + +#[test] +fn restore_owned_hands_the_id_back_to_a_running_replaced_registration() { + let router = SessionRouter::new(); + let resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + assert!(replaced.is_some()); + + assert!(router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.is_registered_owner(&session_id(), resident.token)); +} + +#[test] +fn restore_owned_unregisters_when_the_replaced_session_has_stopped() { + let router = SessionRouter::new(); + let resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + drop(resident); + + assert!(router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.session_ids().is_empty()); +} + +#[test] +fn restore_owned_leaves_a_newer_registration_alone() { + let router = SessionRouter::new(); + let _resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + let newer = router.register(&session_id()); + + assert!(!router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.is_registered_owner(&session_id(), newer.token)); +} diff --git a/rust/src/session.rs b/rust/src/session.rs index 97eb111156..e13ee2309b 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -30,6 +30,7 @@ use crate::handler::{ use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; use crate::session_fs::SessionFsProvider; +use crate::skill_provider::SkillProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ @@ -37,12 +38,13 @@ use crate::types::{ CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride, SessionCapabilities, SessionConfig, SessionEvent, - SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, ToolResult, - ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names, + SessionId, SetModelOptions, SystemMessageConfig, Tool, ToolHandlerMap, ToolInvocation, + ToolResult, ToolResultExpanded, TraceContext, TranscriptRecovery, UiInputOptions, + ensure_attachment_display_names, take_tool_handlers, }; use crate::{ - Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification, - error_codes, + Client, Error, ErrorKind, JsonRpcResponse, ProtocolErrorKind, SessionErrorKind, + SessionEventNotification, error_codes, }; /// Fixed name of the runtime's built-in tool-search tool. A client can replace @@ -91,7 +93,10 @@ pub(crate) struct SessionHandlers { pub user_input: Option>, pub exit_plan_mode: Option>, pub auto_mode_switch: Option>, - pub tools: Arc>>, + pub skill_provider: Option>, + /// Shared with the owning [`Session`], which replaces the map when the + /// runtime accepts a [`Session::set_tools`] call. + pub tools: Arc>, } type PendingExternalTools = Arc>>>; @@ -258,6 +263,11 @@ struct PendingSessionRegistration { session_id: PendingSessionId, shutdown: CancellationToken, external_tools_shutdown: CancellationToken, + startup_tasks: Arc, + /// The resident session a resume displaced. The runtime keeps that + /// session's bindings when the resume fails, so cleanup routes its + /// callbacks back to it; success drops it. + replaced: Option, disarmed: bool, } @@ -290,24 +300,52 @@ impl PendingSessionRegistration { session_id: PendingSessionId::Known(session_id, token), shutdown, external_tools_shutdown, + startup_tasks: Arc::new(StartupTasks::default()), + replaced: None, disarmed: false, } } + fn restoring_on_failure( + mut self, + replaced: Option, + ) -> Self { + self.replaced = replaced; + self + } + /// Guard for a registration whose session ID is assigned by the server. fn deferred( client: Client, stash: Arc>>, shutdown: CancellationToken, external_tools_shutdown: CancellationToken, - ) -> Self { - Self { - client, - session_id: PendingSessionId::Deferred(stash), - shutdown, + ) -> (Self, crate::jsonrpc::InlineResponseCallback) { + let guard = Self { + client: client.clone(), + session_id: PendingSessionId::Deferred(stash.clone()), + shutdown: shutdown.clone(), external_tools_shutdown, + startup_tasks: Arc::new(StartupTasks::default()), + replaced: None, disarmed: false, - } + }; + let callback: crate::jsonrpc::InlineResponseCallback = Box::new(move |response| { + let result = response.result.as_ref().ok_or_else(|| { + Error::with_message(ErrorKind::Json, "session.create response had no result") + })?; + let parsed: CreateSessionResult = serde_json::from_value(result.clone())?; + // Cancellation sets shutdown before inspecting this stash. Under the + // same lock, either registration wins and is cleaned up, or it is rejected. + let mut stashed = stash.lock(); + if shutdown.is_cancelled() { + return Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()); + } + let registration = client.register_session(&parsed.session_id); + *stashed = Some((parsed.session_id, registration)); + Ok(()) + }); + (guard, callback) } fn registered_id(&self) -> Option { @@ -324,13 +362,20 @@ impl PendingSessionRegistration { self.session_id = PendingSessionId::Known(session_id, token); } - async fn cleanup(mut self, event_loop: JoinHandle<()>) { + fn cancel(&self) { self.external_tools_shutdown.cancel(); self.shutdown.cancel(); + // Later callback spawns are rejected without polling their futures. + self.startup_tasks.abort(); + } + + async fn cleanup(mut self, event_loop: JoinHandle<()>) { + self.cancel(); let _ = event_loop.await; if let Some(id) = self.registered_id() { if let PendingSessionId::Known(_, token) = self.session_id { - self.client.unregister_session_owned(&id, token); + self.client + .restore_session_owned(&id, token, self.replaced.take()); } else if let PendingSessionId::Deferred(stash) = &self.session_id && let Some((id, registration)) = stash.lock().as_ref() { @@ -341,6 +386,8 @@ impl PendingSessionRegistration { } fn disarm(&mut self) { + self.startup_tasks.disarm(); + self.replaced = None; self.disarmed = true; } } @@ -348,11 +395,11 @@ impl PendingSessionRegistration { impl Drop for PendingSessionRegistration { fn drop(&mut self) { if !self.disarmed { - self.external_tools_shutdown.cancel(); - self.shutdown.cancel(); + self.cancel(); if let Some(id) = self.registered_id() { if let PendingSessionId::Known(_, token) = self.session_id { - self.client.unregister_session_owned(&id, token); + self.client + .restore_session_owned(&id, token, self.replaced.take()); } else if let PendingSessionId::Deferred(stash) = &self.session_id && let Some((id, registration)) = stash.lock().as_ref() { @@ -377,10 +424,7 @@ struct StartupTasks { } enum StartupTaskState { - Pending { - tasks: Vec>, - nested: Vec, - }, + Pending(Vec), Disarmed, Aborted, } @@ -388,38 +432,18 @@ enum StartupTaskState { impl Default for StartupTasks { fn default() -> Self { Self { - state: ParkingLotMutex::new(StartupTaskState::Pending { - tasks: Vec::new(), - nested: Vec::new(), - }), + state: ParkingLotMutex::new(StartupTaskState::Pending(Vec::new())), } } } impl StartupTasks { - fn track(&self, task: JoinHandle<()>) { - match &mut *self.state.lock() { - StartupTaskState::Pending { tasks, .. } => tasks.push(task), - StartupTaskState::Disarmed => {} - StartupTaskState::Aborted => task.abort(), - } - } - - fn track_nested(&self, task: &JoinHandle) { - match &mut *self.state.lock() { - StartupTaskState::Pending { nested, .. } => nested.push(task.abort_handle()), - StartupTaskState::Disarmed => {} - StartupTaskState::Aborted => task.abort(), - } - } - fn abort(&self) { - if let StartupTaskState::Pending { tasks, nested } = + if let StartupTaskState::Pending(tasks) = std::mem::replace(&mut *self.state.lock(), StartupTaskState::Aborted) { - for handle in nested { - handle.abort(); - } + // Parents register before their nested handlers; cancel them before + // aborting children can wake their JoinHandle waiters. for handle in tasks { handle.abort(); } @@ -431,43 +455,33 @@ impl StartupTasks { } } -fn spawn_startup_tracked( - future: impl std::future::Future + Send + 'static, +fn spawn_startup_tracked( + future: F, startup_tasks: Option<&Arc>, -) { - let task = tokio::spawn(future); - if let Some(startup_tasks) = startup_tasks { - startup_tasks.track(task); - } -} - -struct RunningCreateLoop { - event_loop: Option>, - startup_tasks: Arc, -} - -impl RunningCreateLoop { - fn into_handle(mut self) -> JoinHandle<()> { - self.event_loop - .take() - .expect("running create loop has a handle") +) -> Option> +where + F: std::future::Future + Send + 'static, + F::Output: Send + 'static, +{ + // Hold the cancellation lock across spawn and registration: another worker + // can poll the task immediately, before tokio::spawn returns. + let mut state = startup_tasks.map(|tasks| tasks.state.lock()); + if matches!(state.as_deref(), Some(StartupTaskState::Aborted)) { + return None; } -} - -impl Drop for RunningCreateLoop { - fn drop(&mut self) { - if let Some(event_loop) = self.event_loop.take() { - self.startup_tasks.abort(); - event_loop.abort(); - } + let task = tokio::spawn(future); + if let Some(StartupTaskState::Pending(tasks)) = state.as_deref_mut() { + tasks.push(task.abort_handle()); } + Some(task) } /// A create loop starts before the RPC when the client knows the session ID. /// Server-assigned IDs require waiting for the response to register the loop. +/// The registration guard signals shutdown; inline dispatch never awaits user callbacks. enum CreateEventLoop { Running { - running: RunningCreateLoop, + event_loop: JoinHandle<()>, token: crate::router::RegistrationToken, }, Deferred(EventLoopSpawner), @@ -476,11 +490,8 @@ enum CreateEventLoop { impl CreateEventLoop { async fn cleanup(self, pending_registration: PendingSessionRegistration) { match self { - Self::Running { running, .. } => { - pending_registration.shutdown.cancel(); - // User handlers may not terminate; abort them without waiting on their futures. - running.startup_tasks.abort(); - pending_registration.cleanup(running.into_handle()).await; + Self::Running { event_loop, .. } => { + pending_registration.cleanup(event_loop).await; } Self::Deferred(_) => drop(pending_registration), } @@ -500,6 +511,8 @@ impl CreateEventLoop { /// without calling `destroy`, the `Drop` impl aborts the event loop and /// 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, @@ -527,6 +540,13 @@ pub struct Session { /// Cancels only host-owned external tool callbacks. Disconnect signals this /// before the destroy RPC without stopping unrelated event delivery. external_tools_shutdown: CancellationToken, + /// This client's tool handlers, shared with the event loop's + /// `external_tool.requested` dispatch. + tool_handlers: Arc>, + /// Held for each [`Session::set_tools`] request until the runtime answers, + /// so replacements reach the runtime, and replace the handlers, one at a + /// time. + set_tools_lock: Arc>, /// Only populated while a `send_and_wait` call is in flight. /// /// Sync `parking_lot::Mutex` because the lock is never held across an @@ -550,6 +570,45 @@ pub struct Session { } impl Session { + pub(crate) fn validate_ahp_handoff( + &self, + client: &Client, + expected: &std::collections::HashMap, + resume: bool, + ) -> Result<(), Error> { + let invalid = || { + Error::with_message( + ErrorKind::InvalidConfig, + "AHP callback must return the requested session from this client and preserve its configuration", + ) + }; + if !Arc::ptr_eq(&self.client.inner, &client.inner) + || self.shutdown.is_cancelled() + || !client + .inner + .router + .is_registered_owner(&self.id, self.registration_token) + || expected + .get("sessionId") + .and_then(serde_json::Value::as_str) + != Some(self.id.as_str()) + { + return Err(invalid()); + } + let actual = self.ahp_creation_config.lock().take(); + // A retained original has no new materialization snapshot. The runtime + // validates its resident workspace without replacing application options. + if !(resume && actual.is_none() + || crate::ahp_host::contains_settings( + actual.as_ref(), + &serde_json::to_value(expected)?, + )) + { + return Err(invalid()); + } + Ok(()) + } + /// Session ID assigned by the CLI. pub fn id(&self) -> &SessionId { &self.id @@ -565,6 +624,11 @@ impl Session { self.workspace_path.as_deref() } + /// Transcript repair proposed when this session was resumed, if any. + pub fn transcript_recovery(&self) -> Option<&TranscriptRecovery> { + self.transcript_recovery.as_ref() + } + /// Remote session URL, if the session is running remotely. pub fn remote_url(&self) -> Option<&str> { self.remote_url.as_deref() @@ -1097,6 +1161,115 @@ impl Session { .await } + /// Replace the tools this client supplies to the session. + /// + /// `tools` becomes the complete set of externally implemented tools this + /// client supplies, replacing the set from [`SessionConfig::with_tools`], + /// [`ResumeSessionConfig::with_tools`], or a previous call. Built-in, MCP, + /// plugin, and extension tools, and tools that other clients connected to + /// the session supply, are unaffected. Pass an empty collection to remove + /// all of this client's tools. + /// + /// As at startup, this session dispatches calls to tools that carry a + /// [handler](Tool::with_handler), and advertises declaration-only tools for + /// another client to service. + /// + /// The new handlers take effect as soon as the runtime accepts the + /// replacement: from then on, every tool request this session dispatches + /// uses them, including requests the runtime sent before it accepted. + /// Calls already running finish on the handlers that started them. If the + /// runtime rejects the replacement, nothing changes. Concurrent calls on + /// the same session are applied one at a time, in the order they start. + /// + /// The runtime offers the new tools from the agent's next model request, + /// which can fall within a turn in progress. A model request already in + /// flight was made with the previous tools, so the agent can still call a + /// tool you removed. This session doesn't answer that call, and it can stay + /// pending until the turn is aborted. If a running turn might still call a + /// tool you remove, replace tools while the session is idle. + /// + /// **Experimental.** Wraps the experimental `session.tools.set` RPC, which + /// requires a runtime that supports it. + /// + /// # Errors + /// + /// Returns [`ErrorKind::InvalidConfig`], without contacting the runtime, if + /// two tools carry handlers under the same name. The runtime rejects invalid + /// tool names and names that another connected client already supplies. + /// + /// # Cancel safety + /// + /// **Cancel-safe.** Dropping this future while an earlier replacement is + /// still in flight sends nothing. Once the request starts, it runs to + /// completion in its own task. Dropping this future then doesn't abandon + /// the replacement: an accepted replacement still takes effect, and later + /// calls still wait for the runtime's answer. Only the result is lost. + /// + /// # Example + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use github_copilot_sdk::Tool; + /// # use github_copilot_sdk::tool::ToolHandler; + /// # async fn example( + /// # session: github_copilot_sdk::session::Session, + /// # search_issues: Arc, + /// # ) -> Result<(), github_copilot_sdk::Error> { + /// session + /// .set_tools([Tool::new("search_issues") + /// .with_description("Search the issues shown on the current page") + /// .with_handler(search_issues)]) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn set_tools>(&self, tools: I) -> Result<(), Error> { + let mut tools: Vec = tools.into_iter().collect(); + let handlers = take_tool_handlers(&mut tools)?; + let mut params = serde_json::json!({ "sessionId": self.id }); + params["tools"] = serde_json::to_value(&tools)?; + + let set_tools_lock = self.set_tools_lock.clone().lock_owned().await; + let installed = self.tool_handlers.clone(); + let replaced = Arc::new(ParkingLotMutex::new(None)); + // Swapping on the read task means no tool request read after the + // runtime accepted is dispatched to the previous handlers. + let accepted: crate::jsonrpc::InlineResponseCallback = Box::new({ + let replaced = replaced.clone(); + move |_| { + let previous = std::mem::replace(&mut *installed.write(), handlers); + *replaced.lock() = Some(previous); + Ok(()) + } + }); + let request = self.client.call_with_inline_callback( + rpc_methods::SESSION_TOOLS_SET, + Some(params), + Some(accepted), + ); + let span = tracing::error_span!("set_tools", session_id = %self.id); + // The task owns the request and the lock, so even if this future is + // dropped, an accepted replacement still takes effect and the next + // call waits for the runtime's answer. + let replacement = tokio::spawn( + async move { + let _set_tools_lock = set_tools_lock; + let result = request.await; + // Run the replaced handlers' destructors here rather than on + // the read task, which every session on this client shares. + let previous = replaced.lock().take(); + drop(previous); + result + } + .instrument(span), + ); + match replacement.await { + Ok(result) => result.map(|_| ()), + Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()), + Err(_) => Err(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled).into()), + } + } + /// Disconnect this session from the CLI. /// /// Sends the `session.detach` RPC, stops the event loop, and unregisters @@ -1504,6 +1677,22 @@ impl Client { shutdown: CancellationToken, ) -> Result { let total_start = Instant::now(); + if config.cloud.is_some() && config.skill_provider.is_some() { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "Skill providers are not supported for cloud sessions.", + )); + } + let ahp_creation_config = if self + .inner + .ahp_host_sessions + .upgrade() + .is_some_and(|sessions| sessions.expects_session(config.session_id.as_ref())) + { + Some(crate::ahp_host::config_for_host(&config)?) + } else { + None + }; // Non-cloud IDs are generated locally when omitted by the caller. // Start the loop before the RPC: session.create may issue // sessionFs.writeFile for workspace metadata before its response. @@ -1588,6 +1777,9 @@ impl Client { runtime.permission_handler.take(), runtime.permission_policy.take(), ); + let tool_handlers = Arc::new(parking_lot::RwLock::new(std::mem::take( + &mut runtime.tool_handlers, + ))); let handlers = SessionHandlers { permission: permission_handler, managed_settings_enabled: has_managed_settings( @@ -1599,7 +1791,8 @@ impl Client { user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), - tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)), + skill_provider: runtime.skill_provider.take(), + tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); let transforms = runtime.system_message_transform.take(); @@ -1631,6 +1824,15 @@ impl Client { does not implement SessionFsSqliteProvider", )); } + if self.inner.session_fs_binary_declared + && let Some(ref provider) = session_fs_provider + && provider.binary().is_none() + { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "SessionFs capabilities declare binary support but the provider does not implement SessionFsBinaryProvider", + )); + } let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; @@ -1643,7 +1845,7 @@ impl Client { let external_tools_shutdown = self.inner.rpc.connection_closed_token(); let spawn_loop: EventLoopSpawner = { - let client = self.clone(); + let client = Client::from_inner(self.inner.clone()); let idle_waiter = idle_waiter.clone(); let capabilities = capabilities.clone(); let open_canvases = open_canvases.clone(); @@ -1684,12 +1886,6 @@ impl Client { if let Some(ref sid) = local_session_id { let registration = self.register_session(sid); let token = registration.token; - let startup_tasks = Arc::new(StartupTasks::default()); - let event_loop = spawn_loop( - sid.clone(), - registration.channels, - Some(startup_tasks.clone()), - ); let guard = PendingSessionRegistration::new( self.clone(), sid.clone(), @@ -1697,54 +1893,20 @@ impl Client { shutdown.clone(), external_tools_shutdown.clone(), ); + let event_loop = spawn_loop( + sid.clone(), + registration.channels, + Some(guard.startup_tasks.clone()), + ); ( None, guard, - CreateEventLoop::Running { - running: RunningCreateLoop { - event_loop: Some(event_loop), - startup_tasks, - }, - token, - }, + CreateEventLoop::Running { event_loop, token }, None, ) } else { let inline_stash = Arc::new(ParkingLotMutex::new(None)); - let client = self.clone(); - let stash = inline_stash.clone(); - let expected = caller_session_id.clone(); - let callback: crate::jsonrpc::InlineResponseCallback = Box::new(move |response| { - let result = response.result.as_ref().ok_or_else(|| { - Error::with_message( - ErrorKind::Json, - "session.create response had no result", - ) - })?; - let parsed: CreateSessionResult = - serde_json::from_value(result.clone()).map_err(Error::from)?; - if let Some(requested) = expected.as_ref() - && parsed.session_id != *requested - { - return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch { - requested: requested.clone(), - returned: parsed.session_id, - }) - .into()); - } - // Register and stash under a single stash-lock hold. The - // cancellation guard identifies the session to unregister by - // peeking this stash, so registering outside the lock would - // leave a window where a concurrent guard drop (caller - // cancellation) sees `None` and leaks the registration. - // `register_session` takes the router lock, never the stash - // lock, so there is no lock-order inversion here. - let mut stashed = stash.lock(); - let registration = client.register_session(&parsed.session_id); - *stashed = Some((parsed.session_id, registration)); - Ok(()) - }); - let guard = PendingSessionRegistration::deferred( + let (guard, callback) = PendingSessionRegistration::deferred( self.clone(), inline_stash.clone(), shutdown.clone(), @@ -1793,13 +1955,8 @@ impl Client { } let (session_id, event_loop, registration_token) = match event_loop { - CreateEventLoop::Running { running, token } => { - running.startup_tasks.disarm(); - ( - create_result.session_id.clone(), - running.into_handle(), - token, - ) + CreateEventLoop::Running { event_loop, token } => { + (create_result.session_id.clone(), event_loop, token) } CreateEventLoop::Deferred(spawn_loop) => { let (session_id, registration) = inline_stash @@ -1808,7 +1965,11 @@ impl Client { .take() .expect("session registration must have populated stash on success"); pending_registration.resolve_to(session_id.clone(), registration.token); - let event_loop = spawn_loop(session_id.clone(), registration.channels, None); + let event_loop = spawn_loop( + session_id.clone(), + registration.channels, + Some(pending_registration.startup_tasks.clone()), + ); (session_id, event_loop, registration.token) } }; @@ -1833,16 +1994,19 @@ impl Client { session_id = %session_id, "Client::create_session complete" ); - pending_registration.disarm(); 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, remote_url: create_result.remote_url, - client: self.clone(), + client: Client::from_inner(self.inner.clone()), event_loop: ParkingLotMutex::new(Some(event_loop)), shutdown, external_tools_shutdown, + tool_handlers, + set_tools_lock: Arc::default(), idle_waiter, capabilities, open_canvases, @@ -1851,22 +2015,15 @@ impl Client { github_token_registration: ParkingLotMutex::new(github_token_registration), registration_token, }; - apply_mode_post_create_patch( - &session, + let patch = build_mode_post_create_patch( mode, opt_skip_custom_instructions, opt_custom_agents_local_only, opt_coauthor_enabled, opt_manage_schedule_enabled, opt_included_builtin_skills, - ) - .await?; - if let Some(registration) = session.github_token_registration.lock().as_ref() { - registration.claim(session.id.clone()); - } else { - self.retire_github_token_provider(&session.id); - } - Ok(session) + ); + finish_session_setup(session, pending_registration, patch).await } /// Resume an existing session on the CLI. @@ -1886,6 +2043,17 @@ 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 + .upgrade() + .is_some_and(|sessions| sessions.expects_session(Some(&config.session_id))) + { + Some(crate::ahp_host::resume_config_for_host(&config)?) + } else { + None + }; let session_id = config.session_id.clone(); if config.hooks_handler.is_some() && config.hooks.is_none() { config.hooks = Some(true); @@ -1893,7 +2061,6 @@ impl Client { if let Some(transforms) = config.system_message_transform.clone() { inject_transform_sections_resume(&mut config, transforms.as_ref()); } - let mode = self.inner.mode; if mode == crate::ClientMode::Empty && config.available_tools.is_none() { return Err(Error::with_message( ErrorKind::InvalidConfig, @@ -1956,6 +2123,9 @@ impl Client { runtime.permission_handler.take(), runtime.permission_policy.take(), ); + let tool_handlers = Arc::new(parking_lot::RwLock::new(std::mem::take( + &mut runtime.tool_handlers, + ))); let handlers = SessionHandlers { permission: permission_handler, managed_settings_enabled: has_managed_settings( @@ -1967,7 +2137,8 @@ impl Client { user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), - tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)), + skill_provider: runtime.skill_provider.take(), + tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); let transforms = runtime.system_message_transform.take(); @@ -1999,6 +2170,15 @@ impl Client { does not implement SessionFsSqliteProvider", )); } + if self.inner.session_fs_binary_declared + && let Some(ref provider) = session_fs_provider + && provider.binary().is_none() + { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "SessionFs capabilities declare binary support but the provider does not implement SessionFsBinaryProvider", + )); + } let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; @@ -2009,15 +2189,23 @@ impl Client { // Active prepared subscribers retain their existing bounded delivery. let resume_bootstrap = (event_tx.receiver_count() == 0) .then(|| crate::subscription::ResumeBootstrap::new(&event_tx)); - let registration = self.register_session(&session_id); + let (registration, replaced) = self.replace_session_registration(&session_id); let registration_token = registration.token; let channels = registration.channels; let idle_waiter = Arc::new(ParkingLotMutex::new(None)); let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new())); let external_tools_shutdown = self.inner.rpc.connection_closed_token(); + let registration = PendingSessionRegistration::new( + self.clone(), + session_id.clone(), + registration_token, + shutdown.clone(), + external_tools_shutdown.clone(), + ) + .restoring_on_failure(replaced); let event_loop = spawn_event_loop( session_id.clone(), - self.clone(), + Client::from_inner(self.inner.clone()), handlers, hooks, transforms, @@ -2033,14 +2221,7 @@ impl Client { resume_bootstrap.clone(), shutdown.clone(), external_tools_shutdown.clone(), - None, - ); - let mut registration = PendingSessionRegistration::new( - self.clone(), - session_id.clone(), - registration_token, - shutdown.clone(), - external_tools_shutdown.clone(), + Some(registration.startup_tasks.clone()), ); tracing::debug!( elapsed_ms = setup_start.elapsed().as_millis(), @@ -2130,16 +2311,19 @@ impl Client { session_id = %session_id, "Client::resume_session complete" ); - registration.disarm(); 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, remote_url: resume_result.remote_url, - client: self.clone(), + client: Client::from_inner(self.inner.clone()), event_loop: ParkingLotMutex::new(Some(event_loop)), shutdown, external_tools_shutdown, + tool_handlers, + set_tools_lock: Arc::default(), idle_waiter, capabilities, open_canvases, @@ -2148,22 +2332,15 @@ impl Client { github_token_registration: ParkingLotMutex::new(github_token_registration), registration_token, }; - apply_mode_post_create_patch( - &session, + let patch = build_mode_post_create_patch( mode, opt_skip_custom_instructions, opt_custom_agents_local_only, opt_coauthor_enabled, opt_manage_schedule_enabled, opt_included_builtin_skills, - ) - .await?; - if let Some(registration) = session.github_token_registration.lock().as_ref() { - registration.claim(session.id.clone()); - } else { - self.retire_github_token_provider(&session.id); - } - Ok(session) + ); + finish_session_setup(session, registration, patch).await } } @@ -2189,9 +2366,11 @@ impl Client { /// * Dropping the [`start`](Self::start) future mid-flight cancels the /// session token, unregisters the session from the router if it was /// registered, and closes early subscriptions. A retry with the same -/// session ID succeeds. Cleanup of already-spawned tasks is signalled, -/// not awaited: `Drop` is synchronous and cannot await, so the event loop -/// terminates promptly but not synchronously. +/// session ID succeeds. Callbacks spawned during startup are aborted until +/// all post-create/resume setup succeeds. After success, callbacks retain +/// their established-session lifetime and may outlive the event loop. +/// Cleanup is signalled, not awaited: `Drop` is synchronous and cannot await, +/// so the event loop and callbacks terminate promptly but not synchronously. /// * A startup error from [`start`](Self::start) performs the same cleanup /// and preserves the [`ErrorKind`] the equivalent /// [`Client::create_session`] / [`Client::resume_session`] call has always @@ -2299,16 +2478,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 } } } @@ -2316,30 +2494,25 @@ impl PreparedSession { type CommandHandlerMap = HashMap>; -async fn apply_mode_post_create_patch( - session: &Session, - mode: crate::ClientMode, - opt_skip_custom_instructions: Option, - opt_custom_agents_local_only: Option, - opt_coauthor_enabled: Option, - opt_manage_schedule_enabled: Option, - opt_included_builtin_skills: Option>, -) -> Result<(), Error> { - let Some(patch) = build_mode_post_create_patch( - mode, - opt_skip_custom_instructions, - opt_custom_agents_local_only, - opt_coauthor_enabled, - opt_manage_schedule_enabled, - opt_included_builtin_skills, - ) else { - return Ok(()); - }; - if let Err(error) = session.rpc().options().update(patch).await { +async fn finish_session_setup( + session: Session, + mut registration: PendingSessionRegistration, + patch: Option, +) -> Result { + if let Some(patch) = patch + && let Err(error) = session.rpc().options().update(patch).await + { + registration.cancel(); let _ = session.disconnect().await; return Err(error); } - Ok(()) + if let Some(provider) = session.github_token_registration.lock().as_ref() { + provider.claim(session.id.clone()); + } else { + session.client.retire_github_token_provider(&session.id); + } + registration.disarm(); + Ok(session) } /// Builds the `session.options.update` patch applied immediately after a session @@ -2487,8 +2660,8 @@ fn spawn_event_loop( // `handle_notification` is awaited inline because it only // performs fast dispatch work; its slow interactive callbacks // (permission/tool/elicitation) are themselves spawned as child - // tasks. All of these spawned tasks intentionally outlive the - // parent loop and own their own cleanup — RFD 400's "spawn + // tasks. After successful startup these tasks intentionally + // outlive the parent loop and own their own cleanup — RFD 400's "spawn // background tasks to perform cancel-unsafe operations" pattern. tokio::select! { _ = shutdown.cancelled() => break, @@ -2512,7 +2685,7 @@ fn spawn_event_loop( let bearer_token_providers = bearer_token_providers.clone(); let request_id = request.id; let method = request.method.clone(); - spawn_startup_tracked( + let _ = spawn_startup_tracked( async move { let ctx = RequestDispatchContext { client: &client, @@ -2831,7 +3004,7 @@ async fn handle_notification( session_id = %sid, request_id = %request_id ); - spawn_startup_tracked( + let _ = spawn_startup_tracked( async move { let handler_start = Instant::now(); let result = permission_handler @@ -2905,7 +3078,7 @@ async fn handle_notification( session_id = %sid, request_id = %request_id ); - spawn_startup_tracked( + let _ = spawn_startup_tracked( async move { let rpc_start = Instant::now(); let _ = client @@ -2937,7 +3110,7 @@ async fn handle_notification( let tool_handler = if data.tool_name.is_empty() { None } else { - handlers.tools.get(&data.tool_name).cloned() + handlers.tools.read().get(&data.tool_name).cloned() }; let Some(tool_handler) = tool_handler else { return; @@ -2960,7 +3133,7 @@ async fn handle_notification( session_id = %sid, request_id = %request_id ); - spawn_startup_tracked( + let _ = spawn_startup_tracked( async move { let guard = PendingExternalToolGuard { request_id: guard_request_id, @@ -3133,41 +3306,47 @@ async fn handle_notification( request_id = %request_id ); let nested_startup_tasks = startup_tasks.cloned(); - spawn_startup_tracked( + let _ = spawn_startup_tracked( async move { let cancel = ElicitationResult { action: "cancel".to_string(), content: None, }; // Dispatch to a nested task so panics are caught as JoinErrors. - let handler_task = tokio::spawn({ - let sid = sid.clone(); - let request_id = request_id.clone(); - let span = tracing::error_span!( - "elicitation_callback", - session_id = %sid, - request_id = %request_id - ); - async move { - let handler_start = Instant::now(); - let response = elicitation_handler - .handle(sid.clone(), request_id.clone(), request) - .await; - tracing::debug!( - elapsed_ms = handler_start.elapsed().as_millis(), + let handler_task = spawn_startup_tracked( + { + let sid = sid.clone(); + let request_id = request_id.clone(); + let span = tracing::error_span!( + "elicitation_callback", session_id = %sid, - request_id = %request_id, - "ElicitationHandler::handle dispatch" + request_id = %request_id ); - response - } - .instrument(span) - }); - if let Some(tasks) = nested_startup_tasks.as_ref() { - tasks.track_nested(&handler_task); - } + async move { + let handler_start = Instant::now(); + let response = elicitation_handler + .handle(sid.clone(), request_id.clone(), request) + .await; + tracing::debug!( + elapsed_ms = handler_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + "ElicitationHandler::handle dispatch" + ); + response + } + .instrument(span) + }, + nested_startup_tasks.as_ref(), + ); + let Some(handler_task) = handler_task else { + // Abandoned startup must not emit even a fallback cancellation RPC. + return; + }; let result = match handler_task.await { Ok(r) => r, + // Startup abort is not a handler failure: no fallback RPC. + Err(error) if error.is_cancelled() => return, Err(_) => cancel.clone(), }; let rpc_start = Instant::now(); @@ -3244,37 +3423,43 @@ async fn handle_notification( request_id = %request_id ); let nested_startup_tasks = startup_tasks.cloned(); - spawn_startup_tracked( + let _ = spawn_startup_tracked( async move { let cancel = McpAuthResult::Cancelled; - let handler_task = tokio::spawn({ - let sid = sid.clone(); - let request_id = request_id.clone(); - let span = tracing::error_span!( - "mcp_auth_callback", - session_id = %sid, - request_id = %request_id - ); - async move { - let handler_start = Instant::now(); - let response = mcp_auth_handler - .handle(sid.clone(), request_id.clone(), request) - .await; - tracing::debug!( - elapsed_ms = handler_start.elapsed().as_millis(), + let handler_task = spawn_startup_tracked( + { + let sid = sid.clone(); + let request_id = request_id.clone(); + let span = tracing::error_span!( + "mcp_auth_callback", session_id = %sid, - request_id = %request_id, - "McpAuthHandler::handle dispatch" + request_id = %request_id ); - response - } - .instrument(span) - }); - if let Some(tasks) = nested_startup_tasks.as_ref() { - tasks.track_nested(&handler_task); - } + async move { + let handler_start = Instant::now(); + let response = mcp_auth_handler + .handle(sid.clone(), request_id.clone(), request) + .await; + tracing::debug!( + elapsed_ms = handler_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + "McpAuthHandler::handle dispatch" + ); + response + } + .instrument(span) + }, + nested_startup_tasks.as_ref(), + ); + let Some(handler_task) = handler_task else { + // Abandoned startup must not emit even a fallback cancellation RPC. + return; + }; let result = match handler_task.await { Ok(result) => result, + // Startup abort is not a handler failure: no fallback RPC. + Err(error) if error.is_cancelled() => return, Err(_) => cancel, }; let rpc_start = Instant::now(); @@ -3310,7 +3495,7 @@ async fn handle_notification( let command_handlers = command_handlers.clone(); let sid = session_id.clone(); let span = tracing::error_span!("command_handler", session_id = %sid); - spawn_startup_tracked( + let _ = spawn_startup_tracked( async move { let request_id = data.request_id; let ack_error = match command_handlers.get(&data.command_name).cloned() { @@ -3374,6 +3559,110 @@ struct RequestDispatchContext<'a> { bearer_token_providers: &'a HashMap>, } +/// Serve one `skillProvider.*` callback. Provider failures are reported +/// generically so their details never reach the runtime or the model. +/// +/// A `$/cancelRequest` for this request, or the connection closing, drops the +/// provider future. +async fn handle_skill_provider_request( + client: &Client, + session_id: &SessionId, + provider: Option<&Arc>, + request: &crate::JsonRpcRequest, +) { + let Some(pending) = client.inner.rpc.cancellable_requests.claim(request.id) else { + // The connection closed before dispatch, so nobody awaits a response. + return; + }; + let Some(provider) = provider else { + let _ = send_error_response( + client, + request.id, + error_codes::INTERNAL_ERROR, + &format!("No skill provider for session: {session_id}"), + ) + .await; + return; + }; + + let read_name = if request.method == rpc_methods::SKILLPROVIDER_READ { + let Some(name) = request + .params + .as_ref() + .and_then(|params| params.get("name")) + .and_then(Value::as_str) + else { + let _ = send_error_response( + client, + request.id, + error_codes::INVALID_PARAMS, + "missing required field: name", + ) + .await; + return; + }; + Some(name) + } else { + None + }; + let operation = if read_name.is_some() { + "readSkill" + } else { + "listSkills" + }; + let call = async { + match read_name { + Some(name) => provider + .read_skill(name) + .await + .map(|markdown| serde_json::json!({ "markdown": markdown })), + None => provider + .list_skills() + .await + .map(|skills| serde_json::json!({ "skills": skills })), + } + }; + + let connection_closed = client.inner.rpc.connection_closed_token(); + let result = tokio::select! { + biased; + _ = connection_closed.cancelled() => return, + _ = pending.cancellation().cancelled() => { + let _ = send_error_response( + client, + request.id, + error_codes::REQUEST_CANCELLED, + &format!("Skill provider {operation} cancelled"), + ) + .await; + return; + } + result = call => result, + }; + + match result { + Ok(result) => { + let response = JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: Some(result), + error: None, + }; + let _ = client.send_response(&response).await; + } + Err(error) => { + warn!(error = %error, operation, "skill provider callback failed"); + let _ = send_error_response( + client, + request.id, + error_codes::INTERNAL_ERROR, + &format!("Skill provider {operation} failed"), + ) + .await; + } + } +} + /// Process a JSON-RPC request from the CLI. async fn handle_request( session_id: &SessionId, @@ -3404,6 +3693,14 @@ async fn handle_request( return; } + if request.method == rpc_methods::SKILLPROVIDER_LIST + || request.method == rpc_methods::SKILLPROVIDER_READ + { + handle_skill_provider_request(client, &sid, handlers.skill_provider.as_ref(), &request) + .await; + return; + } + match request.method.as_str() { "hooks.invoke" => { let params = request.params.as_ref(); @@ -3687,308 +3984,7 @@ fn inject_transform_sections_resume( } #[cfg(test)] -mod tests { - use serde_json::json; - - use super::{ - build_mode_post_create_patch, has_managed_settings, is_autopilot_continuation_idle, - permission_request_data, permission_response_params, - }; - use crate::handler::PermissionResult; - use crate::types::{ - PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, - PermissionDecisionSurface, RequestId, SessionEvent, SessionId, - }; - - #[test] - fn identifies_only_autopilot_continuation_idles() { - let mut event = SessionEvent { - id: "event-1".to_string(), - timestamp: "2026-01-01T00:00:00Z".to_string(), - parent_id: None, - ephemeral: None, - agent_id: None, - debug_cli_received_at_ms: None, - debug_ws_forwarded_at_ms: None, - event_type: "session.idle".to_string(), - data: json!({ "mode": "autopilot" }), - }; - - assert!(is_autopilot_continuation_idle(&event)); - - event.data = json!({ "mode": "interactive" }); - assert!(!is_autopilot_continuation_idle(&event)); - - event.data = json!({}); - assert!(!is_autopilot_continuation_idle(&event)); - } - - #[test] - fn empty_mode_post_patch_sets_empty_included_builtin_skills() { - let patch = - build_mode_post_create_patch(crate::ClientMode::Empty, None, None, None, None, None) - .expect("empty mode always sends a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(Vec::new()), - "empty mode must fail closed with an empty includedBuiltinSkills list" - ); - assert_eq!(patch.installed_plugins.as_ref().map(|p| p.len()), Some(0)); - // Serializes as an explicit empty array (not omitted). - let value = serde_json::to_value(&patch).expect("serialize patch"); - assert_eq!(value["includedBuiltinSkills"], serde_json::json!([])); - } - - #[test] - fn empty_mode_post_patch_preserves_explicit_builtin_skill_allowlist() { - let patch = build_mode_post_create_patch( - crate::ClientMode::Empty, - Some(false), - Some(false), - Some(true), - Some(true), - Some(vec!["code-review".to_string()]), - ) - .expect("empty mode always sends a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(vec!["code-review".to_string()]) - ); - } - - #[test] - fn copilot_cli_mode_does_not_inject_included_builtin_skills() { - // No fields set -> no patch at all. - assert!( - build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - None, - None, - None, - None, - None - ) - .is_none() - ); - // A field set -> patch sent, but skills field stays absent. - let patch = build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - Some(true), - None, - None, - None, - None, - ) - .expect("a set field triggers a patch"); - assert_eq!(patch.included_builtin_skills, None); - assert!(patch.installed_plugins.is_none()); - let value = serde_json::to_value(&patch).expect("serialize patch"); - assert!(value.get("includedBuiltinSkills").is_none()); - - let patch = build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - None, - None, - None, - None, - Some(vec!["code-review".to_string()]), - ) - .expect("an explicit allowlist triggers a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(vec!["code-review".to_string()]) - ); - } - - #[test] - fn direct_injection_enables_managed_safeguards() { - let settings = crate::types::ManagedSettings::default(); - assert!(has_managed_settings(None, Some(&settings))); - assert!(!has_managed_settings(None, None)); - } - - fn attribution_context() -> PermissionDecisionContext { - PermissionDecisionContext { - outcome: PermissionDecisionOutcome::AutoApproved, - response_capability: None, - source: PermissionDecisionSource::AssistedApproval, - surface: PermissionDecisionSurface::CopilotApp, - } - } - - #[test] - fn response_params_omit_decision_context_without_attribution() { - for (result, expected) in [ - ( - PermissionResult::approve_once(), - json!({ "kind": "approve-once" }), - ), - (PermissionResult::reject(None), json!({ "kind": "reject" })), - ( - PermissionResult::reject(Some("bad".to_string())), - json!({ "kind": "reject", "feedback": "bad" }), - ), - ( - PermissionResult::user_not_available(), - json!({ "kind": "user-not-available" }), - ), - ] { - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &result, - ) - .unwrap(); - assert_eq!( - params, - json!({ - "sessionId": "session-1", - "requestId": "permission-1", - "result": expected, - }) - ); - } - } - - #[test] - fn response_params_forward_decision_context_alongside_result() { - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &PermissionResult::approve_once().with_context(attribution_context()), - ) - .unwrap(); - assert_eq!( - params, - json!({ - "sessionId": "session-1", - "requestId": "permission-1", - "result": { "kind": "approve-once" }, - "decisionContext": { - "outcome": "auto_approved", - "source": "assisted_approval", - "surface": "copilot_app", - }, - }) - ); - // The context is a sibling of `result`, never nested inside it. - assert!(params["result"].get("decisionContext").is_none()); - } - - #[test] - fn response_params_suppressed_for_no_result() { - assert!( - permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &PermissionResult::NoResult, - ) - .is_none() - ); - } - - #[test] - fn with_context_is_a_no_op_on_no_result() { - let result = PermissionResult::no_result().with_context(attribution_context()); - assert!(matches!(result, PermissionResult::NoResult)); - } +mod tests; - #[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)); - } -} +#[cfg(test)] +mod startup_tasks_tests; diff --git a/rust/src/session/startup_tasks_tests.rs b/rust/src/session/startup_tasks_tests.rs new file mode 100644 index 0000000000..5bc14f3287 --- /dev/null +++ b/rust/src/session/startup_tasks_tests.rs @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![cfg(test)] + +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use tokio::io::AsyncReadExt; +use tokio::sync::oneshot; +use tokio::time::timeout; + +use super::{ + PendingSessionRegistration, SessionHandlers, StartupTaskState, StartupTasks, + spawn_startup_tracked, +}; + +struct PendingNestedCallback(tokio::sync::mpsc::UnboundedSender<()>); + +#[async_trait] +impl crate::handler::ElicitationHandler for PendingNestedCallback { + async fn handle( + &self, + _session_id: crate::SessionId, + _request_id: crate::RequestId, + _request: crate::types::ElicitationRequest, + ) -> crate::types::ElicitationResult { + self.0.send(()).unwrap(); + std::future::pending().await + } +} + +#[async_trait] +impl crate::handler::McpAuthHandler for PendingNestedCallback { + async fn handle( + &self, + _session_id: crate::SessionId, + _request_id: crate::RequestId, + _request: crate::handler::McpAuthRequest, + ) -> crate::handler::McpAuthResult { + self.0.send(()).unwrap(); + std::future::pending().await + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn aborted_nested_dispatches_terminate_before_wire_fence() { + check_aborted_nested_dispatches(false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn cancelled_nested_handlers_do_not_send_fallback_before_outer_abort() { + check_aborted_nested_dispatches(true).await; +} + +async fn check_aborted_nested_dispatches(nested_first: bool) { + let (client_write, mut server_read) = tokio::io::duplex(8192); + let (_server_write, client_read) = tokio::io::duplex(8192); + let client = + crate::Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + let (entered_tx, mut entered_rx) = tokio::sync::mpsc::unbounded_channel(); + let handler = Arc::new(PendingNestedCallback(entered_tx)); + let handlers = SessionHandlers { + permission: None, + managed_settings_enabled: false, + elicitation: Some(handler.clone()), + mcp_auth: Some(handler), + user_input: None, + exit_plan_mode: None, + auto_mode_switch: None, + skill_provider: None, + tools: Arc::new(Default::default()), + }; + let startup_tasks = Arc::new(StartupTasks::default()); + let shutdown = tokio_util::sync::CancellationToken::new(); + let external_tools_shutdown = tokio_util::sync::CancellationToken::new(); + let (event_tx, _) = tokio::sync::broadcast::channel(8); + let session_id = crate::SessionId::new("nested-shutdown"); + for (event_type, data) in [ + ( + "elicitation.requested", + serde_json::json!({ + "requestId": "nested-elicitation", "message": "Confirm" + }), + ), + ( + "mcp.oauth_required", + serde_json::json!({ + "requestId": "nested-oauth", "reason": "initial", + "serverName": "test-server", "serverUrl": "https://example.com/mcp" + }), + ), + ] { + let notification = serde_json::from_value(serde_json::json!({ + "sessionId": session_id, + "event": { + "id": event_type, "timestamp": "2026-09-25T00:00:00Z", + "type": event_type, "data": data + } + })) + .unwrap(); + super::handle_notification( + &session_id, + &client, + &handlers, + &Arc::new(Default::default()), + notification, + &Arc::new(parking_lot::Mutex::new(None)), + &Arc::new(parking_lot::RwLock::new(Default::default())), + &Arc::new(parking_lot::RwLock::new(Vec::new())), + &event_tx, + None, + &shutdown, + &external_tools_shutdown, + &Arc::new(parking_lot::Mutex::new(Default::default())), + Some(&startup_tasks), + ) + .await; + timeout(Duration::from_secs(2), entered_rx.recv()) + .await + .unwrap() + .unwrap(); + } + let tasks = match &*startup_tasks.state.lock() { + StartupTaskState::Pending(tasks) => tasks.clone(), + _ => panic!("startup must still own the callback tasks"), + }; + assert_eq!( + tasks.len(), + 4, + "both outer dispatches and nested callbacks must be tracked" + ); + let late_reply = if nested_first { + // Freeze cleanup after aborting the nested callbacks but before aborting + // their parents, deterministically exposing the fallback-response race. + *startup_tasks.state.lock() = StartupTaskState::Aborted; + for nested in tasks.iter().skip(1).step_by(2) { + nested.abort(); + } + let outcome = timeout(Duration::from_secs(2), async { + tokio::select! { + _ = async { + while tasks.iter().any(|task| !task.is_finished()) { + tokio::task::yield_now().await; + } + } => false, + byte = server_read.read_u8() => { + byte.expect("client transport must remain open"); + true + } + } + }) + .await; + // Clean up the still-waiting dispatches even when the regression fails. + for task in &tasks { + task.abort(); + } + outcome + } else { + startup_tasks.abort(); + Ok(false) + }; + timeout(Duration::from_secs(2), async { + while tasks.iter().any(|task| !task.is_finished()) { + tokio::task::yield_now().await; + } + }) + .await + .expect("all dispatch futures must terminate before fencing the writer"); + assert!( + !late_reply.expect("cancelled nested handlers must terminate their dispatches"), + "cancelled nested handler emitted a fallback RPC before outer abort" + ); + + // is_finished covers both layers, so no woken parent can enqueue a fallback + // after this marker. The writer actor preserves previously enqueued frames. + let marker = crate::JsonRpcResponse { + jsonrpc: "2.0".into(), + id: 999, + result: Some(serde_json::json!({ "marker": "dispatches-finished" })), + error: None, + }; + client.send_response(&marker).await.unwrap(); + let body = serde_json::to_vec(&marker).unwrap(); + let expected = format!( + "Content-Length: {}\r\n\r\n{}", + body.len(), + String::from_utf8(body).unwrap() + ); + let mut received = vec![0; expected.len()]; + timeout( + Duration::from_secs(2), + server_read.read_exact(&mut received), + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + received, + expected.as_bytes(), + "aborted dispatch emitted a late fallback RPC" + ); +} + +#[tokio::test] +async fn deferred_callback_after_guard_drop_cannot_register_or_replace_retry() { + for retry in [false, true] { + let (client_write, _server_read) = tokio::io::duplex(8192); + let (_server_write, client_read) = tokio::io::duplex(8192); + let client = + crate::Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + let stash = Arc::new(parking_lot::Mutex::new(None)); + let (guard, callback) = PendingSessionRegistration::deferred( + client.clone(), + stash.clone(), + tokio_util::sync::CancellationToken::new(), + tokio_util::sync::CancellationToken::new(), + ); + + // The reader owns the dequeued callback, but has not run it when the caller drops. + drop(guard); + let session_id = crate::SessionId::new("deferred-retry"); + let registration = retry.then(|| client.register_session(&session_id)); + let response = crate::JsonRpcResponse { + jsonrpc: "2.0".into(), + id: 1, + result: Some(serde_json::json!({ "sessionId": session_id })), + error: None, + }; + assert!( + callback(&response).is_err(), + "cancelled callback registered a session" + ); + assert!(stash.lock().is_none()); + if let Some(registration) = registration { + client.unregister_session_owned(&session_id, registration.token); + } + assert_eq!(client.registered_session_count_for_test(), 0); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn aborted_startup_rejects_callback_without_polling() { + let tasks = Arc::new(StartupTasks::default()); + let (completed_tx, completed_rx) = oneshot::channel(); + tasks.abort(); + let task = spawn_startup_tracked( + async move { + completed_tx.send(()).unwrap(); + }, + Some(&tasks), + ); + assert!(task.is_none(), "aborted startup must not spawn a callback"); + assert!( + timeout(Duration::from_secs(2), completed_rx) + .await + .unwrap() + .is_err(), + "rejected callback must be dropped without polling" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn abort_cancels_registered_callbacks() { + let tasks = Arc::new(StartupTasks::default()); + let (entered_tx, entered_rx) = oneshot::channel(); + let (mut release_tx, release_rx) = oneshot::channel::<()>(); + let task = spawn_startup_tracked( + async move { + entered_tx.send(()).unwrap(); + release_rx.await.unwrap(); + }, + Some(&tasks), + ) + .unwrap(); + timeout(Duration::from_secs(2), entered_rx) + .await + .unwrap() + .unwrap(); + tasks.abort(); + timeout(Duration::from_secs(2), release_tx.closed()) + .await + .expect("registered callback must be aborted without releasing it"); + assert!( + timeout(Duration::from_secs(2), task) + .await + .unwrap() + .unwrap_err() + .is_cancelled() + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn abort_rejects_nested_spawn_from_an_already_polling_parent() { + let tasks = Arc::new(StartupTasks::default()); + let (entered_tx, entered_rx) = oneshot::channel(); + let (release_tx, release_rx) = std::sync::mpsc::channel(); + let (rejected_tx, rejected_rx) = oneshot::channel(); + let (completed_tx, completed_rx) = oneshot::channel(); + let nested_tasks = tasks.clone(); + let parent = spawn_startup_tracked( + async move { + entered_tx.send(()).unwrap(); + // Keep this poll in flight while the other worker aborts startup. + // An async gate would let Tokio drop the parent before its nested spawn. + release_rx.recv().unwrap(); + let nested = spawn_startup_tracked( + async move { + completed_tx.send(()).unwrap(); + }, + Some(&nested_tasks), + ); + rejected_tx.send(nested.is_none()).unwrap(); + }, + Some(&tasks), + ) + .unwrap(); + timeout(Duration::from_secs(2), entered_rx) + .await + .unwrap() + .unwrap(); + tasks.abort(); + release_tx.send(()).unwrap(); + let rejected = timeout(Duration::from_secs(2), rejected_rx) + .await + .unwrap() + .unwrap(); + // Join before asserting so a failing regression also releases the worker. + let _ = timeout(Duration::from_secs(2), parent).await.unwrap(); + assert!( + rejected, + "already-aborted startup admitted a nested callback" + ); + assert!( + timeout(Duration::from_secs(2), completed_rx) + .await + .unwrap() + .is_err(), + "nested callback ran after startup was aborted" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn disarm_preserves_pending_and_later_tasks() { + let tasks = Arc::new(StartupTasks::default()); + let mut gates = Vec::new(); + for after_disarm in [false, true] { + if after_disarm { + tasks.disarm(); + } + let (release_tx, release_rx) = oneshot::channel::<()>(); + let (completed_tx, completed_rx) = oneshot::channel(); + let task = spawn_startup_tracked( + async move { + release_rx.await.unwrap(); + completed_tx.send(()).unwrap(); + }, + Some(&tasks), + ) + .unwrap(); + gates.push((release_tx, completed_rx, task)); + } + for (release, completed, task) in gates { + release + .send(()) + .expect("successful startup must not abort callbacks"); + timeout(Duration::from_secs(2), completed) + .await + .unwrap() + .unwrap(); + timeout(Duration::from_secs(2), task) + .await + .unwrap() + .unwrap(); + } +} 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 87868101f5..b65a0d883d 100644 --- a/rust/src/session_fs.rs +++ b/rust/src/session_fs.rs @@ -47,9 +47,11 @@ use std::fmt; use async_trait::async_trait; +#[cfg(any(feature = "runtime", test))] +use crate::generated::api_types::SessionFsSetProviderConventions; use crate::generated::api_types::{ SessionFsError, SessionFsErrorCode, SessionFsReaddirWithTypesEntry, - SessionFsReaddirWithTypesEntryType, SessionFsSetProviderConventions, SessionFsStatResult, + SessionFsReaddirWithTypesEntryType, SessionFsStatResult, }; pub use crate::generated::api_types::{ SessionFsSqliteQueryType, SessionFsSqliteTransactionErrorClass, @@ -63,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 { @@ -76,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. @@ -134,6 +144,7 @@ pub enum SessionFsConventions { } impl SessionFsConventions { + #[cfg(any(feature = "runtime", test))] pub(crate) fn into_wire(self) -> SessionFsSetProviderConventions { match self { Self::Posix => SessionFsSetProviderConventions::Posix, @@ -172,6 +183,7 @@ impl fmt::Display for FsErrorKind { #[derive(Debug)] pub struct FsError { repr: Repr, + write_changed: bool, } impl FsError { @@ -185,6 +197,7 @@ impl FsError { kind, error: error.into(), }), + write_changed: false, } } @@ -213,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 { @@ -221,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, }, } } @@ -253,6 +285,7 @@ impl From for FsError { fn from(kind: FsErrorKind) -> Self { Self { repr: Repr::Simple(kind), + write_changed: false, } } } @@ -399,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, @@ -502,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), @@ -618,52 +672,7 @@ pub struct SessionFsSqliteQueryResult { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn fs_error_maps_io_not_found_to_enoent() { - let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt"); - let fs_err: FsError = io_err.into(); - assert!( - matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt") - ); - let wire = fs_err.into_wire(); - assert_eq!(wire.code, SessionFsErrorCode::ENOENT); - } - - #[test] - fn fs_error_maps_other_io_to_unknown() { - let io_err = std::io::Error::other("disk full"); - let fs_err: FsError = io_err.into(); - assert!(matches!(fs_err.kind(), FsErrorKind::Other)); - let wire = fs_err.into_wire(); - assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN); - assert!(wire.message.unwrap().contains("disk full")); - } - - #[test] - fn conventions_maps_to_wire() { - assert_eq!( - SessionFsConventions::Posix.into_wire(), - SessionFsSetProviderConventions::Posix - ); - assert_eq!( - SessionFsConventions::Windows.into_wire(), - SessionFsSetProviderConventions::Windows - ); - } - - struct DefaultProvider; - #[async_trait] - impl SessionFsProvider for DefaultProvider {} - - #[tokio::test] - async fn default_impls_return_unsupported() { - let p = DefaultProvider; - let err = p.read_file("/x").await.unwrap_err(); - assert!( - matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported") - ); - } -} +mod tests; + +#[cfg(test)] +mod write_failure_tests; diff --git a/rust/src/session_fs/tests.rs b/rust/src/session_fs/tests.rs new file mode 100644 index 0000000000..7736636d02 --- /dev/null +++ b/rust/src/session_fs/tests.rs @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn fs_error_maps_io_not_found_to_enoent() { + let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt"); + let fs_err: FsError = io_err.into(); + assert!(matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt")); + let wire = fs_err.into_wire(); + assert_eq!(wire.code, SessionFsErrorCode::ENOENT); +} + +#[test] +fn fs_error_maps_other_io_to_unknown() { + let io_err = std::io::Error::other("disk full"); + let fs_err: FsError = io_err.into(); + assert!(matches!(fs_err.kind(), FsErrorKind::Other)); + let wire = fs_err.into_wire(); + assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN); + assert!(wire.message.unwrap().contains("disk full")); +} + +#[test] +fn conventions_maps_to_wire() { + assert_eq!( + SessionFsConventions::Posix.into_wire(), + SessionFsSetProviderConventions::Posix + ); + assert_eq!( + SessionFsConventions::Windows.into_wire(), + SessionFsSetProviderConventions::Windows + ); +} + +struct DefaultProvider; +#[async_trait] +impl SessionFsProvider for DefaultProvider {} + +#[tokio::test] +async fn default_impls_return_unsupported() { + let p = DefaultProvider; + let err = p.read_file("/x").await.unwrap_err(); + assert!(matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported")); +} diff --git a/rust/src/session_fs/write_failure_tests.rs b/rust/src/session_fs/write_failure_tests.rs new file mode 100644 index 0000000000..905781a0db --- /dev/null +++ b/rust/src/session_fs/write_failure_tests.rs @@ -0,0 +1,26 @@ +#![cfg(test)] + +use super::{FsError, FsErrorKind, SessionFsErrorCode}; + +#[test] +fn failed_write_reports_only_known_changes() { + let ordinary = FsError::with_message(FsErrorKind::Other, "rejected").into_write_wire(); + assert_eq!(ordinary.code, SessionFsErrorCode::UNKNOWN); + assert_eq!(ordinary.write_changed, None); + + let changed = FsError::with_message(FsErrorKind::Other, "disk full") + .with_write_changed() + .into_write_wire(); + assert_eq!(changed.message.as_deref(), Some("disk full")); + assert_eq!(changed.write_changed, Some(true)); + let json = serde_json::to_value(changed).unwrap(); + assert_eq!(json["writeChanged"], true); +} + +#[test] +fn write_marker_does_not_leak_into_other_methods() { + let read_error = FsError::with_message(FsErrorKind::Other, "read failed") + .with_write_changed() + .into_wire(); + assert_eq!(read_error.write_changed, None); +} diff --git a/rust/src/session_fs_dispatch.rs b/rust/src/session_fs_dispatch.rs index c84981ac0c..06cbb016b9 100644 --- a/rust/src/session_fs_dispatch.rs +++ b/rust/src/session_fs_dispatch.rs @@ -7,25 +7,29 @@ use std::sync::Arc; +use base64::Engine as _; use serde::Serialize; use serde_json::Value; use tracing::warn; use crate::generated::api_types::{ SessionFsAppendFileRequest, SessionFsError, SessionFsErrorCode, SessionFsExistsRequest, - SessionFsExistsResult, SessionFsMkdirRequest, SessionFsReadFileRequest, - SessionFsReadFileResult, SessionFsReaddirRequest, SessionFsReaddirResult, - SessionFsReaddirWithTypesRequest, SessionFsReaddirWithTypesResult, SessionFsRenameRequest, - SessionFsRmRequest, SessionFsSqliteExistsParams, SessionFsSqliteExistsResult, - SessionFsSqliteQueryRequest, SessionFsSqliteQueryResult as GeneratedSqliteQueryResult, + SessionFsExistsResult, SessionFsMkdirRequest, SessionFsReadFileBytesRequest, + SessionFsReadFileBytesResult, SessionFsReadFileRequest, SessionFsReadFileResult, + SessionFsReaddirRequest, SessionFsReaddirResult, SessionFsReaddirWithTypesRequest, + SessionFsReaddirWithTypesResult, SessionFsRenameRequest, SessionFsRmRequest, + SessionFsSqliteExistsParams, SessionFsSqliteExistsResult, SessionFsSqliteQueryRequest, + SessionFsSqliteQueryResult as GeneratedSqliteQueryResult, SessionFsSqliteTransactionError as GeneratedSqliteTransactionError, SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionRequest, SessionFsSqliteTransactionResult as GeneratedSqliteTransactionResult, SessionFsStatRequest, - SessionFsStatResult, SessionFsWriteFileRequest, + SessionFsStatResult, SessionFsWriteFileBytesRequest, SessionFsWriteFileRequest, }; use crate::session_fs::SessionFsProvider; use crate::{Client, JsonRpcRequest, JsonRpcResponse, error_codes}; +const MAX_BINARY_BYTES: usize = (64 * 1024 * 1024 - 1024) / 4 * 3; + /// Helper: serialize a typed result, send the response. async fn respond(client: &Client, request_id: u64, result: T) { let value = match serde_json::to_value(&result) { @@ -94,6 +98,53 @@ pub(crate) async fn read_file( respond(client, id, result).await; } +pub(crate) async fn read_file_bytes( + client: &Client, + provider: &Arc, + request: JsonRpcRequest, +) { + let params: SessionFsReadFileBytesRequest = match parse_params(&request) { + Some(params) => params, + None => { + send_error(client, request.id, "invalid sessionFs.readFileBytes params").await; + return; + } + }; + let result = match provider.binary() { + Some(binary) => binary.read_file_bytes(¶ms.path).await, + None => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "binary reads are not supported", + )), + }; + respond(client, request.id, binary_read_response(result)).await; +} + +fn binary_read_response( + result: Result, crate::session_fs::FsError>, +) -> SessionFsReadFileBytesResult { + match result { + Ok(bytes) if bytes.len() > MAX_BINARY_BYTES => SessionFsReadFileBytesResult { + content: String::new(), + error: Some( + crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "sessionFs.readFileBytes content exceeds the binary read limit", + ) + .into_wire(), + ), + }, + Ok(bytes) => SessionFsReadFileBytesResult { + content: base64::engine::general_purpose::STANDARD.encode(bytes), + error: None, + }, + Err(error) => SessionFsReadFileBytesResult { + content: String::new(), + error: Some(error.into_wire()), + }, + } +} + pub(crate) async fn write_file( client: &Client, provider: &Arc, @@ -112,7 +163,54 @@ pub(crate) async fn write_file( .await { Ok(()) => respond(client, id, Value::Null).await, - Err(e) => respond(client, id, e.into_wire()).await, + Err(e) => respond(client, id, e.into_write_wire()).await, + } +} + +pub(crate) async fn write_file_bytes( + client: &Client, + provider: &Arc, + request: JsonRpcRequest, +) { + let params: SessionFsWriteFileBytesRequest = match parse_params(&request) { + Some(params) => params, + None => { + send_error( + client, + request.id, + "invalid sessionFs.writeFileBytes params", + ) + .await; + return; + } + }; + let result = if params.content.len() > MAX_BINARY_BYTES.div_ceil(3) * 4 { + Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "sessionFs.writeFileBytes content exceeds the binary write limit", + )) + } else { + match base64::engine::general_purpose::STANDARD.decode(¶ms.content) { + Ok(content) => match provider.binary() { + Some(binary) => { + binary + .write_file_bytes(¶ms.path, &content, params.mode) + .await + } + None => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "binary writes are not supported", + )), + }, + Err(error) => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + format!("invalid sessionFs.writeFileBytes base64 content: {error}"), + )), + } + }; + match result { + Ok(()) => respond(client, request.id, Value::Null).await, + Err(error) => respond(client, request.id, error.into_wire()).await, } } @@ -334,6 +432,7 @@ pub(crate) async fn sqlite_query( message: Some( "SQLite is not supported by this SessionFs provider".to_string(), ), + write_changed: None, }), last_insert_rowid: None, rows: Vec::new(), @@ -487,6 +586,8 @@ pub(crate) async fn dispatch( }; match method { "sessionFs.readFile" => read_file(client, &provider, request).await, + "sessionFs.readFileBytes" => read_file_bytes(client, &provider, request).await, + "sessionFs.writeFileBytes" => write_file_bytes(client, &provider, request).await, "sessionFs.writeFile" => write_file(client, &provider, request).await, "sessionFs.appendFile" => append_file(client, &provider, request).await, "sessionFs.exists" => exists(client, &provider, request).await, @@ -506,3 +607,6 @@ pub(crate) async fn dispatch( } true } + +#[cfg(test)] +mod tests; diff --git a/rust/src/session_fs_dispatch/tests.rs b/rust/src/session_fs_dispatch/tests.rs new file mode 100644 index 0000000000..c0f70d78b9 --- /dev/null +++ b/rust/src/session_fs_dispatch/tests.rs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![cfg(test)] + +use super::*; + +#[test] +fn oversized_binary_provider_result_is_a_filesystem_error() { + let result = binary_read_response(Ok(vec![0; MAX_BINARY_BYTES + 1])); + assert!(result.content.is_empty()); + let error = result.error.expect("oversized result must carry an error"); + assert_eq!(error.code, SessionFsErrorCode::UNKNOWN); + assert!( + error + .message + .as_deref() + .is_some_and(|message| message.contains("binary read limit")) + ); +} + +#[test] +fn small_binary_provider_result_preserves_exact_bytes() { + let result = binary_read_response(Ok(vec![0, 255, 254, 1])); + assert_eq!(result.content, "AP/+AQ=="); + assert!(result.error.is_none()); +} diff --git a/rust/src/skill_provider.rs b/rust/src/skill_provider.rs new file mode 100644 index 0000000000..28d4fc8911 --- /dev/null +++ b/rust/src/skill_provider.rs @@ -0,0 +1,40 @@ +//! Session-scoped skill provider callbacks. +//! +//!
    +//! +//! **Experimental.** These types are part of an experimental wire-protocol +//! surface and may change or be removed in future SDK or CLI releases. +//! +//!
    + +use async_trait::async_trait; + +use crate::Error; +pub use crate::rpc::SkillProviderDescriptor; + +/// Supplies session-scoped skills to the runtime. +/// +///
    +/// +/// **Experimental.** This trait is part of an experimental wire-protocol +/// surface and may change or be removed in future SDK or CLI releases. +/// +///
    +/// +/// Each inbound `skillProvider.*` request is dispatched on its own spawned +/// task, so implementations must be safe for concurrent calls. When the +/// runtime cancels a call, for example after its time limit or when the +/// session disconnects, the SDK drops the provider future and the runtime +/// ignores any later result. Register a +/// provider with [`SessionConfig::with_skill_provider`](crate::SessionConfig::with_skill_provider) +/// or [`ResumeSessionConfig::with_skill_provider`](crate::ResumeSessionConfig::with_skill_provider). +#[async_trait] +pub trait SkillProvider: Send + Sync + 'static { + /// Return the catalog of skills this session exposes. + async fn list_skills(&self) -> std::result::Result, Error>; + + /// Return the markdown for one skill name. + /// + /// Return `Ok(None)` when the skill name is not found. + async fn read_skill(&self, name: &str) -> std::result::Result, Error>; +} diff --git a/rust/src/startup_timings.rs b/rust/src/startup_timings.rs index 7784a6ab3b..bf0166772d 100644 --- a/rust/src/startup_timings.rs +++ b/rust/src/startup_timings.rs @@ -18,6 +18,7 @@ //! //! [`Instant`]: std::time::Instant +#[cfg(any(feature = "runtime", test))] use std::time::Duration; /// Millisecond breakdown of the phases of [`Client::start`](crate::Client::start). @@ -73,33 +74,11 @@ pub struct StartupTimings { impl StartupTimings { /// Whole milliseconds of `duration`, saturating at [`u64::MAX`]. + #[cfg(any(feature = "runtime", test))] pub(crate) fn millis(duration: Duration) -> u64 { u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) } } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn millis_truncates_to_whole_milliseconds() { - assert_eq!(StartupTimings::millis(Duration::from_micros(1_999)), 1); - assert_eq!(StartupTimings::millis(Duration::from_millis(250)), 250); - assert_eq!(StartupTimings::millis(Duration::ZERO), 0); - } - - #[test] - fn default_leaves_every_phase_unset() { - let timings = StartupTimings::default(); - assert_eq!(timings, StartupTimings::default()); - assert!(timings.program_resolve_ms.is_none()); - assert!(timings.process_spawn_ms.is_none()); - assert!(timings.port_wait_ms.is_none()); - assert_eq!(timings.transport_setup_ms, 0); - assert_eq!(timings.handshake_ms, 0); - assert!(timings.session_fs_ms.is_none()); - assert!(timings.llm_handler_ms.is_none()); - assert_eq!(timings.total_ms, 0); - } -} +mod tests; diff --git a/rust/src/startup_timings/tests.rs b/rust/src/startup_timings/tests.rs new file mode 100644 index 0000000000..f59f60ac04 --- /dev/null +++ b/rust/src/startup_timings/tests.rs @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn millis_truncates_to_whole_milliseconds() { + assert_eq!(StartupTimings::millis(Duration::from_micros(1_999)), 1); + assert_eq!(StartupTimings::millis(Duration::from_millis(250)), 250); + assert_eq!(StartupTimings::millis(Duration::ZERO), 0); +} + +#[test] +fn default_leaves_every_phase_unset() { + let timings = StartupTimings::default(); + assert_eq!(timings, StartupTimings::default()); + assert!(timings.program_resolve_ms.is_none()); + assert!(timings.process_spawn_ms.is_none()); + assert!(timings.port_wait_ms.is_none()); + assert_eq!(timings.transport_setup_ms, 0); + assert_eq!(timings.handshake_ms, 0); + assert!(timings.session_fs_ms.is_none()); + assert!(timings.llm_handler_ms.is_none()); + assert_eq!(timings.total_ms, 0); +} diff --git a/rust/src/tests.rs b/rust/src/tests.rs new file mode 100644 index 0000000000..8656d6f4d3 --- /dev/null +++ b/rust/src/tests.rs @@ -0,0 +1,813 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn is_transport_failure_matches_request_cancelled() { + let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)); + assert!(err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_matches_io_error() { + let err = Error::from(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "gone")); + assert!(err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_rejects_rpc_error() { + let err = Error::with_message(ErrorKind::Rpc { code: -1 }, "bad"); + assert!(!err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_rejects_session_error() { + let err = Error::from(ErrorKind::Session(SessionErrorKind::NotFound("s1".into()))); + assert!(!err.is_transport_failure()); +} + +#[test] +fn client_options_builder_composes() { + let opts = ClientOptions::new() + .with_program(CliProgram::Path(PathBuf::from("/usr/local/bin/copilot"))) + .with_prefix_args(["node"]) + .with_cwd(PathBuf::from("/tmp")) + .with_env([("KEY", "value")]) + .with_env_remove(["UNWANTED"]) + .with_extra_args(["--quiet"]) + .with_github_token("ghp_test") + .with_use_logged_in_user(false) + .with_log_level(LogLevel::Debug) + .with_session_idle_timeout_seconds(120) + .with_enable_remote_sessions(true); + assert!(matches!(opts.program, CliProgram::Path(_))); + assert_eq!(opts.prefix_args, vec![std::ffi::OsString::from("node")]); + assert_eq!(opts.working_directory, PathBuf::from("/tmp")); + assert_eq!( + opts.env, + vec![( + std::ffi::OsString::from("KEY"), + std::ffi::OsString::from("value") + )] + ); + assert_eq!(opts.env_remove, vec![std::ffi::OsString::from("UNWANTED")]); + assert_eq!(opts.extra_args, vec!["--quiet".to_string()]); + assert_eq!(opts.github_token.as_deref(), Some("ghp_test")); + assert_eq!(opts.use_logged_in_user, Some(false)); + assert!(matches!(opts.log_level, Some(LogLevel::Debug))); + assert_eq!(opts.session_idle_timeout_seconds, Some(120)); + assert!(opts.enable_remote_sessions); +} + +#[test] +fn default_transport_values_resolve_without_process_state() { + assert!(matches!( + resolve_default_transport_value(None).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("stdio")).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("INPROCESS")).unwrap(), + Transport::InProcess + )); + assert!(resolve_default_transport_value(Some("tcp")).is_err()); +} + +#[test] +fn inprocess_rejects_process_scoped_options() { + let invalid = [ + ClientOptions::new().with_cwd("."), + ClientOptions::new().with_env([("KEY", "value")]), + ClientOptions::new().with_env_remove(["KEY"]), + ClientOptions::new().with_telemetry(TelemetryConfig::default()), + ClientOptions::new().with_prefix_args(["index.js"]), + ClientOptions::new().with_program(CliProgram::Path("copilot".into())), + ClientOptions::new().with_extra_args(["--verbose"]), + ]; + + for options in invalid { + assert!(validate_inprocess_options(&options).is_err()); + } +} + +#[test] +fn inprocess_allows_typed_runtime_options() { + let options = ClientOptions::new() + .with_base_directory("state") + .with_log_level(LogLevel::Debug) + .with_session_idle_timeout_seconds(10) + .with_github_token("token") + .with_use_logged_in_user(false) + .with_enable_remote_sessions(true); + + assert!(validate_inprocess_options(&options).is_ok()); +} + +#[cfg(not(feature = "in-process"))] +#[tokio::test] +async fn inprocess_requires_cargo_feature() { + let error = Client::start(ClientOptions::new().with_transport(Transport::InProcess)) + .await + .unwrap_err(); + + assert!(error.to_string().contains("in-process")); +} + +#[test] +fn is_transport_failure_rejects_other_protocol_errors() { + let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::CliStartupTimeout)); + assert!(!err.is_transport_failure()); +} + +#[test] +fn build_command_lets_env_remove_strip_injected_token() { + let opts = ClientOptions { + github_token: Some("secret".to_string()), + env_remove: vec![std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN")], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + // get_envs() iter yields the latest action per key — None means removed. + let action = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .map(|(_, v)| v); + assert_eq!( + action, + Some(None), + "env_remove should win over github_token" + ); +} + +#[test] +fn build_command_lets_env_override_injected_token() { + let opts = ClientOptions { + github_token: Some("from-options".to_string()), + env: vec![( + std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN"), + std::ffi::OsString::from("from-env"), + )], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + let value = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .and_then(|(_, v)| v); + assert_eq!(value, Some(std::ffi::OsStr::new("from-env"))); +} + +#[test] +fn build_command_injects_github_token_by_default() { + let opts = ClientOptions { + github_token: Some("just-the-token".to_string()), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + let value = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .and_then(|(_, v)| v); + assert_eq!(value, Some(std::ffi::OsStr::new("just-the-token"))); +} + +fn env_value<'a>(cmd: &'a tokio::process::Command, key: &str) -> Option<&'a std::ffi::OsStr> { + cmd.as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new(key)) + .and_then(|(_, v)| v) +} + +#[test] +fn telemetry_config_builder_composes() { + let cfg = TelemetryConfig::new() + .with_otlp_endpoint("http://collector:4318") + .with_otlp_protocol(OtlpHttpProtocol::HttpProtobuf) + .with_file_path(PathBuf::from("/var/log/copilot.jsonl")) + .with_exporter_type(OtelExporterType::OtlpHttp) + .with_source_name("my-app") + .with_capture_content(true); + + assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://collector:4318")); + assert_eq!(cfg.otlp_protocol, Some(OtlpHttpProtocol::HttpProtobuf)); + assert_eq!( + cfg.file_path.as_deref(), + Some(Path::new("/var/log/copilot.jsonl")), + ); + assert_eq!(cfg.exporter_type, Some(OtelExporterType::OtlpHttp)); + assert_eq!(cfg.source_name.as_deref(), Some("my-app")); + assert_eq!(cfg.capture_content, Some(true)); + assert!(!cfg.is_empty()); + assert!(TelemetryConfig::new().is_empty()); +} + +#[test] +fn otlp_http_protocol_serde_matches_env_value() { + for (protocol, wire) in [ + (OtlpHttpProtocol::HttpJson, "http/json"), + (OtlpHttpProtocol::HttpProtobuf, "http/protobuf"), + ] { + assert_eq!(protocol.as_str(), wire); + + let serialized = serde_json::to_string(&protocol).unwrap(); + assert_eq!(serialized, format!("\"{wire}\"")); + + let deserialized: OtlpHttpProtocol = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, protocol); + } +} + +#[test] +fn build_command_sets_otel_env_when_telemetry_enabled() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://collector:4318".to_string()), + otlp_protocol: Some(OtlpHttpProtocol::HttpProtobuf), + file_path: Some(PathBuf::from("/var/log/copilot.jsonl")), + exporter_type: Some(OtelExporterType::OtlpHttp), + source_name: Some("my-app".to_string()), + capture_content: Some(true), + }), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_ENABLED"), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://collector:4318")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_PROTOCOL"), + Some(std::ffi::OsStr::new("http/protobuf")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_FILE_EXPORTER_PATH"), + Some(std::ffi::OsStr::new("/var/log/copilot.jsonl")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_EXPORTER_TYPE"), + Some(std::ffi::OsStr::new("otlp-http")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_SOURCE_NAME"), + Some(std::ffi::OsStr::new("my-app")), + ); + assert_eq!( + env_value(&cmd, "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"), + Some(std::ffi::OsStr::new("true")), + ); +} + +#[test] +fn build_command_omits_otel_env_when_telemetry_none() { + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + for key in [ + "COPILOT_OTEL_ENABLED", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_PROTOCOL", + "COPILOT_OTEL_FILE_EXPORTER_PATH", + "COPILOT_OTEL_EXPORTER_TYPE", + "COPILOT_OTEL_SOURCE_NAME", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + ] { + assert!( + env_value(&cmd, key).is_none(), + "expected {key} to be unset when telemetry is None", + ); + } +} + +#[test] +fn build_command_omits_unset_telemetry_fields() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://collector:4318".to_string()), + ..Default::default() + }), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + // The one set field plus the implicit enabled flag should propagate. + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_ENABLED"), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://collector:4318")), + ); + // None of the other fields should leak as env vars. + for key in [ + "OTEL_EXPORTER_OTLP_PROTOCOL", + "COPILOT_OTEL_FILE_EXPORTER_PATH", + "COPILOT_OTEL_EXPORTER_TYPE", + "COPILOT_OTEL_SOURCE_NAME", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + ] { + assert!(env_value(&cmd, key).is_none(), "{key} should be unset"); + } +} + +#[test] +fn build_command_lets_user_env_override_telemetry() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://from-config:4318".to_string()), + ..Default::default() + }), + env: vec![( + std::ffi::OsString::from("OTEL_EXPORTER_OTLP_ENDPOINT"), + std::ffi::OsString::from("http://from-user-env:4318"), + )], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://from-user-env:4318")), + "user-supplied options.env should override telemetry config", + ); +} + +#[test] +fn build_command_sets_copilot_home_env_when_configured() { + let opts = ClientOptions::new().with_base_directory(PathBuf::from("/custom/copilot")); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_HOME"), + Some(std::ffi::OsStr::new("/custom/copilot")), + ); + + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert!(env_value(&cmd, "COPILOT_HOME").is_none()); +} + +#[test] +fn build_command_sets_connection_token_env_when_configured() { + let opts = ClientOptions::new().with_transport(Transport::Tcp { + port: 0, + connection_token: Some("secret-token".to_string()), + }); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_CONNECTION_TOKEN"), + Some(std::ffi::OsStr::new("secret-token")), + ); + + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert!(env_value(&cmd, "COPILOT_CONNECTION_TOKEN").is_none()); +} + +#[tokio::test] +async fn start_rejects_empty_connection_token() { + let opts = ClientOptions::new() + .with_transport(Transport::Tcp { + port: 0, + connection_token: Some(String::new()), + }) + .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); + let err = Client::start(opts).await.unwrap_err(); + assert!( + matches!(err.kind(), ErrorKind::InvalidConfig), + "got {err:?}" + ); +} + +#[tokio::test] +async fn start_rejects_empty_external_connection_token() { + let opts = ClientOptions::new() + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port: 1, + connection_token: Some(String::new()), + }) + .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); + let err = Client::start(opts).await.unwrap_err(); + assert!( + matches!(err.kind(), ErrorKind::InvalidConfig), + "got {err:?}" + ); +} + +#[test] +fn telemetry_config_capture_content_serializes_as_lowercase_bool() { + let opts_true = ClientOptions { + telemetry: Some(TelemetryConfig { + capture_content: Some(true), + ..Default::default() + }), + ..Default::default() + }; + let opts_false = ClientOptions { + telemetry: Some(TelemetryConfig { + capture_content: Some(false), + ..Default::default() + }), + ..Default::default() + }; + let cmd_true = Client::build_command(Path::new("/bin/echo"), &opts_true, Path::new("/tmp")); + let cmd_false = Client::build_command(Path::new("/bin/echo"), &opts_false, Path::new("/tmp")); + assert_eq!( + env_value( + &cmd_true, + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" + ), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value( + &cmd_false, + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" + ), + Some(std::ffi::OsStr::new("false")), + ); +} + +#[test] +fn session_idle_timeout_args_are_omitted_by_default() { + let opts = ClientOptions::default(); + assert!(Client::session_idle_timeout_args(&opts).is_empty()); +} + +#[test] +fn session_idle_timeout_args_omitted_for_zero() { + let opts = ClientOptions { + session_idle_timeout_seconds: Some(0), + ..Default::default() + }; + assert!(Client::session_idle_timeout_args(&opts).is_empty()); +} + +#[test] +fn session_idle_timeout_args_emit_flag_for_positive_value() { + let opts = ClientOptions { + session_idle_timeout_seconds: Some(300), + ..Default::default() + }; + assert_eq!( + Client::session_idle_timeout_args(&opts), + vec!["--session-idle-timeout".to_string(), "300".to_string()] + ); +} + +#[test] +fn remote_args_omitted_by_default() { + let opts = ClientOptions::default(); + assert!(Client::remote_args(&opts).is_empty()); +} + +#[test] +fn remote_args_emit_flag_when_enabled() { + let opts = ClientOptions { + enable_remote_sessions: true, + ..Default::default() + }; + assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); +} + +#[test] +fn log_level_args_omitted_when_unset() { + let opts = ClientOptions::default(); + assert!(opts.log_level.is_none()); + assert!( + Client::log_level_args(&opts).is_empty(), + "with no caller-supplied log_level the SDK must not pass --log-level" + ); +} + +#[test] +fn log_level_args_emit_flag_when_set() { + let opts = ClientOptions::default().with_log_level(LogLevel::Debug); + assert_eq!(Client::log_level_args(&opts), vec!["--log-level", "debug"]); +} + +#[test] +fn cli_mode_opts_into_process_logging_without_changing_empty_mode_environment() { + for (mode, expected) in [ + (ClientMode::Empty, None), + (ClientMode::CopilotCli, Some("1")), + ] { + let mut options = ClientOptions::default().with_mode(mode); + options.env.push(( + std::ffi::OsString::from("COPILOT_RUNTIME_PROCESS_FILE_LOGGING"), + std::ffi::OsString::from("opposite"), + )); + let command = Client::build_command(Path::new("copilot-runtime"), &options, Path::new(".")); + let actual = command + .as_std() + .get_envs() + .find(|(key, _)| *key == std::ffi::OsStr::new("COPILOT_RUNTIME_PROCESS_FILE_LOGGING")) + .and_then(|(_, value)| value); + assert_eq!( + actual, + Some(std::ffi::OsStr::new(expected.unwrap_or("opposite"))), + "mode: {mode:?}" + ); + } +} + +#[test] +fn log_level_str_round_trips() { + for level in [ + LogLevel::None, + LogLevel::Error, + LogLevel::Warning, + LogLevel::Info, + LogLevel::Debug, + LogLevel::All, + ] { + let s = level.as_str(); + let json = serde_json::to_string(&level).unwrap(); + assert_eq!(json, format!("\"{s}\"")); + let parsed: LogLevel = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, level); + } +} + +#[test] +fn client_options_debug_redacts_handler() { + struct StubHandler; + #[async_trait] + impl ListModelsHandler for StubHandler { + async fn list_models(&self) -> Result> { + Ok(vec![]) + } + } + let opts = ClientOptions { + on_list_models: Some(Arc::new(StubHandler)), + github_token: Some("secret-token".into()), + ..Default::default() + }; + let debug = format!("{opts:?}"); + assert!(debug.contains("on_list_models: Some(\"\")")); + assert!(debug.contains("github_token: Some(\"\")")); + assert!(!debug.contains("secret-token")); +} + +#[tokio::test] +async fn list_models_uses_on_list_models_handler_when_set() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct CountingHandler { + calls: Arc, + models: Vec, + } + #[async_trait] + impl ListModelsHandler for CountingHandler { + async fn list_models(&self) -> Result> { + self.calls.fetch_add(1, Ordering::SeqCst); + Ok(self.models.clone()) + } + } + + let calls = Arc::new(AtomicUsize::new(0)); + let model = Model { + id: "byok-gpt-4".into(), + name: "BYOK GPT-4".into(), + ..Default::default() + }; + let handler: Arc = Arc::new(CountingHandler { + calls: Arc::clone(&calls), + models: vec![model.clone()], + }); + + let client = client_with_list_models_handler(handler); + + let result = client.list_models().await.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, "byok-gpt-4"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn list_models_serializes_concurrent_cache_misses() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct SlowCountingHandler { + calls: Arc, + models: Vec, + } + #[async_trait] + impl ListModelsHandler for SlowCountingHandler { + async fn list_models(&self) -> Result> { + self.calls.fetch_add(1, Ordering::SeqCst); + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + Ok(self.models.clone()) + } + } + + let calls = Arc::new(AtomicUsize::new(0)); + let model = Model { + id: "single-flight-model".into(), + name: "Single Flight Model".into(), + ..Default::default() + }; + let handler: Arc = Arc::new(SlowCountingHandler { + calls: Arc::clone(&calls), + models: vec![model], + }); + let client = client_with_list_models_handler(handler); + + let (first, second) = tokio::join!(client.list_models(), client.list_models()); + assert_eq!(first.unwrap()[0].id, "single-flight-model"); + assert_eq!(second.unwrap()[0].id, "single-flight-model"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn cancelled_resume_session_unregisters_pending_session() { + let (client_write, _server_read) = tokio::io::duplex(8192); + let (_server_write, client_read) = tokio::io::duplex(8192); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + assert!(client.startup_timings().is_none()); + let session_id = SessionId::new("resume-cancel-test"); + let handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + } + }); + + wait_for_pending_session_registration(&client).await; + handle.abort(); + let _ = handle.await; + + assert!(client.inner.router.session_ids().is_empty()); + client.force_stop(); +} + +#[cfg(any(unix, windows))] +#[tokio::test] +async fn dropping_last_client_kills_spawned_cli() { + let temp = tempfile::tempdir().unwrap(); + let ready = temp.path().join("ready"); + let survived = temp.path().join("survived"); + let child = test_child_command(temp.path(), &ready, &survived) + .spawn() + .unwrap(); + let (client_write, _server_read) = tokio::io::duplex(64); + let (_server_write, client_read) = tokio::io::duplex(64); + let client = Client::from_transport( + client_read, + client_write, + Some(child), + None, + temp.path().to_path_buf(), + None, + None, + false, + false, + false, + None, + None, + None, + ClientMode::default(), + None, + false, + ) + .unwrap(); + + wait_for_test_child(&ready).await; + drop(client); + + assert_test_child_killed(&survived).await; +} + +#[cfg(any(unix, windows))] +#[tokio::test] +async fn spawned_child_is_killed_when_dropped() { + let temp = tempfile::tempdir().unwrap(); + let ready = temp.path().join("ready"); + let survived = temp.path().join("survived"); + let child = test_child_command(temp.path(), &ready, &survived) + .spawn() + .unwrap(); + + wait_for_test_child(&ready).await; + drop(child); + + assert_test_child_killed(&survived).await; +} + +#[cfg(any(unix, windows))] +fn test_child_command(temp: &Path, ready: &Path, survived: &Path) -> Command { + let mut command = Client::build_command(Path::new("node"), &ClientOptions::default(), temp); + #[cfg(windows)] + { + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + command + .args([ + "-e", + r#" + const fs = require("node:fs"); + fs.writeFileSync(process.env.READY, "ready"); + setTimeout(() => fs.writeFileSync(process.env.SURVIVED, "survived"), 1000); + "#, + ]) + .env("READY", ready) + .env("SURVIVED", survived) + .stderr(Stdio::inherit()); + command +} + +#[cfg(any(unix, windows))] +async fn wait_for_test_child(ready: &Path) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(30); + while !ready.exists() { + assert!( + tokio::time::Instant::now() < deadline, + "child did not report readiness" + ); + tokio::time::sleep(Duration::from_millis(10)).await; + } +} + +#[cfg(any(unix, windows))] +async fn assert_test_child_killed(survived: &Path) { + tokio::time::sleep(Duration::from_millis(1500)).await; + + assert!( + !survived.exists(), + "child survived after its owner was dropped" + ); +} + +fn client_with_list_models_handler(handler: Arc) -> Client { + Client { + ahp_host_sessions: None, + inner: Arc::new(ClientInner { + ahp_host_callbacks: Arc::new(ahp_host::ExitCallbacks::default()), + ahp_host_sessions: std::sync::Weak::new(), + child: parking_lot::Mutex::new(None), + owns_stdio: false, + force_stop_requested: tokio_util::sync::CancellationToken::new(), + process_tree: parking_lot::Mutex::new(None), + #[cfg(feature = "in-process")] + ffi_host: parking_lot::Mutex::new(None), + rpc: { + let (req_tx, _req_rx) = mpsc::unbounded_channel(); + let (notif_tx, _notif_rx) = broadcast::channel(16); + let (read_pipe, _write_pipe) = tokio::io::duplex(64); + let (_unused_read, write_pipe) = tokio::io::duplex(64); + JsonRpcClient::new(write_pipe, read_pipe, notif_tx, req_tx) + }, + cwd: PathBuf::from("."), + request_rx: parking_lot::Mutex::new(None), + notification_tx: broadcast::channel(16).0, + router: router::SessionRouter::new(), + github_token_registry: Arc::new(github_token::GitHubTokenRegistry::new()), + negotiated_protocol_version: OnceLock::new(), + state: parking_lot::Mutex::new(ConnectionState::Connected), + lifecycle_tx: broadcast::channel(16).0, + on_list_models: Some(handler), + models_cache: parking_lot::Mutex::new(Arc::new(tokio::sync::OnceCell::new())), + session_fs_configured: false, + session_fs_sqlite_declared: false, + session_fs_binary_declared: false, + llm_inference: OnceLock::new(), + extension_launch_provider: Arc::new( + extension_launch_provider::ExtensionLaunchProviderDispatcher::new(None), + ), + installation_confirmation: Arc::new( + installation_confirmation::InstallationConfirmationDispatcher::new(), + ), + on_github_telemetry: None, + on_get_trace_context: None, + effective_connection_token: None, + mode: ClientMode::default(), + client_info: None, + startup_timings: OnceLock::new(), + }), + } +} + +async fn wait_for_pending_session_registration(client: &Client) { + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1); + while client.inner.router.session_ids().is_empty() { + assert!( + tokio::time::Instant::now() < deadline, + "session was not registered" + ); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } +} diff --git a/rust/src/tool.rs b/rust/src/tool.rs index 344d2894ca..12274dbb8b 100644 --- a/rust/src/tool.rs +++ b/rust/src/tool.rs @@ -363,423 +363,4 @@ where } #[cfg(test)] -mod tests { - use super::*; - use crate::types::SessionId; - - struct EchoTool; - - fn echo_tool() -> Tool { - Tool { - name: "echo".to_string(), - description: "Echo the input".to_string(), - parameters: tool_parameters(serde_json::json!({"type": "object"})), - ..Default::default() - } - .with_handler(std::sync::Arc::new(EchoTool)) - } - - #[async_trait] - impl ToolHandler for EchoTool { - async fn call(&self, inv: ToolInvocation) -> Result { - Ok(ToolResult::Text(inv.arguments.to_string())) - } - } - - #[test] - fn tool_handler_returns_tool_definition() { - let def = echo_tool(); - assert_eq!(def.name, "echo"); - assert_eq!(def.description, "Echo the input"); - assert!(def.parameters.contains_key("type")); - assert!(def.handler.is_some()); - } - - #[test] - fn try_tool_parameters_rejects_non_object_schema() { - let err = try_tool_parameters(serde_json::json!(["not", "an", "object"])) - .expect_err("non-object schemas should be rejected"); - - assert!(err.is_data()); - } - - #[test] - fn tool_parameters_serialize_in_deterministic_order() { - // Regression: `Tool.parameters` was a `HashMap`, whose per-instance - // random iteration order made the serialized top-level schema keys - // differ between constructions (and between sessions), busting the - // model provider's prompt cache. `IndexMap` keeps the order stable. - let schema = serde_json::json!({ - "type": "object", - "properties": { - "url": { "type": "string" }, - "count": { "type": "integer" } - }, - "required": ["url"], - "additionalProperties": false - }); - - let build = || Tool { - name: "fetch".to_string(), - parameters: tool_parameters(schema.clone()), - ..Default::default() - }; - - let expected = serde_json::to_string(&build()).expect("serialize tool"); - for _ in 0..64 { - let actual = serde_json::to_string(&build()).expect("serialize tool"); - assert_eq!(actual, expected); - } - - // Pin the exact top-level key order so a regression to any - // order-randomizing container is caught, not just internal drift. - let tool = build(); - let keys: Vec<&str> = tool.parameters.keys().map(String::as_str).collect(); - assert_eq!( - keys, - ["additionalProperties", "properties", "required", "type"] - ); - } - - #[test] - fn convert_mcp_call_tool_result_collects_text_and_binary_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "isError": true, - "content": [ - { "type": "text", "text": "hello" }, - { "type": "image", "data": "aW1n", "mimeType": "image/png" }, - { - "type": "resource", - "resource": { - "uri": "file:///tmp/data.bin", - "blob": "Ymlu", - "mimeType": "application/octet-stream", - "text": "resource text" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); - assert_eq!(expanded.result_type, "failure"); - let binary_results = expanded - .binary_results_for_llm - .expect("binary results should be captured"); - assert_eq!(binary_results.len(), 2); - assert_eq!(binary_results[0].r#type, "image"); - assert_eq!(binary_results[0].data, "aW1n"); - assert_eq!(binary_results[0].mime_type, "image/png"); - assert_eq!( - binary_results[1].description.as_deref(), - Some("file:///tmp/data.bin") - ); - } - - #[test] - fn convert_mcp_call_tool_result_converts_image_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/jpeg" } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, ""); - assert_eq!(expanded.result_type, "success"); - let binary_results = expanded - .binary_results_for_llm - .expect("image result should be captured"); - assert_eq!(binary_results.len(), 1); - assert_eq!(binary_results[0].data, "aW1hZ2U="); - assert_eq!(binary_results[0].mime_type, "image/jpeg"); - assert_eq!(binary_results[0].r#type, "image"); - assert!(binary_results[0].description.is_none()); - } - - #[test] - fn convert_mcp_call_tool_result_converts_resource_blob_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { - "type": "resource", - "resource": { - "uri": "file:///tmp/report.pdf", - "blob": "cGRm", - "mimeType": "application/pdf" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - let binary_results = expanded - .binary_results_for_llm - .expect("resource result should be captured"); - assert_eq!(binary_results.len(), 1); - assert_eq!(binary_results[0].data, "cGRm"); - assert_eq!(binary_results[0].mime_type, "application/pdf"); - assert_eq!(binary_results[0].r#type, "resource"); - assert_eq!( - binary_results[0].description.as_deref(), - Some("file:///tmp/report.pdf") - ); - } - - #[test] - fn convert_mcp_call_tool_result_defaults_resource_blob_mime_type() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { - "type": "resource", - "resource": { - "uri": "file:///tmp/data.bin", - "blob": "Ymlu" - } - }, - { - "type": "resource", - "resource": { - "blob": "YmluMg==", - "mimeType": "" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - let binary_results = expanded - .binary_results_for_llm - .expect("resource blobs should be captured"); - assert_eq!(binary_results.len(), 2); - assert_eq!(binary_results[0].mime_type, "application/octet-stream"); - assert_eq!(binary_results[1].mime_type, "application/octet-stream"); - } - - #[test] - fn convert_mcp_call_tool_result_omits_binary_results_without_binary_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { "type": "text", "text": "hello" }, - { - "type": "resource", - "resource": { - "uri": "file:///tmp/readme.md", - "text": "resource text" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); - assert!(expanded.binary_results_for_llm.is_none()); - } - - #[tokio::test] - async fn tool_handler_call_returns_result() { - let tool = EchoTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "echo".to_string(), - arguments: serde_json::json!({"msg": "hello"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let result = tool.call(inv).await.unwrap(); - match result { - ToolResult::Text(s) => assert!(s.contains("hello")), - _ => panic!("expected Text result"), - } - } - - #[cfg(feature = "derive")] - #[tokio::test] - async fn define_tool_builds_schema_and_dispatches() { - use serde::Deserialize; - - #[derive(Deserialize, schemars::JsonSchema)] - struct Params { - city: String, - } - - let tool = define_tool( - "weather", - "Get the weather for a city", - |_inv, params: Params| async move { - Ok(ToolResult::Text(format!("sunny in {}", params.city))) - }, - ); - - assert_eq!(tool.name, "weather"); - assert_eq!(tool.description, "Get the weather for a city"); - assert_eq!(tool.parameters["type"], "object"); - assert!(tool.parameters["properties"]["city"].is_object()); - let handler = tool.handler.as_ref().expect("define_tool attaches handler"); - - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "weather".to_string(), - arguments: serde_json::json!({"city": "Seattle"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - match handler.call(inv).await.unwrap() { - ToolResult::Text(s) => assert_eq!(s, "sunny in Seattle"), - _ => panic!("expected Text result"), - } - } - - // Tests requiring `schemars` (the `derive` feature). - #[cfg(feature = "derive")] - mod derive_tests { - use serde::Deserialize; - - use super::super::*; - use crate::{ErrorKind, SessionId}; - - #[derive(Deserialize, schemars::JsonSchema)] - struct GetWeatherParams { - /// City name to get weather for. - city: String, - /// Temperature unit (celsius or fahrenheit). - unit: Option, - } - - #[test] - fn schema_for_generates_clean_schema() { - let schema = schema_for::(); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["city"].is_object()); - assert!(schema["properties"]["unit"].is_object()); - // city is required (non-Option), unit is not - let required = schema["required"].as_array().unwrap(); - assert!(required.contains(&serde_json::json!("city"))); - assert!(!required.contains(&serde_json::json!("unit"))); - // Root-level metadata stripped - assert!(schema.get("$schema").is_none()); - assert!(schema.get("title").is_none()); - } - - struct GetWeatherTool; - - fn get_weather_tool() -> Tool { - Tool { - name: "get_weather".to_string(), - description: "Get weather for a city".to_string(), - parameters: tool_parameters(schema_for::()), - ..Default::default() - } - .with_handler(std::sync::Arc::new(GetWeatherTool)) - } - - #[async_trait] - impl ToolHandler for GetWeatherTool { - async fn call(&self, inv: ToolInvocation) -> Result { - let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; - Ok(ToolResult::Text(format!( - "{} {}", - params.city, - params.unit.unwrap_or_default() - ))) - } - } - - #[test] - fn tool_handler_with_schema_for() { - let def = get_weather_tool(); - assert_eq!(def.name, "get_weather"); - let schema = serde_json::to_value(&def.parameters).expect("serialize tool parameters"); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["city"].is_object()); - assert!(def.handler.is_some()); - } - - #[tokio::test] - async fn tool_handler_deserializes_typed_params() { - let tool = GetWeatherTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"city": "Seattle", "unit": "celsius"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let result = tool.call(inv).await.unwrap(); - match result { - ToolResult::Text(s) => assert_eq!(s, "Seattle celsius"), - _ => panic!("expected Text result"), - } - } - - #[tokio::test] - async fn tool_handler_returns_error_on_bad_params() { - let tool = GetWeatherTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"wrong_field": 42}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let err = tool.call(inv).await.unwrap_err(); - assert!(matches!(err.kind(), ErrorKind::Json)); - } - - #[tokio::test] - async fn schema_for_derived_tool_round_trips_through_call() { - let tool = GetWeatherTool; - - // Calling the tool with matching arguments returns the - // expected typed result. (Per-name dispatch is the SDK's - // concern; here we exercise just the handler contract.) - let result = tool - .call(ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"city": "Portland"}), - available_tools: None, - traceparent: None, - tracestate: None, - }) - .await - .expect("ToolHandler::call should succeed for matching args"); - match result { - ToolResult::Text(s) => assert!(s.contains("Portland")), - _ => panic!("expected ToolResult::Text"), - } - } - } -} +mod tests; diff --git a/rust/src/tool/tests.rs b/rust/src/tool/tests.rs new file mode 100644 index 0000000000..e9a0192964 --- /dev/null +++ b/rust/src/tool/tests.rs @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; +use crate::types::SessionId; + +struct EchoTool; + +fn echo_tool() -> Tool { + Tool { + name: "echo".to_string(), + description: "Echo the input".to_string(), + parameters: tool_parameters(serde_json::json!({"type": "object"})), + ..Default::default() + } + .with_handler(std::sync::Arc::new(EchoTool)) +} + +#[async_trait] +impl ToolHandler for EchoTool { + async fn call(&self, inv: ToolInvocation) -> Result { + Ok(ToolResult::Text(inv.arguments.to_string())) + } +} + +#[test] +fn tool_handler_returns_tool_definition() { + let def = echo_tool(); + assert_eq!(def.name, "echo"); + assert_eq!(def.description, "Echo the input"); + assert!(def.parameters.contains_key("type")); + assert!(def.handler.is_some()); +} + +#[test] +fn try_tool_parameters_rejects_non_object_schema() { + let err = try_tool_parameters(serde_json::json!(["not", "an", "object"])) + .expect_err("non-object schemas should be rejected"); + + assert!(err.is_data()); +} + +#[test] +fn tool_parameters_serialize_in_deterministic_order() { + // Regression: `Tool.parameters` was a `HashMap`, whose per-instance + // random iteration order made the serialized top-level schema keys + // differ between constructions (and between sessions), busting the + // model provider's prompt cache. `IndexMap` keeps the order stable. + let schema = serde_json::json!({ + "type": "object", + "properties": { + "url": { "type": "string" }, + "count": { "type": "integer" } + }, + "required": ["url"], + "additionalProperties": false + }); + + let build = || Tool { + name: "fetch".to_string(), + parameters: tool_parameters(schema.clone()), + ..Default::default() + }; + + let expected = serde_json::to_string(&build()).expect("serialize tool"); + for _ in 0..64 { + let actual = serde_json::to_string(&build()).expect("serialize tool"); + assert_eq!(actual, expected); + } + + // Pin the exact top-level key order so a regression to any + // order-randomizing container is caught, not just internal drift. + let tool = build(); + let keys: Vec<&str> = tool.parameters.keys().map(String::as_str).collect(); + assert_eq!( + keys, + ["additionalProperties", "properties", "required", "type"] + ); +} + +#[test] +fn convert_mcp_call_tool_result_collects_text_and_binary_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "isError": true, + "content": [ + { "type": "text", "text": "hello" }, + { "type": "image", "data": "aW1n", "mimeType": "image/png" }, + { + "type": "resource", + "resource": { + "uri": "file:///tmp/data.bin", + "blob": "Ymlu", + "mimeType": "application/octet-stream", + "text": "resource text" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); + assert_eq!(expanded.result_type, "failure"); + let binary_results = expanded + .binary_results_for_llm + .expect("binary results should be captured"); + assert_eq!(binary_results.len(), 2); + assert_eq!(binary_results[0].r#type, "image"); + assert_eq!(binary_results[0].data, "aW1n"); + assert_eq!(binary_results[0].mime_type, "image/png"); + assert_eq!( + binary_results[1].description.as_deref(), + Some("file:///tmp/data.bin") + ); +} + +#[test] +fn convert_mcp_call_tool_result_converts_image_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/jpeg" } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, ""); + assert_eq!(expanded.result_type, "success"); + let binary_results = expanded + .binary_results_for_llm + .expect("image result should be captured"); + assert_eq!(binary_results.len(), 1); + assert_eq!(binary_results[0].data, "aW1hZ2U="); + assert_eq!(binary_results[0].mime_type, "image/jpeg"); + assert_eq!(binary_results[0].r#type, "image"); + assert!(binary_results[0].description.is_none()); +} + +#[test] +fn convert_mcp_call_tool_result_converts_resource_blob_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { + "type": "resource", + "resource": { + "uri": "file:///tmp/report.pdf", + "blob": "cGRm", + "mimeType": "application/pdf" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + let binary_results = expanded + .binary_results_for_llm + .expect("resource result should be captured"); + assert_eq!(binary_results.len(), 1); + assert_eq!(binary_results[0].data, "cGRm"); + assert_eq!(binary_results[0].mime_type, "application/pdf"); + assert_eq!(binary_results[0].r#type, "resource"); + assert_eq!( + binary_results[0].description.as_deref(), + Some("file:///tmp/report.pdf") + ); +} + +#[test] +fn convert_mcp_call_tool_result_defaults_resource_blob_mime_type() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { + "type": "resource", + "resource": { + "uri": "file:///tmp/data.bin", + "blob": "Ymlu" + } + }, + { + "type": "resource", + "resource": { + "blob": "YmluMg==", + "mimeType": "" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + let binary_results = expanded + .binary_results_for_llm + .expect("resource blobs should be captured"); + assert_eq!(binary_results.len(), 2); + assert_eq!(binary_results[0].mime_type, "application/octet-stream"); + assert_eq!(binary_results[1].mime_type, "application/octet-stream"); +} + +#[test] +fn convert_mcp_call_tool_result_omits_binary_results_without_binary_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { "type": "text", "text": "hello" }, + { + "type": "resource", + "resource": { + "uri": "file:///tmp/readme.md", + "text": "resource text" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); + assert!(expanded.binary_results_for_llm.is_none()); +} + +#[tokio::test] +async fn tool_handler_call_returns_result() { + let tool = EchoTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "echo".to_string(), + arguments: serde_json::json!({"msg": "hello"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let result = tool.call(inv).await.unwrap(); + match result { + ToolResult::Text(s) => assert!(s.contains("hello")), + _ => panic!("expected Text result"), + } +} + +#[cfg(feature = "derive")] +#[tokio::test] +async fn define_tool_builds_schema_and_dispatches() { + use serde::Deserialize; + + #[derive(Deserialize, schemars::JsonSchema)] + struct Params { + city: String, + } + + let tool = define_tool( + "weather", + "Get the weather for a city", + |_inv, params: Params| async move { Ok(ToolResult::Text(format!("sunny in {}", params.city))) }, + ); + + assert_eq!(tool.name, "weather"); + assert_eq!(tool.description, "Get the weather for a city"); + assert_eq!(tool.parameters["type"], "object"); + assert!(tool.parameters["properties"]["city"].is_object()); + let handler = tool.handler.as_ref().expect("define_tool attaches handler"); + + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "weather".to_string(), + arguments: serde_json::json!({"city": "Seattle"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + match handler.call(inv).await.unwrap() { + ToolResult::Text(s) => assert_eq!(s, "sunny in Seattle"), + _ => panic!("expected Text result"), + } +} + +// Tests requiring `schemars` (the `derive` feature). +#[cfg(feature = "derive")] +mod derive_tests { + use serde::Deserialize; + + use super::super::*; + use crate::{ErrorKind, SessionId}; + + #[derive(Deserialize, schemars::JsonSchema)] + struct GetWeatherParams { + /// City name to get weather for. + city: String, + /// Temperature unit (celsius or fahrenheit). + unit: Option, + } + + #[test] + fn schema_for_generates_clean_schema() { + let schema = schema_for::(); + assert_eq!(schema["type"], "object"); + assert!(schema["properties"]["city"].is_object()); + assert!(schema["properties"]["unit"].is_object()); + // city is required (non-Option), unit is not + let required = schema["required"].as_array().unwrap(); + assert!(required.contains(&serde_json::json!("city"))); + assert!(!required.contains(&serde_json::json!("unit"))); + // Root-level metadata stripped + assert!(schema.get("$schema").is_none()); + assert!(schema.get("title").is_none()); + } + + struct GetWeatherTool; + + fn get_weather_tool() -> Tool { + Tool { + name: "get_weather".to_string(), + description: "Get weather for a city".to_string(), + parameters: tool_parameters(schema_for::()), + ..Default::default() + } + .with_handler(std::sync::Arc::new(GetWeatherTool)) + } + + #[async_trait] + impl ToolHandler for GetWeatherTool { + async fn call(&self, inv: ToolInvocation) -> Result { + let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; + Ok(ToolResult::Text(format!( + "{} {}", + params.city, + params.unit.unwrap_or_default() + ))) + } + } + + #[test] + fn tool_handler_with_schema_for() { + let def = get_weather_tool(); + assert_eq!(def.name, "get_weather"); + let schema = serde_json::to_value(&def.parameters).expect("serialize tool parameters"); + assert_eq!(schema["type"], "object"); + assert!(schema["properties"]["city"].is_object()); + assert!(def.handler.is_some()); + } + + #[tokio::test] + async fn tool_handler_deserializes_typed_params() { + let tool = GetWeatherTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"city": "Seattle", "unit": "celsius"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let result = tool.call(inv).await.unwrap(); + match result { + ToolResult::Text(s) => assert_eq!(s, "Seattle celsius"), + _ => panic!("expected Text result"), + } + } + + #[tokio::test] + async fn tool_handler_returns_error_on_bad_params() { + let tool = GetWeatherTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"wrong_field": 42}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let err = tool.call(inv).await.unwrap_err(); + assert!(matches!(err.kind(), ErrorKind::Json)); + } + + #[tokio::test] + async fn schema_for_derived_tool_round_trips_through_call() { + let tool = GetWeatherTool; + + // Calling the tool with matching arguments returns the + // expected typed result. (Per-name dispatch is the SDK's + // concern; here we exercise just the handler contract.) + let result = tool + .call(ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"city": "Portland"}), + available_tools: None, + traceparent: None, + tracestate: None, + }) + .await + .expect("ToolHandler::call should succeed for matching args"); + match result { + ToolResult::Text(s) => assert!(s.contains("Portland")), + _ => panic!("expected ToolResult::Text"), + } + } +} diff --git a/rust/src/trace_context.rs b/rust/src/trace_context.rs index 287c87cbd3..7344e07167 100644 --- a/rust/src/trace_context.rs +++ b/rust/src/trace_context.rs @@ -102,31 +102,4 @@ pub(crate) fn inject_trace_context(params: &mut serde_json::Value, ctx: &TraceCo } #[cfg(test)] -mod tests { - use super::TraceContext; - - #[test] - fn new_yields_empty_context() { - let ctx = TraceContext::new(); - assert!(ctx.is_empty()); - assert!(ctx.traceparent.is_none()); - assert!(ctx.tracestate.is_none()); - } - - #[test] - fn builder_composes_traceparent_and_tracestate() { - let ctx = TraceContext::new() - .with_traceparent("00-trace-span-01") - .with_tracestate("vendor=key"); - assert_eq!(ctx.traceparent.as_deref(), Some("00-trace-span-01")); - assert_eq!(ctx.tracestate.as_deref(), Some("vendor=key")); - assert!(!ctx.is_empty()); - } - - #[test] - fn from_traceparent_matches_builder() { - let direct = TraceContext::from_traceparent("00-trace-span-01"); - let chained = TraceContext::new().with_traceparent("00-trace-span-01"); - assert_eq!(direct, chained); - } -} +mod tests; diff --git a/rust/src/trace_context/tests.rs b/rust/src/trace_context/tests.rs new file mode 100644 index 0000000000..ff151b80cb --- /dev/null +++ b/rust/src/trace_context/tests.rs @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::TraceContext; + +#[test] +fn new_yields_empty_context() { + let ctx = TraceContext::new(); + assert!(ctx.is_empty()); + assert!(ctx.traceparent.is_none()); + assert!(ctx.tracestate.is_none()); +} + +#[test] +fn builder_composes_traceparent_and_tracestate() { + let ctx = TraceContext::new() + .with_traceparent("00-trace-span-01") + .with_tracestate("vendor=key"); + assert_eq!(ctx.traceparent.as_deref(), Some("00-trace-span-01")); + assert_eq!(ctx.tracestate.as_deref(), Some("vendor=key")); + assert!(!ctx.is_empty()); +} + +#[test] +fn from_traceparent_matches_builder() { + let direct = TraceContext::from_traceparent("00-trace-span-01"); + let chained = TraceContext::new().with_traceparent("00-trace-span-01"); + assert_eq!(direct, chained); +} diff --git a/rust/src/transforms.rs b/rust/src/transforms.rs index a090bc6494..dc93094c25 100644 --- a/rust/src/transforms.rs +++ b/rust/src/transforms.rs @@ -110,114 +110,4 @@ pub(crate) async fn dispatch_transform( } #[cfg(test)] -mod tests { - use super::*; - - struct TestTransform; - - #[async_trait] - impl SystemMessageTransform for TestTransform { - fn section_ids(&self) -> Vec { - vec!["instructions".to_string(), "context".to_string()] - } - - async fn transform_section( - &self, - section_id: &str, - content: &str, - _ctx: TransformContext, - ) -> Option { - match section_id { - "instructions" => Some(format!("[modified] {content}")), - _ => None, - } - } - } - - #[tokio::test] - async fn dispatch_applies_matching_transform() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "instructions".to_string(), - TransformSection { - content: "be helpful".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!( - response.sections["instructions"].content, - "[modified] be helpful" - ); - } - - #[tokio::test] - async fn dispatch_passes_through_unhandled_section() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "context".to_string(), - TransformSection { - content: "original context".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!(response.sections["context"].content, "original context"); - } - - #[tokio::test] - async fn dispatch_unknown_section_passes_through() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "unknown".to_string(), - TransformSection { - content: "mystery".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!(response.sections["unknown"].content, "mystery"); - } - - #[tokio::test] - async fn dispatch_mixed_sections() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "instructions".to_string(), - TransformSection { - content: "help me".to_string(), - }, - ); - sections.insert( - "context".to_string(), - TransformSection { - content: "some context".to_string(), - }, - ); - sections.insert( - "other".to_string(), - TransformSection { - content: "other stuff".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!( - response.sections["instructions"].content, - "[modified] help me" - ); - assert_eq!(response.sections["context"].content, "some context"); - assert_eq!(response.sections["other"].content, "other stuff"); - } - - #[tokio::test] - async fn section_ids_returns_registered_sections() { - let transform = TestTransform; - let ids = transform.section_ids(); - assert_eq!(ids, vec!["instructions", "context"]); - } -} +mod tests; diff --git a/rust/src/transforms/tests.rs b/rust/src/transforms/tests.rs new file mode 100644 index 0000000000..c56f804920 --- /dev/null +++ b/rust/src/transforms/tests.rs @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +struct TestTransform; + +#[async_trait] +impl SystemMessageTransform for TestTransform { + fn section_ids(&self) -> Vec { + vec!["instructions".to_string(), "context".to_string()] + } + + async fn transform_section( + &self, + section_id: &str, + content: &str, + _ctx: TransformContext, + ) -> Option { + match section_id { + "instructions" => Some(format!("[modified] {content}")), + _ => None, + } + } +} + +#[tokio::test] +async fn dispatch_applies_matching_transform() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "instructions".to_string(), + TransformSection { + content: "be helpful".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!( + response.sections["instructions"].content, + "[modified] be helpful" + ); +} + +#[tokio::test] +async fn dispatch_passes_through_unhandled_section() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "context".to_string(), + TransformSection { + content: "original context".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!(response.sections["context"].content, "original context"); +} + +#[tokio::test] +async fn dispatch_unknown_section_passes_through() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "unknown".to_string(), + TransformSection { + content: "mystery".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!(response.sections["unknown"].content, "mystery"); +} + +#[tokio::test] +async fn dispatch_mixed_sections() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "instructions".to_string(), + TransformSection { + content: "help me".to_string(), + }, + ); + sections.insert( + "context".to_string(), + TransformSection { + content: "some context".to_string(), + }, + ); + sections.insert( + "other".to_string(), + TransformSection { + content: "other stuff".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!( + response.sections["instructions"].content, + "[modified] help me" + ); + assert_eq!(response.sections["context"].content, "some context"); + assert_eq!(response.sections["other"].content, "other stuff"); +} + +#[tokio::test] +async fn section_ids_returns_registered_sections() { + let transform = TestTransform; + let ids = transform.section_ids(); + assert_eq!(ids, vec!["instructions", "context"]); +} diff --git a/rust/src/types.rs b/rust/src/types.rs index 5c5d5b17b6..7a272cecfc 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -43,11 +43,12 @@ use crate::handler::{ use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; pub use crate::session_fs::{ - DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig, - SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, - SessionFsSqliteQueryType, SessionFsSqliteTransactionError, + DirEntry, DirEntryKind, FileInfo, FsError, SessionFsBinaryProvider, SessionFsCapabilities, + SessionFsConfig, SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, + SessionFsSqliteQueryResult, SessionFsSqliteQueryType, SessionFsSqliteTransactionError, SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement, }; +use crate::skill_provider::SkillProvider; pub use crate::trace_context::{TraceContext, TraceContextProvider}; use crate::transforms::SystemMessageTransform; @@ -532,6 +533,29 @@ impl Tool { } } +/// Handlers for a session's client-supplied tools, keyed by tool name. +pub(crate) type ToolHandlerMap = HashMap>; + +/// Move each tool's handler into a [`ToolHandlerMap`], leaving declaration-only +/// definitions to send on the wire. +/// +/// Two handlers for one name are rejected because dispatch, which is keyed by +/// name, could only ever reach one of them. +pub(crate) fn take_tool_handlers(tools: &mut [Tool]) -> Result { + let mut handlers = ToolHandlerMap::new(); + for tool in tools { + if let Some(handler) = tool.handler.take() + && handlers.insert(tool.name.clone(), handler).is_some() + { + return Err(crate::Error::with_message( + crate::ErrorKind::InvalidConfig, + format!("duplicate tool handler registered for name {:?}", tool.name), + )); + } + } + Ok(handlers) +} + impl std::fmt::Debug for Tool { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Tool") @@ -1245,6 +1269,12 @@ pub struct ProviderConfig { /// Applies to OpenAI-compatible providers using `wire_api` `"responses"`. #[serde(default, skip_serializing_if = "Option::is_none")] pub transport: Option, + /// Product serving the model, such as `"ollama"` or `"lm_studio"`, + /// reported in telemetry as `model_provider`. Allowed values are + /// `"openai"`, `"anthropic"`, `"azure_openai"`, `"ollama"`, + /// `"lm_studio"`, `"foundry_local"`, and `"llama_cpp"`; only affects telemetry. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// API endpoint URL. pub base_url: String, /// API key. Optional for local providers like Ollama. @@ -1298,6 +1328,7 @@ impl std::fmt::Debug for ProviderConfig { .field("provider_type", &self.provider_type) .field("wire_api", &self.wire_api) .field("transport", &self.transport) + .field("model_provider", &self.model_provider) .field("base_url", &self.base_url) .field("api_key", &self.api_key) .field("bearer_token", &self.bearer_token) @@ -1345,6 +1376,14 @@ impl ProviderConfig { self } + /// Set the product serving the model. Allowed values are `"openai"`, + /// `"anthropic"`, `"azure_openai"`, `"ollama"`, `"lm_studio"`, + /// `"foundry_local"`, and `"llama_cpp"`. Only affects telemetry. + pub fn with_model_provider(mut self, model_provider: impl Into) -> Self { + self.model_provider = Some(model_provider.into()); + self + } + /// Set the API key. Optional for local providers like Ollama. pub fn with_api_key(mut self, api_key: impl Into) -> Self { self.api_key = Some(api_key.into()); @@ -1503,6 +1542,12 @@ pub struct NamedProviderConfig { /// Defaults to `"completions"`. #[serde(default, skip_serializing_if = "Option::is_none")] pub wire_api: Option, + /// Product serving this provider's models, such as `"ollama"` or + /// `"lm_studio"`, reported in telemetry as `model_provider`. Allowed values + /// are `"openai"`, `"anthropic"`, `"azure_openai"`, `"ollama"`, + /// `"lm_studio"`, `"foundry_local"`, and `"llama_cpp"`; only affects telemetry. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// API endpoint URL. pub base_url: String, /// API key. Optional for local providers like Ollama. @@ -1532,6 +1577,7 @@ impl std::fmt::Debug for NamedProviderConfig { .field("name", &self.name) .field("provider_type", &self.provider_type) .field("wire_api", &self.wire_api) + .field("model_provider", &self.model_provider) .field("base_url", &self.base_url) .field("api_key", &self.api_key) .field("bearer_token", &self.bearer_token) @@ -1569,6 +1615,14 @@ impl NamedProviderConfig { self } + /// Set the product serving this provider's models. Allowed values are + /// `"openai"`, `"anthropic"`, `"azure_openai"`, `"ollama"`, + /// `"lm_studio"`, `"foundry_local"`, and `"llama_cpp"`. Only affects telemetry. + pub fn with_model_provider(mut self, model_provider: impl Into) -> Self { + self.model_provider = Some(model_provider.into()); + self + } + /// Set the API key. Optional for local providers like Ollama. pub fn with_api_key(mut self, api_key: impl Into) -> Self { self.api_key = Some(api_key.into()); @@ -1827,6 +1881,12 @@ pub struct ManagedSettingsPermissions { /// Tool-permission patterns that are allowed without prompting. #[serde(default, skip_serializing_if = "Option::is_none")] pub allow: Option>, + /// Closed-world host boundary expressed as `Domain(hostname)`, + /// `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, + /// queries, and fragments are rejected. Multiple managed layers intersect + /// their lists. A present empty list denies all hosts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit_to: Option>, } impl ManagedSettingsPermissions { @@ -1853,6 +1913,12 @@ impl ManagedSettingsPermissions { self.allow = Some(rules); self } + + /// Sets the closed-world domain boundary for this managed layer. + pub fn with_limit_to(mut self, rules: Vec) -> Self { + self.limit_to = Some(rules); + self + } } /// Managed-settings layer injected at session startup. Currently carries only a @@ -2062,6 +2128,13 @@ pub struct SessionConfig { pub enable_session_store: Option, /// When true, enables skills for this session. pub enable_skills: Option, + /// **Experimental.** Supplies session-scoped skills from the SDK host. + /// + /// The provider is runtime-only and is not serialized. Set + /// [`enable_skills`](Self::enable_skills) to `Some(true)` when using this + /// in [`ClientMode::Empty`](crate::ClientMode::Empty), where built-in + /// skill loading is otherwise disabled by default. + pub skill_provider: Option>, /// **Experimental.** This option is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -2381,6 +2454,10 @@ impl std::fmt::Debug for SessionConfig { ) .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) + .field( + "skill_provider", + &self.skill_provider.as_ref().map(|_| ""), + ) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) @@ -2512,6 +2589,7 @@ impl Default for SessionConfig { enable_host_git_operations: None, enable_session_store: None, enable_skills: None, + skill_provider: None, embedding_cache_storage: None, enable_mcp_apps: None, github_mcp_tool_config: None, @@ -2585,9 +2663,10 @@ pub(crate) struct SessionConfigRuntime { pub auto_mode_switch_handler: Option>, pub hooks_handler: Option>, pub system_message_transform: Option>, - pub tool_handlers: HashMap>, + pub tool_handlers: ToolHandlerMap, pub canvas_handler: Option>, pub session_fs_provider: Option>, + pub skill_provider: Option>, pub bearer_token_providers: HashMap>, pub github_token_provider: Option>, pub commands: Option>, @@ -2622,20 +2701,9 @@ impl SessionConfig { let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.is_some(); + let has_skill_provider = self.skill_provider.is_some(); - let mut tool_handlers: HashMap> = HashMap::new(); - if let Some(tools) = self.tools.as_mut() { - for tool in tools.iter_mut() { - if let Some(handler) = tool.handler.take() - && tool_handlers.insert(tool.name.clone(), handler).is_some() - { - return Err(crate::Error::with_message( - crate::ErrorKind::InvalidConfig, - format!("duplicate tool handler registered for name {:?}", tool.name), - )); - } - } - } + let tool_handlers = take_tool_handlers(self.tools.as_deref_mut().unwrap_or_default())?; let wire_commands = self.commands.as_ref().map(|cmds| { cmds.iter() @@ -2687,6 +2755,7 @@ impl SessionConfig { enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, + has_skill_provider: has_skill_provider.then_some(true), request_user_input, request_permission: permission_active, request_exit_plan_mode, @@ -2747,6 +2816,7 @@ impl SessionConfig { tool_handlers, canvas_handler, session_fs_provider: self.session_fs_provider, + skill_provider: self.skill_provider, bearer_token_providers, github_token_provider: self.github_token_provider, commands: self.commands, @@ -3110,6 +3180,14 @@ impl SessionConfig { self } + /// Install an experimental session-scoped [`SkillProvider`]. + /// + /// The provider is not serialized and must be re-supplied on resume. + pub fn with_skill_provider(mut self, provider: Arc) -> Self { + self.skill_provider = Some(provider); + self + } + /// **Experimental.** This method is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -3558,6 +3636,11 @@ pub struct ResumeSessionConfig { pub enable_session_store: Option, /// When true, enables skills on resume. pub enable_skills: Option, + /// **Experimental.** Supplies session-scoped skills from the SDK host. + /// + /// The provider is runtime-only and is not serialized. Re-supply it on + /// each resume because providers are not persisted by the runtime. + pub skill_provider: Option>, /// **Experimental.** This option is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -3693,9 +3776,15 @@ pub struct ResumeSessionConfig { /// was dropped. Use this together with [`Client::force_stop`] to hand /// off a session from one process to another without losing in-flight /// work. + /// When omitted or `false` (the default), work still pending on resume + /// is treated as interrupted; completed tool results already recorded by + /// the runtime are preserved. /// /// [`Client::force_stop`]: crate::Client::force_stop pub continue_pending_work: Option, + /// Permit recovery of a damaged transcript on resume. Defaults to `true` + /// in all modes when unset. Set `false` to reject recovery. + pub allow_transcript_recovery: Option, /// Optional permission-request handler. See /// [`SessionConfig::permission_handler`]. pub permission_handler: Option>, @@ -3793,6 +3882,10 @@ impl std::fmt::Debug for ResumeSessionConfig { ) .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) + .field( + "skill_provider", + &self.skill_provider.as_ref().map(|_| ""), + ) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) @@ -3902,20 +3995,9 @@ impl ResumeSessionConfig { let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.is_some(); + let has_skill_provider = self.skill_provider.is_some(); - let mut tool_handlers: HashMap> = HashMap::new(); - if let Some(tools) = self.tools.as_mut() { - for tool in tools.iter_mut() { - if let Some(handler) = tool.handler.take() - && tool_handlers.insert(tool.name.clone(), handler).is_some() - { - return Err(crate::Error::with_message( - crate::ErrorKind::InvalidConfig, - format!("duplicate tool handler registered for name {:?}", tool.name), - )); - } - } - } + let tool_handlers = take_tool_handlers(self.tools.as_deref_mut().unwrap_or_default())?; let wire_commands = self.commands.as_ref().map(|cmds| { cmds.iter() @@ -3967,6 +4049,7 @@ impl ResumeSessionConfig { enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, + has_skill_provider: has_skill_provider.then_some(true), request_user_input, request_permission: permission_active, request_exit_plan_mode, @@ -4013,6 +4096,7 @@ impl ResumeSessionConfig { managed_settings: self.managed_settings, suppress_resume_event: self.suppress_resume_event, continue_pending_work: self.continue_pending_work, + allow_transcript_recovery: self.allow_transcript_recovery, }; let runtime = SessionConfigRuntime { @@ -4028,6 +4112,7 @@ impl ResumeSessionConfig { tool_handlers, canvas_handler, session_fs_provider: self.session_fs_provider, + skill_provider: self.skill_provider, bearer_token_providers, github_token_provider: self.github_token_provider, commands: self.commands, @@ -4077,6 +4162,7 @@ impl ResumeSessionConfig { enable_host_git_operations: None, enable_session_store: None, enable_skills: None, + skill_provider: None, embedding_cache_storage: None, enable_mcp_apps: None, github_mcp_tool_config: None, @@ -4117,6 +4203,7 @@ impl ResumeSessionConfig { session_fs_provider: None, suppress_resume_event: None, continue_pending_work: None, + allow_transcript_recovery: None, permission_handler: None, elicitation_handler: None, mcp_auth_handler: None, @@ -4211,6 +4298,14 @@ impl ResumeSessionConfig { self } + /// Install an experimental session-scoped [`SkillProvider`]. + /// + /// The provider is not persisted and must be re-supplied on each resume. + pub fn with_skill_provider(mut self, provider: Arc) -> Self { + self.skill_provider = Some(provider); + self + } + /// Auto-approve every permission request on the resumed session. See /// [`SessionConfig::approve_all_permissions`]. pub fn approve_all_permissions(mut self) -> Self { @@ -4727,11 +4822,19 @@ impl ResumeSessionConfig { /// was dropped. Use this together with /// [`Client::force_stop`](crate::Client::force_stop) to hand off a /// session from one process to another without losing in-flight work. + /// When `false` (the default), pending work is treated as interrupted on + /// resume; already-recorded tool results are preserved. pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self { self.continue_pending_work = Some(continue_pending); self } + /// Set [`Self::allow_transcript_recovery`]. + pub fn with_allow_transcript_recovery(mut self, allow: bool) -> Self { + self.allow_transcript_recovery = Some(allow); + self + } + /// Set [`Self::skip_custom_instructions`]. pub fn with_skip_custom_instructions(mut self, value: bool) -> Self { self.skip_custom_instructions = Some(value); @@ -4887,6 +4990,18 @@ pub struct CreateSessionResult { pub capabilities: Option, } +/// Details of transcript repair planned during resume. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TranscriptRecovery { + /// Planned backup path; the backup is written on the next append. + pub planned_backup_path: String, + /// One-based physical line numbers removed from the transcript. + pub invalid_line_numbers: Vec, + /// Whether a valid session.start event was moved to the beginning. + pub session_start_moved: bool, +} + /// Response from `session.resume`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4910,6 +5025,9 @@ pub(crate) struct ResumeSessionResult { skip_serializing_if = "Option::is_none" )] pub open_canvases: Option>, + /// Recovery performed in memory while loading the session. + #[serde(default)] + pub transcript_recovery: Option, } /// Severity level for [`Session::log`](crate::session::Session::log) messages. @@ -6354,2205 +6472,13 @@ impl Default for ExitPlanModeData { } #[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::path::PathBuf; - - use serde_json::json; - - use super::{ - AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, - ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, - ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, - InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, - MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, - ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, - SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, - }; - use crate::generated::session_events::TypedSessionEvent; - - #[test] - fn permission_response_capability_is_publicly_exported() { - assert_eq!( - serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(), - json!("interactive") - ); - } - - #[test] - fn tool_builder_composes() { - let tool = Tool::new("greet") - .with_description("Say hello") - .with_namespaced_name("hello/greet") - .with_instructions("Pass the user's name") - .with_parameters(json!({ - "type": "object", - "properties": { "name": { "type": "string" } }, - "required": ["name"] - })) - .with_overrides_built_in_tool(true) - .with_skip_permission(true); - assert_eq!(tool.name, "greet"); - assert_eq!(tool.description, "Say hello"); - assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet")); - assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name")); - assert_eq!(tool.parameters.get("type").unwrap(), &json!("object")); - assert!(tool.overrides_built_in_tool); - assert!(tool.skip_permission); - } - - #[test] - fn tool_defer_serialization() { - let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto); - assert_eq!(tool.defer, Some(super::DeferMode::Auto)); - let value = serde_json::to_value(&tool).unwrap(); - assert_eq!(value.get("defer").unwrap(), &json!("auto")); - - let plain = Tool::new("plain"); - let value = serde_json::to_value(&plain).unwrap(); - assert!(value.get("defer").is_none()); - } - - #[test] - fn tool_metadata_serialization() { - use indexmap::IndexMap; - - let mut metadata = IndexMap::new(); - metadata.insert( - "github.com/copilot:safeForTelemetry".to_string(), - json!({ "name": true, "inputsNames": false }), - ); - let tool = Tool::new("lookup").with_metadata(metadata); - let value = serde_json::to_value(&tool).unwrap(); - assert_eq!( - value - .get("metadata") - .unwrap() - .get("github.com/copilot:safeForTelemetry") - .unwrap(), - &json!({ "name": true, "inputsNames": false }) - ); - - // Empty metadata is omitted on the wire. - let plain = Tool::new("plain"); - let value = serde_json::to_value(&plain).unwrap(); - assert!(value.get("metadata").is_none()); - } - - #[test] - fn custom_agent_config_builder_with_model() { - let agent = CustomAgentConfig::new("my-agent", "You are helpful.") - .with_model("claude-haiku-4.5") - .with_display_name("My Agent"); - assert_eq!(agent.name, "my-agent"); - assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5")); - assert_eq!(agent.display_name.as_deref(), Some("My Agent")); - } - - #[test] - fn custom_agent_config_serializes_model() { - let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5"); - let wire = serde_json::to_value(&agent).unwrap(); - assert_eq!(wire["model"], "claude-haiku-4.5"); - assert_eq!(wire["name"], "model-agent"); - } - - #[test] - fn custom_agent_config_omits_model_when_none() { - let agent = CustomAgentConfig::new("no-model-agent", "prompt"); - let wire = serde_json::to_value(&agent).unwrap(); - assert!(wire.get("model").is_none()); - } - - #[test] - fn custom_agent_config_builder_with_reasoning_effort() { - let agent = - CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); - assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); - } - - #[test] - fn custom_agent_config_serializes_reasoning_effort() { - let agent = - CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); - let wire = serde_json::to_value(&agent).unwrap(); - assert_eq!(wire["reasoningEffort"], "high"); - } - - #[test] - fn custom_agent_config_omits_reasoning_effort_when_none() { - let agent = CustomAgentConfig::new("default-agent", "prompt"); - let wire = serde_json::to_value(&agent).unwrap(); - assert!(wire.get("reasoningEffort").is_none()); - } - - #[test] - #[should_panic(expected = "tool parameter schema must be a JSON object")] - fn tool_with_parameters_panics_on_non_object_value() { - let _ = Tool::new("noop").with_parameters(json!(null)); - } - - #[test] - fn tool_result_expanded_serializes_binary_results_for_llm() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: "rendered chart".to_string(), - result_type: "success".to_string(), - binary_results_for_llm: Some(vec![ToolBinaryResult { - data: "aW1n".to_string(), - mime_type: "image/png".to_string(), - r#type: "image".to_string(), - description: Some("chart preview".to_string()), - }]), - session_log: None, - error: None, - tool_telemetry: None, - tool_references: None, - }), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!( - wire, - json!({ - "result": { - "textResultForLlm": "rendered chart", - "resultType": "success", - "binaryResultsForLlm": [ - { - "data": "aW1n", - "mimeType": "image/png", - "type": "image", - "description": "chart preview" - } - ] - } - }) - ); - } - - #[test] - fn tool_result_expanded_omits_binary_results_for_llm_when_none() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: "ok".to_string(), - result_type: "success".to_string(), - binary_results_for_llm: None, - session_log: None, - error: None, - tool_telemetry: None, - tool_references: None, - }), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!(wire["result"]["textResultForLlm"], "ok"); - assert!(wire["result"].get("binaryResultsForLlm").is_none()); - } - - #[test] - fn tool_result_expanded_serializes_tool_references() { - let response = ToolResultResponse { - result: ToolResult::Expanded( - ToolResultExpanded::new("found 2 tools", "success") - .with_tool_references(["get_weather", "check_status"]), - ), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!( - wire, - json!({ - "result": { - "textResultForLlm": "found 2 tools", - "resultType": "success", - "toolReferences": ["get_weather", "check_status"] - } - }) - ); - } - - #[test] - fn tool_result_expanded_omits_tool_references_when_none() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!(wire["result"]["textResultForLlm"], "ok"); - assert!(wire["result"].get("toolReferences").is_none()); - } - - #[test] - fn tool_result_expanded_with_tool_references_accepts_owned_strings() { - // The builder is generic over `Into`, so an owned `Vec` - // must compile and populate the field just like a `&str` array. - let names: Vec = vec!["alpha".to_string(), "beta".to_string()]; - let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names); - - assert_eq!( - expanded.tool_references.as_deref(), - Some(["alpha".to_string(), "beta".to_string()].as_slice()) - ); - } - - #[test] - fn tool_result_expanded_deserializes_tool_references() { - let wire = json!({ - "textResultForLlm": "found tools", - "resultType": "success", - "toolReferences": ["alpha", "beta"] - }); - - let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap(); - - assert_eq!( - expanded.tool_references.as_deref(), - Some(["alpha".to_string(), "beta".to_string()].as_slice()) - ); - } - - #[test] - fn session_config_default_wire_flags_off_without_handlers() { - let cfg = SessionConfig::default(); - assert_eq!(cfg.mcp_oauth_token_storage, None); - assert_eq!(cfg.allowed_models, None); - // Wire flags are derived from handler presence at create_session - // time, not stored on the config. With no handlers installed, every - // request_* flag should serialize as false. - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("default-flags"))) - .expect("default config has no duplicate handlers"); - assert!(!wire.request_user_input); - assert!(!wire.request_permission); - assert!(!wire.request_elicitation); - assert!(!wire.request_exit_plan_mode); - assert!(!wire.request_auto_mode_switch); - assert!(!wire.hooks); - assert!(!wire.request_mcp_apps); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("askUserVariant").is_none()); - assert!(json.get("allowedModels").is_none()); - } - - #[test] - fn resume_session_config_new_wire_flags_off_without_handlers() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags")); - assert_eq!(cfg.mcp_oauth_token_storage, None); - assert_eq!(cfg.allowed_models, None); - let (wire, _runtime) = cfg - .into_wire() - .expect("default resume config has no duplicate handlers"); - assert!(!wire.request_user_input); - assert!(!wire.request_permission); - assert!(!wire.request_elicitation); - assert!(!wire.request_exit_plan_mode); - assert!(!wire.request_auto_mode_switch); - assert!(!wire.hooks); - assert!(!wire.request_mcp_apps); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("askUserVariant").is_none()); - assert!(json.get("allowedModels").is_none()); - } - - #[test] - fn session_configs_build_debug_and_serialize_allowed_models() { - let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]); - assert_eq!( - create.allowed_models.as_deref(), - Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..]) - ); - assert!(format!("{create:?}").contains("allowed_models")); - - let (create_wire, _) = create - .into_wire(Some(SessionId::from("create-allowed-models"))) - .expect("allowed model config has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!( - create_json["allowedModels"], - json!(["gpt-5.4", "claude-sonnet-4"]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models")) - .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]); - assert_eq!( - resume.allowed_models.as_deref(), - Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..]) - ); - assert!(format!("{resume:?}").contains("allowed_models")); - - let (resume_wire, _) = resume - .into_wire() - .expect("resume allowed model config has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!( - resume_json["allowedModels"], - json!(["gpt-5.4", "gpt-5-mini"]) - ); - } - - #[test] - fn custom_agents_local_only_serializes_on_create_and_resume() { - let (create_wire, _) = SessionConfig::default() - .with_custom_agents_local_only(false) - .into_wire(Some(SessionId::from("create-locality"))) - .expect("create config has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["customAgentsLocalOnly"], false); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality")) - .with_custom_agents_local_only(false) - .into_wire() - .expect("resume config has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["customAgentsLocalOnly"], false); - - let (unset_create_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("create-unset"))) - .expect("create config has no duplicate handlers"); - let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); - assert!(unset_create_json.get("customAgentsLocalOnly").is_none()); - - let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset")) - .into_wire() - .expect("resume config has no duplicate handlers"); - let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); - assert!(unset_resume_json.get("customAgentsLocalOnly").is_none()); - } - - #[test] - fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { - let cfg = SessionConfig::default().with_enable_mcp_apps(true); - assert_eq!(cfg.enable_mcp_apps, Some(true)); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("enable-mcp-apps"))) - .expect("enable_mcp_apps config has no duplicate handlers"); - assert!(wire.request_mcp_apps); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); - } - - #[test] - fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps")) - .with_enable_mcp_apps(true); - assert_eq!(cfg.enable_mcp_apps, Some(true)); - - let (wire, _runtime) = cfg - .into_wire() - .expect("resume enable_mcp_apps config has no duplicate handlers"); - assert!(wire.request_mcp_apps); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); - } - - #[test] - fn github_mcp_tool_config_serializes_for_create_and_resume() { - let github_config = GitHubMcpToolConfig::new() - .with_enable_all_tools(true) - .with_additional_toolsets(["repos"]) - .with_additional_tools(["get_issue"]) - .with_enable_insiders_mode(true) - .with_disable_form_deferral(true); - - let (create_wire, _) = SessionConfig::default() - .with_github_mcp_tool_config(github_config.clone()) - .into_wire(Some(SessionId::from("github-mcp"))) - .expect("create config has no duplicate handlers"); - assert_eq!( - serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"], - serde_json::json!({ - "enableAllTools": true, - "additionalToolsets": ["repos"], - "additionalTools": ["get_issue"], - "enableInsidersMode": true, - "disableFormDeferral": true, - }) - ); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp")) - .with_github_mcp_tool_config(github_config) - .into_wire() - .expect("resume config has no duplicate handlers"); - assert!(resume_wire.github_mcp_tool_config.is_some()); - - let (unset_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("github-mcp-unset"))) - .expect("default config has no duplicate handlers"); - assert!( - serde_json::to_value(&unset_wire) - .unwrap() - .get("githubMcpToolConfig") - .is_none() - ); - } - - #[test] - fn memory_configuration_constructors_and_serde() { - assert!(MemoryConfiguration::enabled().enabled); - assert!(!MemoryConfiguration::disabled().enabled); - assert!(MemoryConfiguration::disabled().with_enabled(true).enabled); - - let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap(); - assert_eq!(json, serde_json::json!({ "enabled": true })); - } - - #[test] - fn session_config_with_memory_serializes() { - let (wire, _runtime) = SessionConfig::default() - .with_memory(MemoryConfiguration::enabled()) - .into_wire(Some(SessionId::from("memory-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); - - let (wire_off, _) = SessionConfig::default() - .with_memory(MemoryConfiguration::disabled()) - .into_wire(Some(SessionId::from("memory-off"))) - .expect("no duplicate handlers"); - let json_off = serde_json::to_value(&wire_off).unwrap(); - assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false })); - - // Unset memory is omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("memory-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("memory").is_none()); - } - - #[test] - fn resume_session_config_with_memory_serializes() { - let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on")) - .with_memory(MemoryConfiguration::enabled()) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); - - // Unset memory is omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("memory").is_none()); - } - - #[test] - fn feature_flags_serialize_on_create_and_resume() { - let feature_flags = HashMap::from([ - ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), - ("DISABLED_TEST_FLAG".to_string(), false), - ]); - let expected = serde_json::json!({ - "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, - "DISABLED_TEST_FLAG": false, - }); - - let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); - assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); - let (create_wire, _) = create_config - .into_wire(Some(SessionId::from("feature-flags-create"))) - .expect("no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["featureFlags"], expected); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) - .with_feature_flags(feature_flags) - .into_wire() - .expect("no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["featureFlags"], expected); - - let (unset_create_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("feature-flags-create-unset"))) - .expect("no duplicate handlers"); - let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); - assert!(unset_create_json.get("featureFlags").is_none()); - - let (unset_resume_wire, _) = - ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) - .into_wire() - .expect("no duplicate handlers"); - let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); - assert!(unset_resume_json.get("featureFlags").is_none()); - } - - fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { - CopilotExpAssignmentResponse { - features: vec!["copilot_exp_flag".to_string()], - flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]), - configs: vec![ExpConfigEntry { - id: "cfg-1".to_string(), - parameters: HashMap::from([ - ("threshold".to_string(), ExpFlagValue::Integer(5)), - ("enabled".to_string(), ExpFlagValue::Bool(true)), - ]), - }], - assignment_context: context.to_string(), - ..Default::default() - } - } - - #[test] - fn exp_flag_value_round_trips_all_variants() { - let values = serde_json::json!({ - "s": "text", - "i": 7, - "f": 1.5, - "b": true, - "n": null, - }); - let parsed: HashMap = serde_json::from_value(values.clone()).unwrap(); - assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string())); - assert_eq!(parsed["i"], ExpFlagValue::Integer(7)); - assert_eq!(parsed["f"], ExpFlagValue::Float(1.5)); - assert_eq!(parsed["b"], ExpFlagValue::Bool(true)); - assert_eq!(parsed["n"], ExpFlagValue::Null); - assert_eq!(serde_json::to_value(&parsed).unwrap(), values); - } - - #[test] - fn session_config_with_exp_assignments_serializes() { - let assignments = sample_exp_assignments("ctx-123"); - let expected = serde_json::to_value(&assignments).unwrap(); - let (wire, _runtime) = SessionConfig::default() - .with_exp_assignments(assignments) - .into_wire(Some(SessionId::from("exp-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["expAssignments"], expected); - assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123"); - assert_eq!( - json["expAssignments"]["Flights"]["copilot_exp_flag"], - "treatment" - ); - - // Unset exp assignments are omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("exp-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("expAssignments").is_none()); - } - - #[test] - fn resume_session_config_with_exp_assignments_serializes() { - let assignments = sample_exp_assignments("ctx-456"); - let expected = serde_json::to_value(&assignments).unwrap(); - let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on")) - .with_exp_assignments(assignments) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["expAssignments"], expected); - - // Unset exp assignments are omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("expAssignments").is_none()); - } - - #[test] - fn session_config_clone_preserves_exp_assignments() { - let assignments = sample_exp_assignments("ctx-clone"); - let config = SessionConfig::default().with_exp_assignments(assignments.clone()); - let cloned = config.clone(); - - assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); - - let (wire, _runtime) = cloned - .into_wire(Some(SessionId::from("exp-clone"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["expAssignments"], - serde_json::to_value(&assignments).unwrap() - ); - } - - #[test] - fn resume_session_config_clone_preserves_exp_assignments() { - let assignments = sample_exp_assignments("ctx-clone-resume"); - let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone")) - .with_exp_assignments(assignments.clone()); - let cloned = config.clone(); - - assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); - - let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["expAssignments"], - serde_json::to_value(&assignments).unwrap() - ); - } - - #[test] - #[allow(clippy::field_reassign_with_default)] - fn session_config_into_wire_serializes_bucket_b_fields() { - use std::path::PathBuf; - - use super::{CloudSessionOptions, CloudSessionRepository}; - - let mut cfg = SessionConfig::default(); - cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); - cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.github_token = Some("ghs_secret".to_string()); - cfg.include_sub_agent_streaming_events = Some(false); - cfg.enable_session_telemetry = Some(false); - cfg.reasoning_summary = Some(ReasoningSummary::Concise); - cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); - cfg.enable_on_demand_instruction_discovery = Some(false); - cfg.cloud = Some(CloudSessionOptions::with_repository( - CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), - )); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("custom-id"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["sessionId"], "custom-id"); - assert_eq!(wire_json["configDir"], "/tmp/cfg"); - assert_eq!(wire_json["workingDirectory"], "/tmp/work"); - assert_eq!(wire_json["gitHubToken"], "ghs_secret"); - assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); - assert_eq!(wire_json["enableSessionTelemetry"], false); - assert_eq!(wire_json["reasoningSummary"], "concise"); - assert_eq!(wire_json["remoteSession"], "export"); - assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); - assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); - assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); - assert_eq!(wire_json["cloud"]["repository"]["branch"], "main"); - - // Unset fields are omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("gitHubToken").is_none()); - assert!(empty_json.get("enableSessionTelemetry").is_none()); - assert!(empty_json.get("reasoningSummary").is_none()); - assert!(empty_json.get("remoteSession").is_none()); - assert!( - empty_json - .get("enableOnDemandInstructionDiscovery") - .is_none() - ); - assert!(empty_json.get("cloud").is_none()); - } - - #[test] - fn session_config_into_wire_serializes_named_providers_and_models() { - let cfg = SessionConfig::default() - .with_providers(vec![ - NamedProviderConfig::new("my-openai", "https://api.example.com/v1") - .with_provider_type("openai") - .with_wire_api("responses") - .with_api_key("sk-test"), - ]) - .with_models(vec![ - ProviderModelConfig::new("gpt-x", "my-openai") - .with_wire_model("gpt-x-2025") - .with_max_output_tokens(2048), - ]); - - let (wire, _) = cfg - .into_wire(Some(SessionId::from("sess-providers"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["providers"][0]["name"], "my-openai"); - assert_eq!( - wire_json["providers"][0]["baseUrl"], - "https://api.example.com/v1" - ); - assert_eq!(wire_json["providers"][0]["type"], "openai"); - assert_eq!(wire_json["providers"][0]["wireApi"], "responses"); - assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test"); - assert_eq!(wire_json["models"][0]["id"], "gpt-x"); - assert_eq!(wire_json["models"][0]["provider"], "my-openai"); - assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025"); - assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("providers").is_none()); - assert!(empty_json.get("models").is_none()); - } - - #[test] - fn resume_config_into_wire_serializes_named_providers_and_models() { - let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume")) - .with_providers(vec![ - NamedProviderConfig::new("my-azure", "https://example.openai.azure.com") - .with_provider_type("azure") - .with_azure(AzureProviderOptions { - api_version: Some("2024-10-21".to_string()), - }), - ]) - .with_models(vec![ - ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"), - ]); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["providers"][0]["name"], "my-azure"); - assert_eq!(wire_json["providers"][0]["type"], "azure"); - assert_eq!( - wire_json["providers"][0]["azure"]["apiVersion"], - "2024-10-21" - ); - assert_eq!(wire_json["models"][0]["id"], "deploy-1"); - assert_eq!(wire_json["models"][0]["provider"], "my-azure"); - assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o"); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty")) - .into_wire() - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("providers").is_none()); - assert!(empty_json.get("models").is_none()); - } - - #[test] - fn session_config_into_wire_serializes_plugin_directories_and_large_output() { - use std::path::PathBuf; - - let cfg = SessionConfig { - plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), - disabled_mcp_servers: Some(vec![ - "local-files".to_string(), - "remote-github".to_string(), - ]), - large_output: Some( - LargeToolOutputConfig::new() - .with_enabled(true) - .with_max_size_bytes(1024) - .with_output_directory(PathBuf::from("/tmp/large-output")), - ), - ..Default::default() - }; - - let (wire, _) = cfg - .into_wire(Some(SessionId::from("sess-1"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); - assert_eq!( - wire_json["disabledMcpServers"], - serde_json::json!(["local-files", "remote-github"]) - ); - assert_eq!(wire_json["largeOutput"]["enabled"], true); - assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); - assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("pluginDirectories").is_none()); - assert!(empty_json.get("disabledMcpServers").is_none()); - assert!(empty_json.get("largeOutput").is_none()); - } - - #[test] - fn resume_session_config_into_wire_serializes_bucket_b_fields() { - use std::path::PathBuf; - - let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); - cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); - cfg.github_token = Some("ghs_secret".to_string()); - cfg.include_sub_agent_streaming_events = Some(true); - cfg.enable_session_telemetry = Some(false); - cfg.reasoning_summary = Some(ReasoningSummary::Detailed); - cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); - cfg.enable_on_demand_instruction_discovery = Some(false); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["sessionId"], "sess-1"); - assert_eq!(wire_json["workingDirectory"], "/tmp/work"); - assert_eq!(wire_json["configDir"], "/tmp/cfg"); - assert_eq!(wire_json["gitHubToken"], "ghs_secret"); - assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); - assert_eq!(wire_json["enableSessionTelemetry"], false); - assert_eq!(wire_json["reasoningSummary"], "detailed"); - assert_eq!(wire_json["remoteSession"], "on"); - assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); - - // Unset remote_session is omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("reasoningSummary").is_none()); - assert!(empty_json.get("remoteSession").is_none()); - assert!( - empty_json - .get("enableOnDemandInstructionDiscovery") - .is_none() - ); - } - - #[test] - fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() { - use std::path::PathBuf; - - let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); - cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); - cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]); - cfg.large_output = Some( - LargeToolOutputConfig::new() - .with_enabled(false) - .with_max_size_bytes(2048) - .with_output_directory(PathBuf::from("/tmp/large-output-r")), - ); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); - assert_eq!( - wire_json["disabledMcpServers"], - serde_json::json!(["local-files-r"]) - ); - assert_eq!(wire_json["largeOutput"]["enabled"], false); - assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); - assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("pluginDirectories").is_none()); - assert!(empty_json.get("disabledMcpServers").is_none()); - assert!(empty_json.get("largeOutput").is_none()); - } - - #[test] - fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() { - let url = "https://example.com/oauth/client-metadata.json"; - - let (create_wire, _) = SessionConfig::default() - .with_auth_client_id_metadata_url(url) - .into_wire(None) - .expect("default create has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["authClientIdMetadataUrl"], url); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_auth_client_id_metadata_url(url) - .into_wire() - .expect("default resume has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["authClientIdMetadataUrl"], url); - - let (empty_create_wire, _) = SessionConfig::default() - .into_wire(None) - .expect("default create has no duplicate handlers"); - let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap(); - assert!(empty_create_json.get("authClientIdMetadataUrl").is_none()); - - let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap(); - assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none()); - } - - #[test] - fn session_config_clones_disabled_mcp_servers() { - let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); - let mut create_clone = create.clone(); - create_clone - .disabled_mcp_servers - .as_mut() - .expect("configured disabled MCP servers") - .push("remote-github".to_string()); - assert_eq!( - create.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_disabled_mcp_servers(["local-files"]); - let mut resume_clone = resume.clone(); - resume_clone - .disabled_mcp_servers - .as_mut() - .expect("configured disabled MCP servers") - .push("remote-github".to_string()); - assert_eq!( - resume.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - } - - #[test] - fn session_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = SessionConfig::default() - .with_session_id(SessionId::from("sess-1")) - .with_model("claude-sonnet-4") - .with_client_name("test-app") - .with_reasoning_effort("medium") - .with_reasoning_summary(ReasoningSummary::Concise) - .with_context_tier("long_context") - .with_streaming(true) - .with_tools([Tool::new("greet")]) - .with_available_tools(["bash", "view"]) - .with_excluded_tools(["dangerous"]) - .with_mcp_servers(IndexMap::new()) - .with_mcp_oauth_token_storage("persistent") - .with_enable_config_discovery(true) - .with_enable_on_demand_instruction_discovery(true) - .with_skill_directories([PathBuf::from("/tmp/skills")]) - .with_disabled_skills(["broken-skill"]) - .with_disabled_mcp_servers(["local-files"]) - .with_agent("researcher") - .with_config_directory(PathBuf::from("/tmp/config")) - .with_working_directory(PathBuf::from("/tmp/work")) - .with_additional_directories([PathBuf::from("/tmp/shared")]) - .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .with_enable_session_telemetry(false) - .with_include_sub_agent_streaming_events(false) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); - - assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); - assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); - assert_eq!(cfg.client_name.as_deref(), Some("test-app")); - assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium")); - assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise)); - assert_eq!(cfg.context_tier.as_deref(), Some("long_context")); - assert_eq!(cfg.streaming, Some(true)); - assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); - assert_eq!( - cfg.available_tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert_eq!( - cfg.excluded_tools.as_deref(), - Some(&["dangerous".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); - assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true)); - assert_eq!( - cfg.skill_directories.as_deref(), - Some(&[PathBuf::from("/tmp/skills")][..]) - ); - assert_eq!( - cfg.disabled_skills.as_deref(), - Some(&["broken-skill".to_string()][..]) - ); - assert_eq!( - cfg.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); - assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); - assert_eq!( - cfg.additional_directories.as_deref(), - Some(&[PathBuf::from("/tmp/shared")][..]) - ); - assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); - assert_eq!( - cfg.capi, - Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - ); - assert_eq!(cfg.enable_session_telemetry, Some(false)); - assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); - assert_eq!( - cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) - ); - } - - #[test] - fn resume_session_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) - .with_client_name("test-app") - .with_reasoning_summary(ReasoningSummary::None) - .with_context_tier("default") - .with_streaming(true) - .with_tools([Tool::new("greet")]) - .with_available_tools(["bash", "view"]) - .with_excluded_tools(["dangerous"]) - .with_mcp_servers(IndexMap::new()) - .with_mcp_oauth_token_storage("persistent") - .with_enable_config_discovery(true) - .with_enable_on_demand_instruction_discovery(false) - .with_skill_directories([PathBuf::from("/tmp/skills")]) - .with_disabled_skills(["broken-skill"]) - .with_disabled_mcp_servers(["local-files"]) - .with_agent("researcher") - .with_config_directory(PathBuf::from("/tmp/config")) - .with_working_directory(PathBuf::from("/tmp/work")) - .with_additional_directories([PathBuf::from("/tmp/shared")]) - .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .with_enable_session_telemetry(false) - .with_include_sub_agent_streaming_events(true) - .with_suppress_resume_event(true) - .with_continue_pending_work(true) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); - - assert_eq!(cfg.session_id.as_str(), "sess-2"); - assert_eq!(cfg.client_name.as_deref(), Some("test-app")); - assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None)); - assert_eq!(cfg.context_tier.as_deref(), Some("default")); - assert_eq!(cfg.streaming, Some(true)); - assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); - assert_eq!( - cfg.available_tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert_eq!( - cfg.excluded_tools.as_deref(), - Some(&["dangerous".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); - assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false)); - assert_eq!( - cfg.skill_directories.as_deref(), - Some(&[PathBuf::from("/tmp/skills")][..]) - ); - assert_eq!( - cfg.disabled_skills.as_deref(), - Some(&["broken-skill".to_string()][..]) - ); - assert_eq!( - cfg.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); - assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); - assert_eq!( - cfg.additional_directories.as_deref(), - Some(&[PathBuf::from("/tmp/shared")][..]) - ); - assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); - assert_eq!( - cfg.capi, - Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - ); - assert_eq!(cfg.enable_session_telemetry, Some(false)); - assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); - assert_eq!(cfg.suppress_resume_event, Some(true)); - assert_eq!(cfg.continue_pending_work, Some(true)); - assert_eq!( - cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) - ); - } - - /// `continue_pending_work` must serialize to wire as `continuePendingWork` - /// — the runtime keys off this exact field name to opt into the - /// pending-work-handoff pattern. - #[test] - fn resume_session_config_serializes_continue_pending_work_to_camel_case() { - let cfg = - ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["continuePendingWork"], true); - - // Unset case — skip_serializing_if must omit the field. - let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("continuePendingWork").is_none()); - } - - #[test] - fn session_configs_serialize_additional_directories() { - let create = SessionConfig::default().with_additional_directories([ - PathBuf::from("/tmp/shared"), - PathBuf::from("/tmp/generated"), - ]); - let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!( - create_json["additionalDirectories"], - serde_json::json!(["/tmp/shared", "/tmp/generated"]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_additional_directories([PathBuf::from("/tmp/resumed")]); - let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!( - resume_json["additionalDirectories"], - serde_json::json!(["/tmp/resumed"]) - ); - } - - /// The Rust field is `suppress_resume_event`, but the wire field stays - /// `disableResume` to preserve compatibility with the runtime and other - /// SDKs. - #[test] - fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() { - let cfg = - ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["disableResume"], true); - assert!(json.get("suppressResumeEvent").is_none()); - } - - /// `instruction_directories` must serialize to wire as - /// `instructionDirectories` on `SessionConfig`. - #[test] - fn session_config_serializes_instruction_directories_to_camel_case() { - let cfg = - SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]); - let (wire, _) = cfg - .into_wire(Some(SessionId::from("instr-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["instructionDirectories"], - serde_json::json!(["/tmp/instr"]) - ); - - // Unset case — skip_serializing_if must omit the field. - let (wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("instr-off"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("instructionDirectories").is_none()); - } - - /// Same check on the resume path. Forwarded to the CLI on - /// `session.resume`. - #[test] - fn resume_session_config_serializes_instruction_directories_to_camel_case() { - let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_instruction_directories([PathBuf::from("/tmp/instr")]); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["instructionDirectories"], - serde_json::json!(["/tmp/instr"]) - ); - - let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("instructionDirectories").is_none()); - } - - #[test] - fn custom_agent_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.") - .with_display_name("Research Assistant") - .with_description("Investigates technical questions.") - .with_tools(["bash", "view"]) - .with_mcp_servers(IndexMap::new()) - .with_infer(true) - .with_skills(["rust-coding-skill"]); - - assert_eq!(cfg.name, "researcher"); - assert_eq!(cfg.prompt, "You are a research assistant."); - assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant")); - assert_eq!( - cfg.description.as_deref(), - Some("Investigates technical questions.") - ); - assert_eq!( - cfg.tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.infer, Some(true)); - assert_eq!( - cfg.skills.as_deref(), - Some(&["rust-coding-skill".to_string()][..]) - ); - } - - #[test] - fn mcp_servers_serialize_in_insertion_order() { - use indexmap::IndexMap; - - // Regression: `mcp_servers` was a `HashMap`, so the server keys (and - // thus the `session.create` payload) serialized in a per-process - // random order; `IndexMap` pins them to insertion order. The long - // sequence makes a `HashMap` regression reproduce this exact order by - // chance only 1/N!, avoiding a flaky false pass. - let order = [ - "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon", - "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet", - ]; - let mut servers = IndexMap::new(); - for name in order { - servers.insert( - name.to_string(), - McpServerConfig::Stdio(McpStdioServerConfig { - command: "run".to_string(), - ..Default::default() - }), - ); - } - - let (wire, _runtime) = SessionConfig::default() - .with_mcp_servers(servers) - .into_wire(None) - .expect("into_wire should succeed"); - let json = serde_json::to_string(&wire).expect("serialize wire"); - - let positions: Vec = order - .iter() - .map(|name| { - json.find(&format!("\"{name}\"")) - .unwrap_or_else(|| panic!("server {name} missing from wire JSON")) - }) - .collect(); - let mut ascending = positions.clone(); - ascending.sort_unstable(); - assert_eq!( - positions, ascending, - "mcp server keys must serialize in insertion order: {json}" - ); - } - - #[test] - fn infinite_session_config_builder_composes() { - let cfg = InfiniteSessionConfig::new() - .with_enabled(true) - .with_background_compaction_threshold(0.75) - .with_buffer_exhaustion_threshold(0.92); - - assert_eq!(cfg.enabled, Some(true)); - assert_eq!(cfg.background_compaction_threshold, Some(0.75)); - assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92)); - } - - #[test] - fn provider_config_builder_composes() { - use std::collections::HashMap; - - let mut headers = HashMap::new(); - headers.insert("X-Custom".to_string(), "value".to_string()); - - let cfg = ProviderConfig::new("https://api.example.com") - .with_provider_type("openai") - .with_wire_api("completions") - .with_transport("websockets") - .with_api_key("sk-test") - .with_bearer_token("bearer-test") - .with_headers(headers) - .with_model_id("gpt-4") - .with_wire_model("azure-gpt-4-deployment") - .with_max_prompt_tokens(8192) - .with_max_output_tokens(2048); - - assert_eq!(cfg.base_url, "https://api.example.com"); - assert_eq!(cfg.provider_type.as_deref(), Some("openai")); - assert_eq!(cfg.wire_api.as_deref(), Some("completions")); - assert_eq!(cfg.transport.as_deref(), Some("websockets")); - assert_eq!(cfg.api_key.as_deref(), Some("sk-test")); - assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test")); - assert_eq!( - cfg.headers - .as_ref() - .and_then(|h| h.get("X-Custom")) - .map(String::as_str), - Some("value"), - ); - assert_eq!(cfg.model_id.as_deref(), Some("gpt-4")); - assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment")); - assert_eq!(cfg.max_prompt_tokens, Some(8192)); - assert_eq!(cfg.max_output_tokens, Some(2048)); - - // Wire-shape: camelCase, skip_serializing_if when unset. - let wire = serde_json::to_value(&cfg).unwrap(); - assert_eq!(wire["modelId"], "gpt-4"); - assert_eq!(wire["wireModel"], "azure-gpt-4-deployment"); - assert_eq!(wire["maxPromptTokens"], 8192); - assert_eq!(wire["maxOutputTokens"], 2048); - - let unset = ProviderConfig::new("https://api.example.com"); - let wire_unset = serde_json::to_value(&unset).unwrap(); - assert!(wire_unset.get("modelId").is_none()); - assert!(wire_unset.get("wireModel").is_none()); - assert!(wire_unset.get("maxPromptTokens").is_none()); - assert!(wire_unset.get("maxOutputTokens").is_none()); - } - - #[test] - fn capi_session_options_builder_composes_and_serializes() { - let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false); - - assert_eq!(cfg.enable_web_socket_responses, Some(false)); - - let wire = serde_json::to_value(&cfg).unwrap(); - assert_eq!( - wire, - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let unset = CapiSessionOptions::new(); - let wire_unset = serde_json::to_value(&unset).unwrap(); - assert!(wire_unset.get("enableWebSocketResponses").is_none()); - assert!(wire_unset.get("autoTier").is_none()); - assert_eq!(wire_unset, json!({})); - } - - #[test] - fn capi_auto_tier_canonical_values_round_trip_and_forward() { - for (tier, value) in [ - (AutoTier::Efficiency, "efficiency"), - (AutoTier::Balance, "balance"), - (AutoTier::Intelligence, "intelligence"), - (AutoTier::Fast, "fast"), - ] { - let exported: crate::AutoTier = tier.clone(); - let capi = CapiSessionOptions::new().with_auto_tier(exported); - assert_eq!(capi.auto_tier, Some(tier)); - assert_eq!( - serde_json::to_value(&capi).unwrap(), - json!({"autoTier": value}) - ); - assert_eq!( - serde_json::from_value::(json!({"autoTier": value})).unwrap(), - capi - ); - - let capi = capi.with_enable_web_socket_responses(false); - let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); - let (create, _) = SessionConfig::default() - .with_model("auto") - .with_capi(capi.clone()) - .into_wire(Some(SessionId::from("capi-create"))) - .unwrap(); - assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); - - let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) - .with_capi(capi) - .into_wire() - .unwrap(); - assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); - } - } - - #[test] - fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { - for value in ["balanced", "Balance", "unknown"] { - assert_eq!( - serde_json::from_value::(json!(value)).unwrap(), - AutoTier::Unknown - ); - } - let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); - assert_eq!(capi.auto_tier, None); - } - - #[test] - fn session_config_with_capi_serializes() { - let (wire, _) = SessionConfig::default() - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .into_wire(Some(SessionId::from("capi-create"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["capi"], - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("capi-create-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("capi").is_none()); - } - - #[test] - fn resume_session_config_with_capi_serializes() { - let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["capi"], - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("capi").is_none()); - } - - #[test] - fn system_message_config_builder_composes() { - use std::collections::HashMap; - - let cfg = SystemMessageConfig::new() - .with_mode("replace") - .with_content("Custom system message.") - .with_sections(HashMap::new()); - - assert_eq!(cfg.mode.as_deref(), Some("replace")); - assert_eq!(cfg.content.as_deref(), Some("Custom system message.")); - assert!(cfg.sections.is_some()); - } - - #[test] - fn delivery_mode_serializes_to_kebab_case_strings() { - assert_eq!( - serde_json::to_string(&DeliveryMode::Enqueue).unwrap(), - "\"enqueue\"" - ); - assert_eq!( - serde_json::to_string(&DeliveryMode::Immediate).unwrap(), - "\"immediate\"" - ); - let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap(); - assert_eq!(parsed, DeliveryMode::Immediate); - } - - #[test] - fn agent_mode_serializes_to_kebab_case_strings() { - assert_eq!( - serde_json::to_string(&AgentMode::Interactive).unwrap(), - "\"interactive\"" - ); - assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\""); - assert_eq!( - serde_json::to_string(&AgentMode::Autopilot).unwrap(), - "\"autopilot\"" - ); - assert_eq!( - serde_json::to_string(&AgentMode::Shell).unwrap(), - "\"shell\"" - ); - let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap(); - assert_eq!(parsed, AgentMode::Plan); - } - - #[test] - fn connection_state_distinguishes_variants() { - // ConnectionState is now an internal type; verify we can construct - // and compare the variants used by the lifecycle code paths. - assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected); - } - - /// `agentId` is the sub-agent attribution field added in copilot-sdk - /// commit f8cf846 ("Derive session event envelopes from schema"). - /// Every other SDK (Node, Python, Go, .NET) carries it on the event - /// envelope; Rust must too or sub-agent events lose attribution at - /// the deserialization boundary. Cross-SDK parity test. - #[test] - fn session_event_round_trips_agent_id_on_envelope() { - let wire = json!({ - "id": "evt-1", - "timestamp": "2026-04-30T12:00:00Z", - "parentId": null, - "agentId": "sub-agent-42", - "type": "assistant.message", - "data": { "message": "hi" } - }); - - let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap(); - assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); - - // Round-trip preserves the field on the wire. - let roundtripped = serde_json::to_value(&event).unwrap(); - assert_eq!(roundtripped["agentId"], "sub-agent-42"); - - // Absent agentId remains absent (skip_serializing_if). - let main_agent_event: SessionEvent = serde_json::from_value(json!({ - "id": "evt-2", - "timestamp": "2026-04-30T12:00:01Z", - "parentId": null, - "type": "session.idle", - "data": {} - })) - .unwrap(); - assert!(main_agent_event.agent_id.is_none()); - let roundtripped = serde_json::to_value(&main_agent_event).unwrap(); - assert!(roundtripped.get("agentId").is_none()); - } - - /// Same parity for the typed event envelope produced by the codegen. - #[test] - fn typed_session_event_round_trips_agent_id_on_envelope() { - let wire = json!({ - "id": "evt-1", - "timestamp": "2026-04-30T12:00:00Z", - "parentId": null, - "agentId": "sub-agent-42", - "type": "session.idle", - "data": {} - }); - - let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); - assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); - - let roundtripped = serde_json::to_value(&event).unwrap(); - assert_eq!(roundtripped["agentId"], "sub-agent-42"); - } - - #[test] - fn connection_state_variants_compile() { - // Defensive smoke test: all variants must be constructable from - // within the crate. (The enum was demoted from pub to pub(crate) - // in Phase D; this test guards against accidental removal.) - let _ = ConnectionState::Disconnected; - let _ = ConnectionState::Connecting; - let _ = ConnectionState::Connected; - let _ = ConnectionState::Error; - } - - #[test] - fn deserializes_runtime_attachment_variants() { - let attachments: Vec = serde_json::from_value(json!([ - { - "type": "file", - "path": "/tmp/file.rs", - "displayName": "file.rs", - "lineRange": { "start": 7, "end": 12 } - }, - { - "type": "directory", - "path": "/tmp/project", - "displayName": "project" - }, - { - "type": "selection", - "filePath": "/tmp/lib.rs", - "displayName": "lib.rs", - "text": "fn main() {}", - "selection": { - "start": { "line": 1, "character": 2 }, - "end": { "line": 3, "character": 4 } - } - }, - { - "type": "blob", - "data": "Zm9v", - "mimeType": "image/png", - "displayName": "image.png" - }, - { - "type": "github_reference", - "number": 42, - "title": "Fix rendering", - "referenceType": "issue", - "state": "open", - "url": "https://github.com/example/repo/issues/42" - }, - { - "type": "extension_context", - "capturedAt": "2026-09-18T11:00:00Z", - "extensionId": "example:extension", - "title": "Unbound context" - } - ])) - .expect("attachments should deserialize"); - - assert_eq!(attachments.len(), 6); - assert!(matches!( - &attachments[0], - Attachment::File { - path, - display_name, - line_range: Some(AttachmentLineRange { start: 7, end: 12 }), - } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs") - )); - assert!(matches!( - &attachments[1], - Attachment::Directory { path, display_name } - if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project") - )); - assert!(matches!( - &attachments[2], - Attachment::Selection { - file_path, - display_name, - selection: - AttachmentSelectionRange { - start: AttachmentSelectionPosition { line: 1, character: 2 }, - end: AttachmentSelectionPosition { line: 3, character: 4 }, - }, - .. - } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs") - )); - assert!(matches!( - &attachments[3], - Attachment::Blob { - data, - mime_type, - display_name, - } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png") - )); - assert!(matches!( - &attachments[4], - Attachment::GitHubReference { - number: 42, - title, - reference_type: GitHubReferenceType::Issue, - state, - url, - } if title == "Fix rendering" - && state == "open" - && url == "https://github.com/example/repo/issues/42" - )); - assert!(matches!( - &attachments[5], - Attachment::ExtensionContext { - captured_at, - extension_id, - canvas_id: None, - instance_id: None, - title, - payload: None, - } if captured_at == "2026-09-18T11:00:00Z" - && extension_id == "example:extension" - && title == "Unbound context" - )); - assert_eq!( - serde_json::to_value(&attachments[5]).expect("serialize extension context"), - json!({ - "type": "extension_context", - "capturedAt": "2026-09-18T11:00:00Z", - "extensionId": "example:extension", - "title": "Unbound context" - }) - ); - } - - #[test] - fn ensures_display_names_for_variants_that_support_them() { - let mut attachments = vec![ - Attachment::File { - path: PathBuf::from("/tmp/file.rs"), - display_name: None, - line_range: None, - }, - Attachment::Selection { - file_path: PathBuf::from("/tmp/src/lib.rs"), - display_name: None, - text: "fn main() {}".to_string(), - selection: AttachmentSelectionRange { - start: AttachmentSelectionPosition { - line: 0, - character: 0, - }, - end: AttachmentSelectionPosition { - line: 0, - character: 10, - }, - }, - }, - Attachment::Blob { - data: "Zm9v".to_string(), - mime_type: "image/png".to_string(), - display_name: None, - }, - Attachment::GitHubReference { - number: 7, - title: "Track regressions".to_string(), - reference_type: GitHubReferenceType::Issue, - state: "open".to_string(), - url: "https://example.com/issues/7".to_string(), - }, - ]; - - ensure_attachment_display_names(&mut attachments); - - assert_eq!(attachments[0].display_name(), Some("file.rs")); - assert_eq!(attachments[1].display_name(), Some("lib.rs")); - assert_eq!(attachments[2].display_name(), Some("attachment")); - assert_eq!(attachments[3].display_name(), None); - assert_eq!( - attachments[3].label(), - Some("Track regressions".to_string()) - ); - } - - #[test] - fn github_anchored_attachment_variants_round_trip() { - let cases = vec![ - ( - "github_commit", - json!({ - "type": "github_commit", - "message": "Fix the thing", - "oid": "abc123", - "repo": { "id": 1, "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/commit/abc123" - }), - ), - ( - "github_release", - json!({ - "type": "github_release", - "name": "v1.2.3", - "repo": { "name": "repo", "owner": "octocat" }, - "tagName": "v1.2.3", - "url": "https://github.com/octocat/repo/releases/tag/v1.2.3" - }), - ), - ( - "github_actions_job", - json!({ - "type": "github_actions_job", - "conclusion": "failure", - "jobId": 99, - "jobName": "build", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/actions/runs/1/job/99", - "workflowName": "CI" - }), - ), - ( - "github_repository", - json!({ - "type": "github_repository", - "description": "An example repository", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo" - }), - ), - ( - "github_file_diff", - json!({ - "type": "github_file_diff", - "base": { - "path": "src/lib.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" } - }, - "head": { - "path": "src/lib.rs", - "ref": "feature", - "repo": { "name": "repo", "owner": "octocat" } - }, - "url": "https://github.com/octocat/repo/compare/main...feature" - }), - ), - ( - "github_tree_comparison", - json!({ - "type": "github_tree_comparison", - "base": { - "repo": { "name": "repo", "owner": "octocat" }, - "revision": "main" - }, - "head": { - "repo": { "name": "repo", "owner": "octocat" }, - "revision": "feature" - }, - "url": "https://github.com/octocat/repo/compare/main...feature" - }), - ), - ( - "github_url", - json!({ - "type": "github_url", - "url": "https://github.com/octocat/repo/wiki" - }), - ), - ( - "github_file", - json!({ - "type": "github_file", - "path": "src/main.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/blob/main/src/main.rs" - }), - ), - ( - "github_snippet", - json!({ - "type": "github_snippet", - "lineRange": { "start": 10, "end": 20 }, - "path": "src/main.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20" - }), - ), - ]; - - for (expected_type, input) in cases { - let attachment: Attachment = serde_json::from_value(input.clone()) - .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}")); - - // Serialize to a string first: parsing into `serde_json::Value` would - // silently dedupe a duplicate `type` key, hiding the exact regression - // this test guards against (e.g. a wrapped generated struct emitting its - // own `type` alongside the enum tag). - let serialized_string = serde_json::to_string(&attachment) - .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}")); - - // Exactly one `type` key, carrying the expected discriminator. - assert_eq!( - serialized_string.matches("\"type\":").count(), - 1, - "{expected_type} must serialize a single `type` key" - ); - - let serialized: serde_json::Value = serde_json::from_str(&serialized_string) - .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}")); - assert_eq!( - serialized.get("type").and_then(|value| value.as_str()), - Some(expected_type), - "{expected_type} must serialize the correct discriminator" - ); - - // Round-trips without dropping fields. - assert_eq!( - serialized, input, - "{expected_type} should round-trip without data loss" - ); - let reparsed: Attachment = serde_json::from_value(serialized) - .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}")); - assert_eq!( - reparsed, attachment, - "{expected_type} should re-deserialize to the same value" - ); - } - } -} +mod tests; #[cfg(test)] -mod permission_builder_tests { - use std::sync::Arc; - - use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; - use crate::permission; - use crate::types::{ - PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, - SessionId, - }; - - fn data() -> PermissionRequestData { - PermissionRequestData { - extra: serde_json::json!({"tool": "shell"}), - ..Default::default() - } - } - - /// Apply the same policy-resolution logic that `Client::create_session` - /// uses, so tests exercise the effective handler. - fn resolve_create(mut cfg: SessionConfig) -> Option> { - permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) - } - - fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option> { - permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) - } - - async fn dispatch(handler: &Arc) -> PermissionResult { - handler - .handle(SessionId::from("s1"), RequestId::new("1"), data()) - .await - } - - #[tokio::test] - async fn approve_all_with_handler_present_approves() { - let cfg = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let h = resolve_create(cfg).expect("policy + handler yields handler"); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_standalone_produces_handler() { - let cfg = SessionConfig::default().approve_all_permissions(); - let h = resolve_create(cfg).expect("policy alone yields handler"); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - /// Phase I: order between with_permission_handler and the policy - /// builder must not matter. - #[tokio::test] - async fn approve_all_is_order_independent() { - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let b = SessionConfig::default() - .approve_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn deny_all_is_order_independent() { - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .deny_all_permissions(); - let b = SessionConfig::default() - .deny_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_permissions_if_consults_predicate() { - let cfg = SessionConfig::default().approve_permissions_if(|d| { - d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }); - let h = resolve_create(cfg).unwrap(); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_permissions_if_is_order_independent() { - let predicate = |d: &PermissionRequestData| { - d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }; - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_permissions_if(predicate); - let b = SessionConfig::default() - .approve_permissions_if(predicate) - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn resume_session_config_approve_all_works() { - let cfg = ResumeSessionConfig::new(SessionId::from("s1")) - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let h = resolve_resume(cfg).unwrap(); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn resume_session_config_approve_all_is_order_independent() { - let a = ResumeSessionConfig::new(SessionId::from("s1")) - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let b = ResumeSessionConfig::new(SessionId::from("s1")) - .approve_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_resume(a).unwrap(); - let hb = resolve_resume(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[test] - fn session_config_enable_experimental_mode_serializes_when_set() { - let cfg = SessionConfig::default().with_enable_experimental_mode(false); - assert_eq!(cfg.enable_experimental_mode, Some(false)); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("experimental-mode"))) - .expect("enable_experimental_mode config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, Some(false)); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); - } - - #[test] - fn session_config_enable_experimental_mode_omitted_when_none() { - let cfg = SessionConfig::default(); - assert_eq!(cfg.enable_experimental_mode, None); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("no-experimental-mode"))) - .expect("default config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, None); - - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("isExperimentalMode").is_none()); - } - - #[test] - fn resume_session_config_enable_experimental_mode_serializes_when_set() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) - .with_enable_experimental_mode(false); - assert_eq!(cfg.enable_experimental_mode, Some(false)); - - let (wire, _runtime) = cfg - .into_wire() - .expect("resume enable_experimental_mode config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, Some(false)); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); - } - - #[test] - fn resume_session_config_enable_experimental_mode_omitted_when_none() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); - assert_eq!(cfg.enable_experimental_mode, None); - - let (wire, _runtime) = cfg - .into_wire() - .expect("default resume config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, None); - - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("isExperimentalMode").is_none()); - } -} +mod permission_builder_tests; #[cfg(test)] -mod is_terminal_tests { - use super::Tool; - - #[test] - fn is_terminal_serializes_as_camel_case_when_set() { - let tool = Tool { - name: "clear_context".to_owned(), - is_terminal: true, - ..Default::default() - }; - let value = serde_json::to_value(&tool).expect("tool serializes"); - assert_eq!( - value.get("isTerminal"), - Some(&serde_json::Value::Bool(true)) - ); - } - - #[test] - fn is_terminal_is_omitted_when_false() { - let tool = Tool { - name: "plain".to_owned(), - ..Default::default() - }; - let value = serde_json::to_value(&tool).expect("tool serializes"); - assert!(value.get("isTerminal").is_none()); - } - - /// `Tool` has a hand-written `Debug` impl, so a new field is only reported - /// if it is added there by hand. Guard against that drift. - #[test] - fn is_terminal_appears_in_debug_output() { - let terminal = Tool { - name: "clear_context".to_owned(), - is_terminal: true, - ..Default::default() - }; - assert!(format!("{terminal:?}").contains("is_terminal: true")); - - let plain = Tool { - name: "plain".to_owned(), - ..Default::default() - }; - assert!(format!("{plain:?}").contains("is_terminal: false")); - } -} +mod is_terminal_tests; #[cfg(test)] mod refresh_custom_instructions_tests; diff --git a/rust/src/types/is_terminal_tests.rs b/rust/src/types/is_terminal_tests.rs new file mode 100644 index 0000000000..8e0090ab36 --- /dev/null +++ b/rust/src/types/is_terminal_tests.rs @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::Tool; + +#[test] +fn is_terminal_serializes_as_camel_case_when_set() { + let tool = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert_eq!( + value.get("isTerminal"), + Some(&serde_json::Value::Bool(true)) + ); +} + +#[test] +fn is_terminal_is_omitted_when_false() { + let tool = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert!(value.get("isTerminal").is_none()); +} + +/// `Tool` has a hand-written `Debug` impl, so a new field is only reported +/// if it is added there by hand. Guard against that drift. +#[test] +fn is_terminal_appears_in_debug_output() { + let terminal = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + assert!(format!("{terminal:?}").contains("is_terminal: true")); + + let plain = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + assert!(format!("{plain:?}").contains("is_terminal: false")); +} diff --git a/rust/src/types/permission_builder_tests.rs b/rust/src/types/permission_builder_tests.rs new file mode 100644 index 0000000000..c549f9d4d8 --- /dev/null +++ b/rust/src/types/permission_builder_tests.rs @@ -0,0 +1,259 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::Arc; + +use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; +use crate::permission; +use crate::types::{ + PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, + SessionId, +}; + +fn data() -> PermissionRequestData { + PermissionRequestData { + extra: serde_json::json!({"tool": "shell"}), + ..Default::default() + } +} + +/// Apply the same policy-resolution logic that `Client::create_session` +/// uses, so tests exercise the effective handler. +fn resolve_create(mut cfg: SessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) +} + +fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) +} + +async fn dispatch(handler: &Arc) -> PermissionResult { + handler + .handle(SessionId::from("s1"), RequestId::new("1"), data()) + .await +} + +#[tokio::test] +async fn approve_all_with_handler_present_approves() { + let cfg = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let h = resolve_create(cfg).expect("policy + handler yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_standalone_produces_handler() { + let cfg = SessionConfig::default().approve_all_permissions(); + let h = resolve_create(cfg).expect("policy alone yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +/// Phase I: order between with_permission_handler and the policy +/// builder must not matter. +#[tokio::test] +async fn approve_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = SessionConfig::default() + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn deny_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .deny_all_permissions(); + let b = SessionConfig::default() + .deny_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_permissions_if_consults_predicate() { + let cfg = SessionConfig::default() + .approve_permissions_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); + let h = resolve_create(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_permissions_if_is_order_independent() { + let predicate = + |d: &PermissionRequestData| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell"); + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_permissions_if(predicate); + let b = SessionConfig::default() + .approve_permissions_if(predicate) + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn resume_session_config_approve_all_works() { + let cfg = ResumeSessionConfig::new(SessionId::from("s1")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let h = resolve_resume(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn resume_session_config_approve_all_is_order_independent() { + let a = ResumeSessionConfig::new(SessionId::from("s1")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = ResumeSessionConfig::new(SessionId::from("s1")) + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_resume(a).unwrap(); + let hb = resolve_resume(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[test] +fn session_config_enable_experimental_mode_serializes_when_set() { + let cfg = SessionConfig::default().with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("experimental-mode"))) + .expect("enable_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); +} + +#[test] +fn session_config_enable_experimental_mode_omitted_when_none() { + let cfg = SessionConfig::default(); + assert_eq!(cfg.enable_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("no-experimental-mode"))) + .expect("default config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); +} + +#[test] +fn resume_session_config_enable_experimental_mode_serializes_when_set() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) + .with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume enable_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); +} + +#[test] +fn resume_session_config_enable_experimental_mode_omitted_when_none() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); + assert_eq!(cfg.enable_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); +} diff --git a/rust/src/types/tests.rs b/rust/src/types/tests.rs new file mode 100644 index 0000000000..a61c083a4f --- /dev/null +++ b/rust/src/types/tests.rs @@ -0,0 +1,1927 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::collections::HashMap; +use std::path::PathBuf; + +use serde_json::json; + +use super::{ + AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, + AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, ConnectionState, + CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry, ExpFlagValue, + ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, InfiniteSessionConfig, + LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, + NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, + ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, + SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, + ToolResultResponse, ensure_attachment_display_names, +}; +use crate::generated::session_events::TypedSessionEvent; + +#[test] +fn permission_response_capability_is_publicly_exported() { + assert_eq!( + serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(), + json!("interactive") + ); +} + +#[test] +fn tool_builder_composes() { + let tool = Tool::new("greet") + .with_description("Say hello") + .with_namespaced_name("hello/greet") + .with_instructions("Pass the user's name") + .with_parameters(json!({ + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] + })) + .with_overrides_built_in_tool(true) + .with_skip_permission(true); + assert_eq!(tool.name, "greet"); + assert_eq!(tool.description, "Say hello"); + assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet")); + assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name")); + assert_eq!(tool.parameters.get("type").unwrap(), &json!("object")); + assert!(tool.overrides_built_in_tool); + assert!(tool.skip_permission); +} + +#[test] +fn tool_defer_serialization() { + let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto); + assert_eq!(tool.defer, Some(super::DeferMode::Auto)); + let value = serde_json::to_value(&tool).unwrap(); + assert_eq!(value.get("defer").unwrap(), &json!("auto")); + + let plain = Tool::new("plain"); + let value = serde_json::to_value(&plain).unwrap(); + assert!(value.get("defer").is_none()); +} + +#[test] +fn tool_metadata_serialization() { + use indexmap::IndexMap; + + let mut metadata = IndexMap::new(); + metadata.insert( + "github.com/copilot:safeForTelemetry".to_string(), + json!({ "name": true, "inputsNames": false }), + ); + let tool = Tool::new("lookup").with_metadata(metadata); + let value = serde_json::to_value(&tool).unwrap(); + assert_eq!( + value + .get("metadata") + .unwrap() + .get("github.com/copilot:safeForTelemetry") + .unwrap(), + &json!({ "name": true, "inputsNames": false }) + ); + + // Empty metadata is omitted on the wire. + let plain = Tool::new("plain"); + let value = serde_json::to_value(&plain).unwrap(); + assert!(value.get("metadata").is_none()); +} + +#[test] +fn custom_agent_config_builder_with_model() { + let agent = CustomAgentConfig::new("my-agent", "You are helpful.") + .with_model("claude-haiku-4.5") + .with_display_name("My Agent"); + assert_eq!(agent.name, "my-agent"); + assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5")); + assert_eq!(agent.display_name.as_deref(), Some("My Agent")); +} + +#[test] +fn custom_agent_config_serializes_model() { + let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["model"], "claude-haiku-4.5"); + assert_eq!(wire["name"], "model-agent"); +} + +#[test] +fn custom_agent_config_omits_model_when_none() { + let agent = CustomAgentConfig::new("no-model-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("model").is_none()); +} + +#[test] +fn custom_agent_config_builder_with_reasoning_effort() { + let agent = CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); +} + +#[test] +fn custom_agent_config_serializes_reasoning_effort() { + let agent = CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["reasoningEffort"], "high"); +} + +#[test] +fn custom_agent_config_omits_reasoning_effort_when_none() { + let agent = CustomAgentConfig::new("default-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("reasoningEffort").is_none()); +} + +#[test] +#[should_panic(expected = "tool parameter schema must be a JSON object")] +fn tool_with_parameters_panics_on_non_object_value() { + let _ = Tool::new("noop").with_parameters(json!(null)); +} + +#[test] +fn tool_result_expanded_serializes_binary_results_for_llm() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded { + text_result_for_llm: "rendered chart".to_string(), + result_type: "success".to_string(), + binary_results_for_llm: Some(vec![ToolBinaryResult { + data: "aW1n".to_string(), + mime_type: "image/png".to_string(), + r#type: "image".to_string(), + description: Some("chart preview".to_string()), + }]), + session_log: None, + error: None, + tool_telemetry: None, + tool_references: None, + }), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!( + wire, + json!({ + "result": { + "textResultForLlm": "rendered chart", + "resultType": "success", + "binaryResultsForLlm": [ + { + "data": "aW1n", + "mimeType": "image/png", + "type": "image", + "description": "chart preview" + } + ] + } + }) + ); +} + +#[test] +fn tool_result_expanded_omits_binary_results_for_llm_when_none() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded { + text_result_for_llm: "ok".to_string(), + result_type: "success".to_string(), + binary_results_for_llm: None, + session_log: None, + error: None, + tool_telemetry: None, + tool_references: None, + }), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!(wire["result"]["textResultForLlm"], "ok"); + assert!(wire["result"].get("binaryResultsForLlm").is_none()); +} + +#[test] +fn tool_result_expanded_serializes_tool_references() { + let response = ToolResultResponse { + result: ToolResult::Expanded( + ToolResultExpanded::new("found 2 tools", "success") + .with_tool_references(["get_weather", "check_status"]), + ), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!( + wire, + json!({ + "result": { + "textResultForLlm": "found 2 tools", + "resultType": "success", + "toolReferences": ["get_weather", "check_status"] + } + }) + ); +} + +#[test] +fn tool_result_expanded_omits_tool_references_when_none() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!(wire["result"]["textResultForLlm"], "ok"); + assert!(wire["result"].get("toolReferences").is_none()); +} + +#[test] +fn tool_result_expanded_with_tool_references_accepts_owned_strings() { + // The builder is generic over `Into`, so an owned `Vec` + // must compile and populate the field just like a `&str` array. + let names: Vec = vec!["alpha".to_string(), "beta".to_string()]; + let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); +} + +#[test] +fn tool_result_expanded_deserializes_tool_references() { + let wire = json!({ + "textResultForLlm": "found tools", + "resultType": "success", + "toolReferences": ["alpha", "beta"] + }); + + let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap(); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); +} + +#[test] +fn session_config_default_wire_flags_off_without_handlers() { + let cfg = SessionConfig::default(); + assert_eq!(cfg.mcp_oauth_token_storage, None); + assert_eq!(cfg.allowed_models, None); + // Wire flags are derived from handler presence at create_session + // time, not stored on the config. With no handlers installed, every + // request_* flag should serialize as false. + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("default-flags"))) + .expect("default config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); + assert!(json.get("allowedModels").is_none()); +} + +#[test] +fn resume_session_config_new_wire_flags_off_without_handlers() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags")); + assert_eq!(cfg.mcp_oauth_token_storage, None); + assert_eq!(cfg.allowed_models, None); + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); + assert!(json.get("allowedModels").is_none()); +} + +#[test] +fn session_configs_build_debug_and_serialize_allowed_models() { + let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]); + assert_eq!( + create.allowed_models.as_deref(), + Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..]) + ); + assert!(format!("{create:?}").contains("allowed_models")); + + let (create_wire, _) = create + .into_wire(Some(SessionId::from("create-allowed-models"))) + .expect("allowed model config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["allowedModels"], + json!(["gpt-5.4", "claude-sonnet-4"]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models")) + .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]); + assert_eq!( + resume.allowed_models.as_deref(), + Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..]) + ); + assert!(format!("{resume:?}").contains("allowed_models")); + + let (resume_wire, _) = resume + .into_wire() + .expect("resume allowed model config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["allowedModels"], + json!(["gpt-5.4", "gpt-5-mini"]) + ); +} + +#[test] +fn custom_agents_local_only_serializes_on_create_and_resume() { + let (create_wire, _) = SessionConfig::default() + .with_custom_agents_local_only(false) + .into_wire(Some(SessionId::from("create-locality"))) + .expect("create config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["customAgentsLocalOnly"], false); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality")) + .with_custom_agents_local_only(false) + .into_wire() + .expect("resume config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["customAgentsLocalOnly"], false); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("create-unset"))) + .expect("create config has no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("customAgentsLocalOnly").is_none()); + + let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset")) + .into_wire() + .expect("resume config has no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("customAgentsLocalOnly").is_none()); +} + +#[test] +fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = SessionConfig::default().with_enable_mcp_apps(true); + assert_eq!(cfg.enable_mcp_apps, Some(true)); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("enable-mcp-apps"))) + .expect("enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); +} + +#[test] +fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps")) + .with_enable_mcp_apps(true); + assert_eq!(cfg.enable_mcp_apps, Some(true)); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); +} + +#[test] +fn github_mcp_tool_config_serializes_for_create_and_resume() { + let github_config = GitHubMcpToolConfig::new() + .with_enable_all_tools(true) + .with_additional_toolsets(["repos"]) + .with_additional_tools(["get_issue"]) + .with_enable_insiders_mode(true) + .with_disable_form_deferral(true); + + let (create_wire, _) = SessionConfig::default() + .with_github_mcp_tool_config(github_config.clone()) + .into_wire(Some(SessionId::from("github-mcp"))) + .expect("create config has no duplicate handlers"); + assert_eq!( + serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"], + serde_json::json!({ + "enableAllTools": true, + "additionalToolsets": ["repos"], + "additionalTools": ["get_issue"], + "enableInsidersMode": true, + "disableFormDeferral": true, + }) + ); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp")) + .with_github_mcp_tool_config(github_config) + .into_wire() + .expect("resume config has no duplicate handlers"); + assert!(resume_wire.github_mcp_tool_config.is_some()); + + let (unset_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("github-mcp-unset"))) + .expect("default config has no duplicate handlers"); + assert!( + serde_json::to_value(&unset_wire) + .unwrap() + .get("githubMcpToolConfig") + .is_none() + ); +} + +#[test] +fn memory_configuration_constructors_and_serde() { + assert!(MemoryConfiguration::enabled().enabled); + assert!(!MemoryConfiguration::disabled().enabled); + assert!(MemoryConfiguration::disabled().with_enabled(true).enabled); + + let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap(); + assert_eq!(json, serde_json::json!({ "enabled": true })); +} + +#[test] +fn session_config_with_memory_serializes() { + let (wire, _runtime) = SessionConfig::default() + .with_memory(MemoryConfiguration::enabled()) + .into_wire(Some(SessionId::from("memory-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); + + let (wire_off, _) = SessionConfig::default() + .with_memory(MemoryConfiguration::disabled()) + .into_wire(Some(SessionId::from("memory-off"))) + .expect("no duplicate handlers"); + let json_off = serde_json::to_value(&wire_off).unwrap(); + assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false })); + + // Unset memory is omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("memory-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("memory").is_none()); +} + +#[test] +fn resume_session_config_with_memory_serializes() { + let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on")) + .with_memory(MemoryConfiguration::enabled()) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); + + // Unset memory is omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("memory").is_none()); +} + +#[test] +fn feature_flags_serialize_on_create_and_resume() { + let feature_flags = HashMap::from([ + ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), + ("DISABLED_TEST_FLAG".to_string(), false), + ]); + let expected = serde_json::json!({ + "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, + "DISABLED_TEST_FLAG": false, + }); + + let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); + assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); + let (create_wire, _) = create_config + .into_wire(Some(SessionId::from("feature-flags-create"))) + .expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["featureFlags"], expected); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) + .with_feature_flags(feature_flags) + .into_wire() + .expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["featureFlags"], expected); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("feature-flags-create-unset"))) + .expect("no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("featureFlags").is_none()); + + let (unset_resume_wire, _) = + ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("featureFlags").is_none()); +} + +fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { + CopilotExpAssignmentResponse { + features: vec!["copilot_exp_flag".to_string()], + flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]), + configs: vec![ExpConfigEntry { + id: "cfg-1".to_string(), + parameters: HashMap::from([ + ("threshold".to_string(), ExpFlagValue::Integer(5)), + ("enabled".to_string(), ExpFlagValue::Bool(true)), + ]), + }], + assignment_context: context.to_string(), + ..Default::default() + } +} + +#[test] +fn exp_flag_value_round_trips_all_variants() { + let values = serde_json::json!({ + "s": "text", + "i": 7, + "f": 1.5, + "b": true, + "n": null, + }); + let parsed: HashMap = serde_json::from_value(values.clone()).unwrap(); + assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string())); + assert_eq!(parsed["i"], ExpFlagValue::Integer(7)); + assert_eq!(parsed["f"], ExpFlagValue::Float(1.5)); + assert_eq!(parsed["b"], ExpFlagValue::Bool(true)); + assert_eq!(parsed["n"], ExpFlagValue::Null); + assert_eq!(serde_json::to_value(&parsed).unwrap(), values); +} + +#[test] +fn session_config_with_exp_assignments_serializes() { + let assignments = sample_exp_assignments("ctx-123"); + let expected = serde_json::to_value(&assignments).unwrap(); + let (wire, _runtime) = SessionConfig::default() + .with_exp_assignments(assignments) + .into_wire(Some(SessionId::from("exp-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["expAssignments"], expected); + assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123"); + assert_eq!( + json["expAssignments"]["Flights"]["copilot_exp_flag"], + "treatment" + ); + + // Unset exp assignments are omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("exp-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("expAssignments").is_none()); +} + +#[test] +fn resume_session_config_with_exp_assignments_serializes() { + let assignments = sample_exp_assignments("ctx-456"); + let expected = serde_json::to_value(&assignments).unwrap(); + let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on")) + .with_exp_assignments(assignments) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["expAssignments"], expected); + + // Unset exp assignments are omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("expAssignments").is_none()); +} + +#[test] +fn session_config_clone_preserves_exp_assignments() { + let assignments = sample_exp_assignments("ctx-clone"); + let config = SessionConfig::default().with_exp_assignments(assignments.clone()); + let cloned = config.clone(); + + assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); + + let (wire, _runtime) = cloned + .into_wire(Some(SessionId::from("exp-clone"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["expAssignments"], + serde_json::to_value(&assignments).unwrap() + ); +} + +#[test] +fn resume_session_config_clone_preserves_exp_assignments() { + let assignments = sample_exp_assignments("ctx-clone-resume"); + let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone")) + .with_exp_assignments(assignments.clone()); + let cloned = config.clone(); + + assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); + + let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["expAssignments"], + serde_json::to_value(&assignments).unwrap() + ); +} + +#[test] +#[allow(clippy::field_reassign_with_default)] +fn session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + use super::{CloudSessionOptions, CloudSessionRepository}; + + let mut cfg = SessionConfig::default(); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(false); + cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Concise); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); + cfg.enable_on_demand_instruction_discovery = Some(false); + cfg.cloud = Some(CloudSessionOptions::with_repository( + CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), + )); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("custom-id"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "custom-id"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "concise"); + assert_eq!(wire_json["remoteSession"], "export"); + assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); + assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); + assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); + assert_eq!(wire_json["cloud"]["repository"]["branch"], "main"); + + // Unset fields are omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("gitHubToken").is_none()); + assert!(empty_json.get("enableSessionTelemetry").is_none()); + assert!(empty_json.get("reasoningSummary").is_none()); + assert!(empty_json.get("remoteSession").is_none()); + assert!( + empty_json + .get("enableOnDemandInstructionDiscovery") + .is_none() + ); + assert!(empty_json.get("cloud").is_none()); +} + +#[test] +fn session_config_into_wire_serializes_named_providers_and_models() { + let cfg = SessionConfig::default() + .with_providers(vec![ + NamedProviderConfig::new("my-openai", "https://api.example.com/v1") + .with_provider_type("openai") + .with_wire_api("responses") + .with_model_provider("ollama") + .with_api_key("sk-test"), + ]) + .with_models(vec![ + ProviderModelConfig::new("gpt-x", "my-openai") + .with_wire_model("gpt-x-2025") + .with_max_output_tokens(2048), + ]); + + let (wire, _) = cfg + .into_wire(Some(SessionId::from("sess-providers"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["providers"][0]["name"], "my-openai"); + assert_eq!( + wire_json["providers"][0]["baseUrl"], + "https://api.example.com/v1" + ); + assert_eq!(wire_json["providers"][0]["type"], "openai"); + assert_eq!(wire_json["providers"][0]["wireApi"], "responses"); + assert_eq!(wire_json["providers"][0]["modelProvider"], "ollama"); + assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test"); + assert_eq!(wire_json["models"][0]["id"], "gpt-x"); + assert_eq!(wire_json["models"][0]["provider"], "my-openai"); + assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025"); + assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("providers").is_none()); + assert!(empty_json.get("models").is_none()); +} + +#[test] +fn resume_config_into_wire_serializes_named_providers_and_models() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume")) + .with_providers(vec![ + NamedProviderConfig::new("my-azure", "https://example.openai.azure.com") + .with_provider_type("azure") + .with_azure(AzureProviderOptions { + api_version: Some("2024-10-21".to_string()), + }), + ]) + .with_models(vec![ + ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"), + ]); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["providers"][0]["name"], "my-azure"); + assert_eq!(wire_json["providers"][0]["type"], "azure"); + assert_eq!( + wire_json["providers"][0]["azure"]["apiVersion"], + "2024-10-21" + ); + assert_eq!(wire_json["models"][0]["id"], "deploy-1"); + assert_eq!(wire_json["models"][0]["provider"], "my-azure"); + assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o"); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty")) + .into_wire() + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("providers").is_none()); + assert!(empty_json.get("models").is_none()); +} + +#[test] +fn session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let cfg = SessionConfig { + plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), + disabled_mcp_servers: Some(vec!["local-files".to_string(), "remote-github".to_string()]), + large_output: Some( + LargeToolOutputConfig::new() + .with_enabled(true) + .with_max_size_bytes(1024) + .with_output_directory(PathBuf::from("/tmp/large-output")), + ), + ..Default::default() + }; + + let (wire, _) = cfg + .into_wire(Some(SessionId::from("sess-1"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files", "remote-github"]) + ); + assert_eq!(wire_json["largeOutput"]["enabled"], true); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); + assert!(empty_json.get("largeOutput").is_none()); +} + +#[test] +fn resume_session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(true); + cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Detailed); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); + cfg.enable_on_demand_instruction_discovery = Some(false); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "sess-1"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "detailed"); + assert_eq!(wire_json["remoteSession"], "on"); + assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); + + // Unset remote_session is omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("reasoningSummary").is_none()); + assert!(empty_json.get("remoteSession").is_none()); + assert!( + empty_json + .get("enableOnDemandInstructionDiscovery") + .is_none() + ); +} + +#[test] +fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); + cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]); + cfg.large_output = Some( + LargeToolOutputConfig::new() + .with_enabled(false) + .with_max_size_bytes(2048) + .with_output_directory(PathBuf::from("/tmp/large-output-r")), + ); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files-r"]) + ); + assert_eq!(wire_json["largeOutput"]["enabled"], false); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); + assert!(empty_json.get("largeOutput").is_none()); +} + +#[test] +fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() { + let url = "https://example.com/oauth/client-metadata.json"; + + let (create_wire, _) = SessionConfig::default() + .with_auth_client_id_metadata_url(url) + .into_wire(None) + .expect("default create has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["authClientIdMetadataUrl"], url); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_auth_client_id_metadata_url(url) + .into_wire() + .expect("default resume has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["authClientIdMetadataUrl"], url); + + let (empty_create_wire, _) = SessionConfig::default() + .into_wire(None) + .expect("default create has no duplicate handlers"); + let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap(); + assert!(empty_create_json.get("authClientIdMetadataUrl").is_none()); + + let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap(); + assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none()); +} + +#[test] +fn session_config_clones_disabled_mcp_servers() { + let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); + let mut create_clone = create.clone(); + create_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + create.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_disabled_mcp_servers(["local-files"]); + let mut resume_clone = resume.clone(); + resume_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + resume.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); +} + +#[test] +fn session_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = SessionConfig::default() + .with_session_id(SessionId::from("sess-1")) + .with_model("claude-sonnet-4") + .with_client_name("test-app") + .with_reasoning_effort("medium") + .with_reasoning_summary(ReasoningSummary::Concise) + .with_context_tier("long_context") + .with_streaming(true) + .with_tools([Tool::new("greet")]) + .with_available_tools(["bash", "view"]) + .with_excluded_tools(["dangerous"]) + .with_mcp_servers(IndexMap::new()) + .with_mcp_oauth_token_storage("persistent") + .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(true) + .with_skill_directories([PathBuf::from("/tmp/skills")]) + .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) + .with_agent("researcher") + .with_config_directory(PathBuf::from("/tmp/config")) + .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) + .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .with_enable_session_telemetry(false) + .with_include_sub_agent_streaming_events(false) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); + + assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); + assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); + assert_eq!(cfg.client_name.as_deref(), Some("test-app")); + assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise)); + assert_eq!(cfg.context_tier.as_deref(), Some("long_context")); + assert_eq!(cfg.streaming, Some(true)); + assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); + assert_eq!( + cfg.available_tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert_eq!( + cfg.excluded_tools.as_deref(), + Some(&["dangerous".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); + assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true)); + assert_eq!( + cfg.skill_directories.as_deref(), + Some(&[PathBuf::from("/tmp/skills")][..]) + ); + assert_eq!( + cfg.disabled_skills.as_deref(), + Some(&["broken-skill".to_string()][..]) + ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + assert_eq!(cfg.agent.as_deref(), Some("researcher")); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); + assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + ); + assert_eq!(cfg.enable_session_telemetry, Some(false)); + assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); +} + +#[test] +fn resume_session_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) + .with_client_name("test-app") + .with_reasoning_summary(ReasoningSummary::None) + .with_context_tier("default") + .with_streaming(true) + .with_tools([Tool::new("greet")]) + .with_available_tools(["bash", "view"]) + .with_excluded_tools(["dangerous"]) + .with_mcp_servers(IndexMap::new()) + .with_mcp_oauth_token_storage("persistent") + .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(false) + .with_skill_directories([PathBuf::from("/tmp/skills")]) + .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) + .with_agent("researcher") + .with_config_directory(PathBuf::from("/tmp/config")) + .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) + .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .with_enable_session_telemetry(false) + .with_include_sub_agent_streaming_events(true) + .with_suppress_resume_event(true) + .with_continue_pending_work(true) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); + + assert_eq!(cfg.session_id.as_str(), "sess-2"); + assert_eq!(cfg.client_name.as_deref(), Some("test-app")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None)); + assert_eq!(cfg.context_tier.as_deref(), Some("default")); + assert_eq!(cfg.streaming, Some(true)); + assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); + assert_eq!( + cfg.available_tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert_eq!( + cfg.excluded_tools.as_deref(), + Some(&["dangerous".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); + assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false)); + assert_eq!( + cfg.skill_directories.as_deref(), + Some(&[PathBuf::from("/tmp/skills")][..]) + ); + assert_eq!( + cfg.disabled_skills.as_deref(), + Some(&["broken-skill".to_string()][..]) + ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + assert_eq!(cfg.agent.as_deref(), Some("researcher")); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); + assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + ); + assert_eq!(cfg.enable_session_telemetry, Some(false)); + assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); + assert_eq!(cfg.suppress_resume_event, Some(true)); + assert_eq!(cfg.continue_pending_work, Some(true)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); +} + +/// `continue_pending_work` must serialize to wire as `continuePendingWork` +/// — the runtime keys off this exact field name to opt into the +/// pending-work-handoff pattern. +#[test] +fn resume_session_config_serializes_continue_pending_work_to_camel_case() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["continuePendingWork"], true); + + // Unset case — skip_serializing_if must omit the field. + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("continuePendingWork").is_none()); +} + +#[test] +fn resume_policy_and_recovery_report_round_trip() { + let config = + ResumeSessionConfig::new(SessionId::from("sess-1")).with_allow_transcript_recovery(false); + let (wire, _) = config.into_wire().unwrap(); + let value = serde_json::to_value(&wire).unwrap(); + assert_eq!(value["allowTranscriptRecovery"], false); + + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .unwrap(); + assert!( + serde_json::to_value(&wire) + .unwrap() + .get("allowTranscriptRecovery") + .is_none() + ); + + let result: crate::types::ResumeSessionResult = serde_json::from_value(serde_json::json!({ + "sessionId": "sess-1", + "transcriptRecovery": { + "plannedBackupPath": "events.jsonl.backup", + "invalidLineNumbers": [2], + "sessionStartMoved": false + } + })) + .unwrap(); + let recovery = result.transcript_recovery.unwrap(); + assert_eq!(recovery.invalid_line_numbers, vec![2]); + assert_eq!(recovery.planned_backup_path, "events.jsonl.backup"); +} + +#[test] +fn session_configs_serialize_additional_directories() { + let create = SessionConfig::default().with_additional_directories([ + PathBuf::from("/tmp/shared"), + PathBuf::from("/tmp/generated"), + ]); + let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["additionalDirectories"], + serde_json::json!(["/tmp/shared", "/tmp/generated"]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_additional_directories([PathBuf::from("/tmp/resumed")]); + let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["additionalDirectories"], + serde_json::json!(["/tmp/resumed"]) + ); +} + +/// The Rust field is `suppress_resume_event`, but the wire field stays +/// `disableResume` to preserve compatibility with the runtime and other +/// SDKs. +#[test] +fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["disableResume"], true); + assert!(json.get("suppressResumeEvent").is_none()); +} + +/// `instruction_directories` must serialize to wire as +/// `instructionDirectories` on `SessionConfig`. +#[test] +fn session_config_serializes_instruction_directories_to_camel_case() { + let cfg = SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]); + let (wire, _) = cfg + .into_wire(Some(SessionId::from("instr-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["instructionDirectories"], + serde_json::json!(["/tmp/instr"]) + ); + + // Unset case — skip_serializing_if must omit the field. + let (wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("instr-off"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); +} + +/// Same check on the resume path. Forwarded to the CLI on +/// `session.resume`. +#[test] +fn resume_session_config_serializes_instruction_directories_to_camel_case() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_instruction_directories([PathBuf::from("/tmp/instr")]); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["instructionDirectories"], + serde_json::json!(["/tmp/instr"]) + ); + + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); +} + +#[test] +fn custom_agent_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.") + .with_display_name("Research Assistant") + .with_description("Investigates technical questions.") + .with_tools(["bash", "view"]) + .with_mcp_servers(IndexMap::new()) + .with_infer(true) + .with_skills(["rust-coding-skill"]); + + assert_eq!(cfg.name, "researcher"); + assert_eq!(cfg.prompt, "You are a research assistant."); + assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant")); + assert_eq!( + cfg.description.as_deref(), + Some("Investigates technical questions.") + ); + assert_eq!( + cfg.tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.infer, Some(true)); + assert_eq!( + cfg.skills.as_deref(), + Some(&["rust-coding-skill".to_string()][..]) + ); +} + +#[test] +fn mcp_servers_serialize_in_insertion_order() { + use indexmap::IndexMap; + + // Regression: `mcp_servers` was a `HashMap`, so the server keys (and + // thus the `session.create` payload) serialized in a per-process + // random order; `IndexMap` pins them to insertion order. The long + // sequence makes a `HashMap` regression reproduce this exact order by + // chance only 1/N!, avoiding a flaky false pass. + let order = [ + "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon", "ceres", + "nova", "kelp", "otter", "yodel", "plum", "garnet", + ]; + let mut servers = IndexMap::new(); + for name in order { + servers.insert( + name.to_string(), + McpServerConfig::Stdio(McpStdioServerConfig { + command: "run".to_string(), + ..Default::default() + }), + ); + } + + let (wire, _runtime) = SessionConfig::default() + .with_mcp_servers(servers) + .into_wire(None) + .expect("into_wire should succeed"); + let json = serde_json::to_string(&wire).expect("serialize wire"); + + let positions: Vec = order + .iter() + .map(|name| { + json.find(&format!("\"{name}\"")) + .unwrap_or_else(|| panic!("server {name} missing from wire JSON")) + }) + .collect(); + let mut ascending = positions.clone(); + ascending.sort_unstable(); + assert_eq!( + positions, ascending, + "mcp server keys must serialize in insertion order: {json}" + ); +} + +#[test] +fn infinite_session_config_builder_composes() { + let cfg = InfiniteSessionConfig::new() + .with_enabled(true) + .with_background_compaction_threshold(0.75) + .with_buffer_exhaustion_threshold(0.92); + + assert_eq!(cfg.enabled, Some(true)); + assert_eq!(cfg.background_compaction_threshold, Some(0.75)); + assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92)); +} + +#[test] +fn provider_config_builder_composes() { + use std::collections::HashMap; + + let mut headers = HashMap::new(); + headers.insert("X-Custom".to_string(), "value".to_string()); + + let cfg = ProviderConfig::new("https://api.example.com") + .with_provider_type("openai") + .with_wire_api("completions") + .with_transport("websockets") + .with_model_provider("lm_studio") + .with_api_key("sk-test") + .with_bearer_token("bearer-test") + .with_headers(headers) + .with_model_id("gpt-4") + .with_wire_model("azure-gpt-4-deployment") + .with_max_prompt_tokens(8192) + .with_max_output_tokens(2048); + + assert_eq!(cfg.base_url, "https://api.example.com"); + assert_eq!(cfg.provider_type.as_deref(), Some("openai")); + assert_eq!(cfg.wire_api.as_deref(), Some("completions")); + assert_eq!(cfg.transport.as_deref(), Some("websockets")); + assert_eq!(cfg.model_provider.as_deref(), Some("lm_studio")); + assert_eq!(cfg.api_key.as_deref(), Some("sk-test")); + assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test")); + assert_eq!( + cfg.headers + .as_ref() + .and_then(|h| h.get("X-Custom")) + .map(String::as_str), + Some("value"), + ); + assert_eq!(cfg.model_id.as_deref(), Some("gpt-4")); + assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment")); + assert_eq!(cfg.max_prompt_tokens, Some(8192)); + assert_eq!(cfg.max_output_tokens, Some(2048)); + + // Wire-shape: camelCase, skip_serializing_if when unset. + let wire = serde_json::to_value(&cfg).unwrap(); + assert_eq!(wire["modelId"], "gpt-4"); + assert_eq!(wire["wireModel"], "azure-gpt-4-deployment"); + assert_eq!(wire["modelProvider"], "lm_studio"); + assert_eq!(wire["maxPromptTokens"], 8192); + assert_eq!(wire["maxOutputTokens"], 2048); + + let unset = ProviderConfig::new("https://api.example.com"); + let wire_unset = serde_json::to_value(&unset).unwrap(); + assert!(wire_unset.get("modelId").is_none()); + assert!(wire_unset.get("wireModel").is_none()); + assert!(wire_unset.get("modelProvider").is_none()); + assert!(wire_unset.get("maxPromptTokens").is_none()); + assert!(wire_unset.get("maxOutputTokens").is_none()); +} + +#[test] +fn capi_session_options_builder_composes_and_serializes() { + let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false); + + assert_eq!(cfg.enable_web_socket_responses, Some(false)); + + let wire = serde_json::to_value(&cfg).unwrap(); + assert_eq!( + wire, + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let unset = CapiSessionOptions::new(); + let wire_unset = serde_json::to_value(&unset).unwrap(); + assert!(wire_unset.get("enableWebSocketResponses").is_none()); + assert!(wire_unset.get("autoTier").is_none()); + assert_eq!(wire_unset, json!({})); +} + +#[test] +fn capi_auto_tier_canonical_values_round_trip_and_forward() { + for (tier, value) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + (AutoTier::Fast, "fast"), + ] { + let exported: crate::AutoTier = tier.clone(); + let capi = CapiSessionOptions::new().with_auto_tier(exported); + assert_eq!(capi.auto_tier, Some(tier)); + assert_eq!( + serde_json::to_value(&capi).unwrap(), + json!({"autoTier": value}) + ); + assert_eq!( + serde_json::from_value::(json!({"autoTier": value})).unwrap(), + capi + ); + + let capi = capi.with_enable_web_socket_responses(false); + let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); + let (create, _) = SessionConfig::default() + .with_model("auto") + .with_capi(capi.clone()) + .into_wire(Some(SessionId::from("capi-create"))) + .unwrap(); + assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); + + let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(capi) + .into_wire() + .unwrap(); + assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); + } +} + +#[test] +fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { + for value in ["balanced", "Balance", "unknown"] { + assert_eq!( + serde_json::from_value::(json!(value)).unwrap(), + AutoTier::Unknown + ); + } + let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); + assert_eq!(capi.auto_tier, None); +} + +#[test] +fn session_config_with_capi_serializes() { + let (wire, _) = SessionConfig::default() + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .into_wire(Some(SessionId::from("capi-create"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("capi-create-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); +} + +#[test] +fn resume_session_config_with_capi_serializes() { + let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); +} + +#[test] +fn system_message_config_builder_composes() { + use std::collections::HashMap; + + let cfg = SystemMessageConfig::new() + .with_mode("replace") + .with_content("Custom system message.") + .with_sections(HashMap::new()); + + assert_eq!(cfg.mode.as_deref(), Some("replace")); + assert_eq!(cfg.content.as_deref(), Some("Custom system message.")); + assert!(cfg.sections.is_some()); +} + +#[test] +fn delivery_mode_serializes_to_kebab_case_strings() { + assert_eq!( + serde_json::to_string(&DeliveryMode::Enqueue).unwrap(), + "\"enqueue\"" + ); + assert_eq!( + serde_json::to_string(&DeliveryMode::Immediate).unwrap(), + "\"immediate\"" + ); + let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap(); + assert_eq!(parsed, DeliveryMode::Immediate); +} + +#[test] +fn agent_mode_serializes_to_kebab_case_strings() { + assert_eq!( + serde_json::to_string(&AgentMode::Interactive).unwrap(), + "\"interactive\"" + ); + assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\""); + assert_eq!( + serde_json::to_string(&AgentMode::Autopilot).unwrap(), + "\"autopilot\"" + ); + assert_eq!( + serde_json::to_string(&AgentMode::Shell).unwrap(), + "\"shell\"" + ); + let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap(); + assert_eq!(parsed, AgentMode::Plan); +} + +#[test] +fn connection_state_distinguishes_variants() { + // ConnectionState is now an internal type; verify we can construct + // and compare the variants used by the lifecycle code paths. + assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected); +} + +/// `agentId` is the sub-agent attribution field added in copilot-sdk +/// commit f8cf846 ("Derive session event envelopes from schema"). +/// Every other SDK (Node, Python, Go, .NET) carries it on the event +/// envelope; Rust must too or sub-agent events lose attribution at +/// the deserialization boundary. Cross-SDK parity test. +#[test] +fn session_event_round_trips_agent_id_on_envelope() { + let wire = json!({ + "id": "evt-1", + "timestamp": "2026-04-30T12:00:00Z", + "parentId": null, + "agentId": "sub-agent-42", + "type": "assistant.message", + "data": { "message": "hi" } + }); + + let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); + + // Round-trip preserves the field on the wire. + let roundtripped = serde_json::to_value(&event).unwrap(); + assert_eq!(roundtripped["agentId"], "sub-agent-42"); + + // Absent agentId remains absent (skip_serializing_if). + let main_agent_event: SessionEvent = serde_json::from_value(json!({ + "id": "evt-2", + "timestamp": "2026-04-30T12:00:01Z", + "parentId": null, + "type": "session.idle", + "data": {} + })) + .unwrap(); + assert!(main_agent_event.agent_id.is_none()); + let roundtripped = serde_json::to_value(&main_agent_event).unwrap(); + assert!(roundtripped.get("agentId").is_none()); +} + +/// Same parity for the typed event envelope produced by the codegen. +#[test] +fn typed_session_event_round_trips_agent_id_on_envelope() { + let wire = json!({ + "id": "evt-1", + "timestamp": "2026-04-30T12:00:00Z", + "parentId": null, + "agentId": "sub-agent-42", + "type": "session.idle", + "data": {} + }); + + let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); + assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); + + let roundtripped = serde_json::to_value(&event).unwrap(); + assert_eq!(roundtripped["agentId"], "sub-agent-42"); +} + +#[test] +fn connection_state_variants_compile() { + // Defensive smoke test: all variants must be constructable from + // within the crate. (The enum was demoted from pub to pub(crate) + // in Phase D; this test guards against accidental removal.) + let _ = ConnectionState::Disconnected; + let _ = ConnectionState::Connecting; + let _ = ConnectionState::Connected; + let _ = ConnectionState::Error; +} + +#[test] +fn deserializes_runtime_attachment_variants() { + let attachments: Vec = serde_json::from_value(json!([ + { + "type": "file", + "path": "/tmp/file.rs", + "displayName": "file.rs", + "lineRange": { "start": 7, "end": 12 } + }, + { + "type": "directory", + "path": "/tmp/project", + "displayName": "project" + }, + { + "type": "selection", + "filePath": "/tmp/lib.rs", + "displayName": "lib.rs", + "text": "fn main() {}", + "selection": { + "start": { "line": 1, "character": 2 }, + "end": { "line": 3, "character": 4 } + } + }, + { + "type": "blob", + "data": "Zm9v", + "mimeType": "image/png", + "displayName": "image.png" + }, + { + "type": "github_reference", + "number": 42, + "title": "Fix rendering", + "referenceType": "issue", + "state": "open", + "url": "https://github.com/example/repo/issues/42" + }, + { + "type": "extension_context", + "capturedAt": "2026-09-18T11:00:00Z", + "extensionId": "example:extension", + "title": "Unbound context" + } + ])) + .expect("attachments should deserialize"); + + assert_eq!(attachments.len(), 6); + assert!(matches!( + &attachments[0], + Attachment::File { + path, + display_name, + line_range: Some(AttachmentLineRange { start: 7, end: 12 }), + } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs") + )); + assert!(matches!( + &attachments[1], + Attachment::Directory { path, display_name } + if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project") + )); + assert!(matches!( + &attachments[2], + Attachment::Selection { + file_path, + display_name, + selection: + AttachmentSelectionRange { + start: AttachmentSelectionPosition { line: 1, character: 2 }, + end: AttachmentSelectionPosition { line: 3, character: 4 }, + }, + .. + } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs") + )); + assert!(matches!( + &attachments[3], + Attachment::Blob { + data, + mime_type, + display_name, + } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png") + )); + assert!(matches!( + &attachments[4], + Attachment::GitHubReference { + number: 42, + title, + reference_type: GitHubReferenceType::Issue, + state, + url, + } if title == "Fix rendering" + && state == "open" + && url == "https://github.com/example/repo/issues/42" + )); + assert!(matches!( + &attachments[5], + Attachment::ExtensionContext { + captured_at, + extension_id, + canvas_id: None, + instance_id: None, + title, + payload: None, + } if captured_at == "2026-09-18T11:00:00Z" + && extension_id == "example:extension" + && title == "Unbound context" + )); + assert_eq!( + serde_json::to_value(&attachments[5]).expect("serialize extension context"), + json!({ + "type": "extension_context", + "capturedAt": "2026-09-18T11:00:00Z", + "extensionId": "example:extension", + "title": "Unbound context" + }) + ); +} + +#[test] +fn ensures_display_names_for_variants_that_support_them() { + let mut attachments = vec![ + Attachment::File { + path: PathBuf::from("/tmp/file.rs"), + display_name: None, + line_range: None, + }, + Attachment::Selection { + file_path: PathBuf::from("/tmp/src/lib.rs"), + display_name: None, + text: "fn main() {}".to_string(), + selection: AttachmentSelectionRange { + start: AttachmentSelectionPosition { + line: 0, + character: 0, + }, + end: AttachmentSelectionPosition { + line: 0, + character: 10, + }, + }, + }, + Attachment::Blob { + data: "Zm9v".to_string(), + mime_type: "image/png".to_string(), + display_name: None, + }, + Attachment::GitHubReference { + number: 7, + title: "Track regressions".to_string(), + reference_type: GitHubReferenceType::Issue, + state: "open".to_string(), + url: "https://example.com/issues/7".to_string(), + }, + ]; + + ensure_attachment_display_names(&mut attachments); + + assert_eq!(attachments[0].display_name(), Some("file.rs")); + assert_eq!(attachments[1].display_name(), Some("lib.rs")); + assert_eq!(attachments[2].display_name(), Some("attachment")); + assert_eq!(attachments[3].display_name(), None); + assert_eq!( + attachments[3].label(), + Some("Track regressions".to_string()) + ); +} + +#[test] +fn github_anchored_attachment_variants_round_trip() { + let cases = vec![ + ( + "github_commit", + json!({ + "type": "github_commit", + "message": "Fix the thing", + "oid": "abc123", + "repo": { "id": 1, "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/commit/abc123" + }), + ), + ( + "github_release", + json!({ + "type": "github_release", + "name": "v1.2.3", + "repo": { "name": "repo", "owner": "octocat" }, + "tagName": "v1.2.3", + "url": "https://github.com/octocat/repo/releases/tag/v1.2.3" + }), + ), + ( + "github_actions_job", + json!({ + "type": "github_actions_job", + "conclusion": "failure", + "jobId": 99, + "jobName": "build", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/actions/runs/1/job/99", + "workflowName": "CI" + }), + ), + ( + "github_repository", + json!({ + "type": "github_repository", + "description": "An example repository", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo" + }), + ), + ( + "github_file_diff", + json!({ + "type": "github_file_diff", + "base": { + "path": "src/lib.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" } + }, + "head": { + "path": "src/lib.rs", + "ref": "feature", + "repo": { "name": "repo", "owner": "octocat" } + }, + "url": "https://github.com/octocat/repo/compare/main...feature" + }), + ), + ( + "github_tree_comparison", + json!({ + "type": "github_tree_comparison", + "base": { + "repo": { "name": "repo", "owner": "octocat" }, + "revision": "main" + }, + "head": { + "repo": { "name": "repo", "owner": "octocat" }, + "revision": "feature" + }, + "url": "https://github.com/octocat/repo/compare/main...feature" + }), + ), + ( + "github_url", + json!({ + "type": "github_url", + "url": "https://github.com/octocat/repo/wiki" + }), + ), + ( + "github_file", + json!({ + "type": "github_file", + "path": "src/main.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/blob/main/src/main.rs" + }), + ), + ( + "github_snippet", + json!({ + "type": "github_snippet", + "lineRange": { "start": 10, "end": 20 }, + "path": "src/main.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20" + }), + ), + ]; + + for (expected_type, input) in cases { + let attachment: Attachment = serde_json::from_value(input.clone()) + .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}")); + + // Serialize to a string first: parsing into `serde_json::Value` would + // silently dedupe a duplicate `type` key, hiding the exact regression + // this test guards against (e.g. a wrapped generated struct emitting its + // own `type` alongside the enum tag). + let serialized_string = serde_json::to_string(&attachment) + .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}")); + + // Exactly one `type` key, carrying the expected discriminator. + assert_eq!( + serialized_string.matches("\"type\":").count(), + 1, + "{expected_type} must serialize a single `type` key" + ); + + let serialized: serde_json::Value = serde_json::from_str(&serialized_string) + .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}")); + assert_eq!( + serialized.get("type").and_then(|value| value.as_str()), + Some(expected_type), + "{expected_type} must serialize the correct discriminator" + ); + + // Round-trips without dropping fields. + assert_eq!( + serialized, input, + "{expected_type} should round-trip without data loss" + ); + let reparsed: Attachment = serde_json::from_value(serialized) + .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}")); + assert_eq!( + reparsed, attachment, + "{expected_type} should re-deserialize to the same value" + ); + } +} diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 8bb8f76d3e..35a6dcf9ee 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -95,7 +95,10 @@ pub(crate) struct SessionCreateWire { pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub diagnostics: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + rename = "mcpOAuthTokenStorage", + skip_serializing_if = "Option::is_none" + )] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth_client_id_metadata_url: Option, @@ -120,6 +123,8 @@ pub(crate) struct SessionCreateWire { pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub has_skill_provider: Option, pub request_user_input: bool, pub request_permission: bool, pub request_exit_plan_mode: bool, @@ -262,7 +267,10 @@ pub(crate) struct SessionResumeWire { pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub diagnostics: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + rename = "mcpOAuthTokenStorage", + skip_serializing_if = "Option::is_none" + )] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth_client_id_metadata_url: Option, @@ -285,6 +293,8 @@ pub(crate) struct SessionResumeWire { pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub has_skill_provider: Option, pub request_user_input: bool, pub request_permission: bool, pub request_exit_plan_mode: bool, @@ -368,6 +378,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub allow_transcript_recovery: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub feature_flags: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index b8a0eafb9a..d0b670f0af 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -4,19 +4,291 @@ #![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, - 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::{ - PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent, + McpServerStatus, PermissionRequest, PermissionRequestedData, SessionEventData, + TypedSessionEvent, }; 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 [ + "preparing", + "prepared", + "awaiting-confirmation", + "revalidating", + "applying", + "completed", + ] { + let mut wire = serde_json::json!({ + "phase": phase, + "operationId": "original-operation", + "cancellationRequested": true, + }); + if phase == "completed" { + wire["outcome"] = serde_json::json!({ + "kind": "cancelled", + "operationId": "original-operation", + }); + } + let status: McpInstallationOperationStatus = serde_json::from_value(wire.clone()).unwrap(); + assert!(matches!( + (phase, &status), + ("preparing", McpInstallationOperationStatus::Preparing(_)) + | ("prepared", McpInstallationOperationStatus::Prepared(_)) + | ( + "awaiting-confirmation", + McpInstallationOperationStatus::AwaitingConfirmation(_) + ) + | ( + "revalidating", + McpInstallationOperationStatus::Revalidating(_) + ) + | ("applying", McpInstallationOperationStatus::Applying(_)) + | ("completed", McpInstallationOperationStatus::Completed(_)) + )); + assert_eq!(serde_json::to_value(status).unwrap(), wire); + } +} + +#[test] +fn host_start_round_trips_absent_and_legacy_child_pid() { + for pid in [None, Some(1234)] { + let mut wire = serde_json::json!({ + "hostId": "host", + "url": "ws://127.0.0.1:4321" + }); + if let Some(pid) = pid { + wire["pid"] = serde_json::json!(pid); + } + let host: github_copilot_sdk::rpc::HostStartResult = + serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(host.pid, pid); + assert_eq!(serde_json::to_value(host).unwrap(), wire); + } +} + +#[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!({ + "phase": "completed", + "operationId": "original-operation", + "cancellationRequested": false, + "outcome": { "kind": "declined", "operationId": "original-operation" }, + }); + for field in ["phase", "operationId", "cancellationRequested", "outcome"] { + let mut wire = original.clone(); + wire.as_object_mut().unwrap().remove(field); + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "unexpectedly accepted {wire}", + ); + } + for phase in [ + serde_json::Value::Null, + serde_json::json!("future-phase"), + serde_json::json!(42), + ] { + let mut wire = original.clone(); + wire["phase"] = phase; + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "unexpectedly accepted {wire}", + ); + } +} + +#[test] +fn manual_mcp_requests_omit_unselected_owned_identity() { + let payloads = [ + serde_json::to_value(McpEnableRequest { + server_name: "manual".to_string(), + }) + .unwrap(), + serde_json::to_value(McpDisableRequest { + server_name: "manual".to_string(), + }) + .unwrap(), + serde_json::to_value(McpStopServerRequest { + server_name: "manual".to_string(), + }) + .unwrap(), + serde_json::to_value(McpOauthLoginRequest { + server_name: "manual".to_string(), + ..Default::default() + }) + .unwrap(), + serde_json::to_value(McpEnableOptions::new("manual")).unwrap(), + serde_json::to_value(McpOauthLoginOptions::new("manual")).unwrap(), + ]; + for payload in payloads { + assert_eq!(payload, serde_json::json!({"serverName": "manual"})); + } +} + +#[test] +fn owned_mcp_identity_is_only_reachable_through_options() { + assert_eq!( + serde_json::to_value( + McpEnableOptions::new("owned").expected_installation_id("a".repeat(32)) + ) + .unwrap(), + serde_json::json!({"serverName": "owned", "expectedInstallationId": "a".repeat(32)}) + ); + assert_eq!( + serde_json::to_value( + McpOauthLoginOptions::new("owned") + .expected_installation_id("a".repeat(32)) + .login_id("b".repeat(32)) + ) + .unwrap(), + serde_json::json!({ + "serverName": "owned", + "expectedInstallationId": "a".repeat(32), + "loginId": "b".repeat(32), + }) + ); +} + #[test] fn session_events_deserialize_auto_tier() { for event_type in ["session.start", "session.resume"] { @@ -181,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] @@ -194,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 { @@ -431,3 +813,99 @@ fn set_model_options_distinguishes_unset_tier_from_reset() { let cleared = SetModelOptions::default().with_reset_auto_tier(); assert_eq!(cleared.auto_tier, Some(AutoTierPreference::Reset)); } + +#[test] +fn listed_mcp_server_literals_with_defaults_survive_the_owned_marker() { + let manual = McpServer { + name: "manual".to_string(), + status: McpServerStatus::Stopped, + ..Default::default() + }; + assert!(manual.owned.is_none()); + assert!( + serde_json::to_value(&manual) + .unwrap() + .get("owned") + .is_none() + ); + + let owned: McpServer = serde_json::from_value(serde_json::json!({ + "name": "owned", + "status": "stopped", + "owned": {"installationId": "installation"}, + })) + .unwrap(); + assert_eq!(owned.owned.unwrap().installation_id, "installation"); +} + +/// Released structs and enums that are unrelated to the MCP installation payloads keep +/// distinct types, so downstream trait impls, variant imports and literals still compile. +mod released_type_shapes { + use github_copilot_sdk::rpc::{ + DiagnosticsReadResultEntriesItem, DiagnosticsReadResultEntriesItemSource, + MetadataContextAttributionResultContextAttribution, MetadataContextInfoResultContextInfo, + SendMessagesRequestResponseFormat, SendMessagesRequestResponseFormatType, + SendRequestResponseFormat, SendRequestResponseFormatType, + SessionDiagnosticsReadResultEntriesItem, SessionDiagnosticsReadResultEntriesItemSource, + SessionMetadataContextInfoResultContextInfo, + SessionMetadataGetContextAttributionResultContextAttribution, + SessionMetadataSnapshotResultWorkspace, SessionMetadataSnapshotWorkspace, + UpdateSubagentSettingsRequestSubagents, + }; + + trait Released { + const NAME: &'static str; + } + macro_rules! released { + ($($ty:ty),* $(,)?) => { $(impl Released for $ty { const NAME: &'static str = stringify!($ty); })* }; + } + // Separate impls on each released name fail to compile if two names alias one type. + released!( + DiagnosticsReadResultEntriesItem, + SessionDiagnosticsReadResultEntriesItem, + DiagnosticsReadResultEntriesItemSource, + SessionDiagnosticsReadResultEntriesItemSource, + MetadataContextAttributionResultContextAttribution, + SessionMetadataGetContextAttributionResultContextAttribution, + MetadataContextInfoResultContextInfo, + SessionMetadataContextInfoResultContextInfo, + SendRequestResponseFormat, + SendMessagesRequestResponseFormat, + SendRequestResponseFormatType, + SendMessagesRequestResponseFormatType, + SessionMetadataSnapshotWorkspace, + SessionMetadataSnapshotResultWorkspace, + UpdateSubagentSettingsRequestSubagents, + ); + + #[test] + fn released_names_are_distinct_types() { + assert_ne!( + ::NAME, + ::NAME + ); + } + + #[test] + fn released_enum_variants_import_and_match() { + use SendRequestResponseFormatType::*; + let format = SendRequestResponseFormat { + r#type: JsonSchema, + ..Default::default() + }; + assert!(matches!(format.r#type, JsonSchema)); + use DiagnosticsReadResultEntriesItemSource::Mcp; + assert!(matches!( + DiagnosticsReadResultEntriesItemSource::default(), + Mcp + )); + assert!(matches!( + SessionDiagnosticsReadResultEntriesItemSource::default(), + SessionDiagnosticsReadResultEntriesItemSource::Mcp + )); + assert!(matches!( + SendMessagesRequestResponseFormatType::default(), + SendMessagesRequestResponseFormatType::JsonSchema + )); + } +} 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/catalog_candidate_test.rs b/rust/tests/catalog_candidate_test.rs index 79f6290fa7..52bedc3dfa 100644 --- a/rust/tests/catalog_candidate_test.rs +++ b/rust/tests/catalog_candidate_test.rs @@ -198,3 +198,131 @@ fn digest_algorithm_enforces_its_field_constraint() { let error = serde_json::from_value::(wire).unwrap_err(); assert!(error.to_string().contains("sha256-rfc8785")); } + +#[test] +fn candidate_trust_preserves_raw_metadata() { + for status in [ + "current", + "absent", + "stale", + "downgraded", + "revoked", + "unsupported", + "malformed", + ] { + let mut trust = json!({ + "schemaVersion": "v1", + "status": status, + "eligibility": "unknown", + "provenance": { + "source": "agent-finder", + "observedAt": "2026-09-02T11:00:00Z" + } + }); + if status == "current" { + trust["tier"] = json!("T1"); + } + let mut wire = search_result_wire()["candidates"][0].clone(); + wire["trust"] = trust.clone(); + let candidate: CatalogMcpServerCandidate = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(candidate.trust.as_ref().unwrap(), &trust); + assert_eq!(serde_json::to_value(candidate).unwrap(), wire); + } +} + +#[test] +fn optional_trust_does_not_reject_candidates_or_discard_unbounded_metadata() { + let current = json!({ + "schemaVersion": "v1", + "status": "current", + "tier": "T1", + "eligibility": "unknown", + "provenance": { + "source": "agent-finder", + "observedAt": "2026-09-02T11:00:00Z" + } + }); + let mut inputs = vec![ + json!(42), + json!("private-invalid"), + json!([]), + json!({}), + json!({"schemaVersion": "v2"}), + json!({"schemaVersion": "v2", "status": null, "eligibility": 42}), + json!({"schemaVersion": "v2", "provenance": {"observedAt": "changed-format"}}), + json!({"schemaVersion": "v2", "extra": "x".repeat(4097)}), + ]; + for (field, value) in [ + ("schemaVersion", "v2"), + ("status", "future-status"), + ("eligibility", "approved"), + ("tier", "T3"), + ] { + let mut snapshot = current.clone(); + snapshot[field] = json!(value); + inputs.push(snapshot); + } + let mut unknown_authority = current.clone(); + unknown_authority["provenance"]["source"] = json!("future-authority"); + inputs.push(unknown_authority); + for field in [ + "schemaVersion", + "status", + "eligibility", + "provenance", + "tier", + ] { + let mut snapshot = current.clone(); + snapshot.as_object_mut().unwrap().remove(field); + inputs.push(snapshot); + } + for time in ["not-a-date", "2026-09-18T10:00:00", "2026-02-30T10:00:00Z"] { + let mut snapshot = current.clone(); + snapshot["provenance"]["observedAt"] = json!(time); + inputs.push(snapshot); + } + // Hosts must receive both sides of their bounds intact before projecting trust. + for extra in [ + json!("x".repeat(4096)), + json!("x".repeat(4097)), + json!({ "x".repeat(64): 0 }), + json!({ "x".repeat(65): "private-key" }), + json!([[["bounded"]]]), + json!([[[["too-deep"]]]]), + json!([[[[[["private-depth"]]]]]]), + json!(vec![0; 119]), + json!(vec![0; 120]), + json!(vec![0; 129]), + ] { + let mut snapshot = current.clone(); + snapshot["extra"] = extra; + inputs.push(snapshot); + } + + for trust in inputs { + let mut wire = search_result_wire(); + for candidate in wire["candidates"].as_array_mut().unwrap() { + candidate["trust"] = trust.clone(); + } + let result: CatalogSearchResult = serde_json::from_value(wire).unwrap(); + let CatalogSearchResult::Succeeded(result) = result else { + panic!("optional trust must not discard a successful search"); + }; + assert_eq!(result.candidates.len(), 2); + for candidate in result.candidates { + assert_eq!(serde_json::to_value(candidate).unwrap()["trust"], trust); + } + } +} + +#[test] +fn optional_trust_keeps_missing_and_null_metadata_unavailable() { + for trust in [None, Some(Value::Null)] { + let mut wire = search_result_wire()["candidates"][0].clone(); + if let Some(trust) = trust { + wire["trust"] = trust; + } + let candidate: CatalogMcpServerCandidate = serde_json::from_value(wire).unwrap(); + assert!(candidate.trust.is_none()); + } +} 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..8c4e912bb5 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,10 @@ mod session_fs_sqlite; mod session_lifecycle; #[path = "e2e/session_todos_changed.rs"] mod session_todos_changed; +#[path = "e2e/set_tools.rs"] +mod set_tools; +#[path = "e2e/skill_provider.rs"] +mod skill_provider; #[path = "e2e/skills.rs"] mod skills; #[path = "e2e/streaming_fidelity.rs"] diff --git a/rust/tests/e2e/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.rs b/rust/tests/e2e/client.rs index 3abedce1a8..2e5d90c6d6 100644 --- a/rust/tests/e2e/client.rs +++ b/rust/tests/e2e/client.rs @@ -102,6 +102,41 @@ async fn should_get_authenticated_status() { .await; } +#[tokio::test] +async fn should_use_configured_github_host_for_authentication() { + with_e2e_context("client", "should_get_authenticated_status", |ctx| { + Box::pin(async move { + for (copilot_host, gh_host, expected_host) in [ + ( + "tenant.ghe.example", + "fallback.ghe.example", + "https://tenant.ghe.example", + ), + ("", "fallback.ghe.example", "https://fallback.ghe.example"), + ("", "", "https://github.com"), + ] { + let _host_env = super::support::set_inprocess_host_for_test(copilot_host, gh_host); + let mut options = + ctx.client_options_with_github_token(super::support::DEFAULT_TEST_TOKEN); + if !is_inprocess_default() { + options.env.extend([ + ("COPILOT_GH_HOST".into(), copilot_host.into()), + ("GH_HOST".into(), gh_host.into()), + ]); + } + let client = Client::start(options).await.expect("start client"); + let status = client.get_auth_status().await.expect("auth status"); + + assert!(status.is_authenticated, "{status:?}"); + assert_eq!(status.host.as_deref(), Some(expected_host)); + + client.stop().await.expect("stop client"); + } + }) + }) + .await; +} + #[tokio::test] async fn should_list_models_when_authenticated() { // TODO(cli-1.0.81-2): CLI 1.0.81-2 stopped honoring client-level GitHub tokens over the diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index b39506ec02..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(); @@ -651,6 +695,252 @@ async fn remote_resource_mismatch_is_observable_before_resume() { ); } +#[tokio::test] +async fn stdio_stop_waits_for_eof_cleanup() { + let fake = ShutdownCli::new("stop"); + let client = Client::start(fake.options()).await.expect("start fake CLI"); + let pid = client.pid().expect("owned child"); + tokio::time::timeout(Duration::from_secs(40), client.stop()) + .await + .expect("shutdown RPC, graceful exit, and reap must be bounded") + .expect("stop client"); + assert_eq!( + std::fs::read_to_string(&fake.marker).expect("EOF cleanup"), + "{\"type\":\"span\"}\n" + ); + assert!(!process_is_alive(pid).await); + client.stop().await.expect("repeated stop"); +} + +#[tokio::test] +async fn stdio_stop_drains_stdout_during_eof_cleanup() { + let fake = FakeCli::new(); + let client = Client::start(fake.client_options_with_behavior("token", "shutdown-output")) + .await + .expect("start fake CLI"); + tokio::time::timeout(Duration::from_secs(40), client.stop()) + .await + .expect("shutdown RPC, graceful exit, and reap must be bounded") + .expect("stop client"); + assert_eq!( + std::fs::read_to_string(fake.capture_path.with_extension("cleanup")) + .expect("cleanup after draining final stdout"), + "flushed" + ); +} + +#[tokio::test] +async fn stdio_stop_preserves_shutdown_error_after_eof_cleanup() { + let fake = FakeCli::new(); + let client = Client::start(fake.client_options_with_behavior("token", "shutdown-error")) + .await + .expect("start fake CLI"); + let errors = tokio::time::timeout(Duration::from_secs(40), client.stop()) + .await + .expect("shutdown RPC, graceful exit, and reap must be bounded") + .expect_err("shutdown error must be returned"); + assert!(errors.to_string().contains("shutdown rejected"), "{errors}"); + assert_eq!( + std::fs::read_to_string(fake.capture_path.with_extension("cleanup")) + .expect("EOF cleanup despite RPC failure"), + "flushed" + ); +} + +#[tokio::test] +async fn stdio_stop_bounds_unresponsive_shutdown_and_eof() { + let fake = FakeCli::new(); + let client = Client::start(fake.client_options_with_behavior("token", "ignore-shutdown")) + .await + .expect("start fake CLI"); + let pid = client.pid().expect("owned child"); + let start = std::time::Instant::now(); + let errors = tokio::time::timeout(Duration::from_secs(40), client.stop()) + .await + .expect("10s RPC + 10s graceful exit + 10s reap must be bounded") + .expect_err("unanswered shutdown must report its timeout"); + assert!(errors.to_string().contains("timed out"), "{errors}"); + assert!(start.elapsed() >= Duration::from_secs(20)); + assert!(!process_is_alive(pid).await); + assert!(!fake.capture_path.with_extension("cleanup").exists()); +} + +#[tokio::test] +async fn stdio_stop_terminates_child_that_does_not_exit_after_eof() { + let fake = ShutdownCli::new("fallback"); + let client = Client::start(fake.options()).await.expect("start fake CLI"); + let pid = client.pid().expect("owned child"); + let start = std::time::Instant::now(); + tokio::time::timeout(Duration::from_secs(40), client.stop()) + .await + .expect("shutdown RPC, graceful exit, and reap must be bounded") + .expect("fallback termination succeeds"); + assert!(start.elapsed() >= Duration::from_secs(10)); + assert!(!process_is_alive(pid).await); + assert!( + fake.marker.exists(), + "fixture must observe shutdown and EOF" + ); + assert_eq!( + std::fs::read_to_string(&fake.marker).expect("EOF cleanup"), + "{\"type\":\"span\"}\n" + ); +} + +#[tokio::test] +async fn stdio_force_stop_interrupts_graceful_exit_wait() { + let fake = ShutdownCli::new("fallback"); + let client = Client::start(fake.options()).await.expect("start fake CLI"); + let pid = client.pid().expect("owned child"); + let stopping = tokio::spawn({ + let client = client.clone(); + async move { client.stop().await } + }); + wait_for_cleanup_marker(&fake.marker).await; + + client.force_stop(); + tokio::time::timeout(Duration::from_secs(5), async { + stopping.await.expect("stop task").expect("stop client"); + wait_for_process_exit(pid).await; + }) + .await + .expect("force stop must interrupt grace before its 10-second deadline"); +} + +#[tokio::test] +async fn stdio_cancelled_graceful_exit_wait_terminates_child() { + let fake = ShutdownCli::new("fallback"); + let client = Client::start(fake.options()).await.expect("start fake CLI"); + let pid = client.pid().expect("owned child"); + let stopping = tokio::spawn({ + let client = client.clone(); + async move { client.stop().await } + }); + wait_for_cleanup_marker(&fake.marker).await; + + stopping.abort(); + assert!( + stopping + .await + .expect_err("stop task cancelled") + .is_cancelled() + ); + wait_for_process_exit(pid).await; + assert!(client.pid().is_none()); +} + +async fn wait_for_cleanup_marker(path: &std::path::Path) { + tokio::time::timeout(Duration::from_secs(5), async { + loop { + match std::fs::read_to_string(path) { + Ok(content) if content == "{\"type\":\"span\"}\n" => return, + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => panic!("read cleanup marker: {error}"), + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("fixture must reach EOF cleanup during graceful shutdown"); +} + +#[tokio::test] +async fn stdio_force_stop_and_drop_remain_immediate() { + for force in [true, false] { + let fake = ShutdownCli::new("force"); + let client = Client::start(fake.options()).await.expect("start fake CLI"); + let pid = client.pid().expect("owned child"); + if force { + client.force_stop(); + assert!(client.pid().is_none()); + } + drop(client); + wait_for_process_exit(pid).await; + assert!(!fake.marker.exists()); + } +} + +#[tokio::test] +async fn stdio_startup_failure_terminates_owned_child() { + let fake = ShutdownCli::new("start-failure"); + let result = tokio::time::timeout(Duration::from_secs(5), Client::start(fake.options())) + .await + .expect("startup failure must not await graceful shutdown"); + assert!(result.is_err()); + let pid = std::fs::read_to_string(&fake.pid_file) + .expect("read child PID") + .parse() + .expect("parse child PID"); + wait_for_process_exit(pid).await; + assert!(!fake.marker.exists()); +} + +struct ShutdownCli { + dir: TempDir, + marker: PathBuf, + pid_file: PathBuf, + mode: &'static str, +} + +impl ShutdownCli { + fn new(mode: &'static str) -> Self { + let dir = tempfile::tempdir().expect("create shutdown fixture directory"); + Self { + marker: dir.path().join("cleanup.jsonl"), + pid_file: dir.path().join("child.pid"), + dir, + mode, + } + } + + fn options(&self) -> ClientOptions { + let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../test/harness/stdio-shutdown-runtime.cjs"); + ClientOptions::new() + .with_program(CliProgram::Path("node".into())) + .with_prefix_args([ + script.into_os_string(), + self.marker.clone().into_os_string(), + self.mode.into(), + self.pid_file.clone().into_os_string(), + ]) + .with_cwd(self.dir.path()) + .with_use_logged_in_user(false) + .with_transport(Transport::Stdio) + } +} + +async fn process_is_alive(pid: u32) -> bool { + let output = tokio::process::Command::new("node") + .args([ + "-e", + "try { process.kill(Number(process.argv[1]), 0); } catch (e) { if (e.code === 'ESRCH') process.exit(3); throw e; }", + &pid.to_string(), + ]) + .output() + .await + .expect("query child process"); + match output.status.code() { + Some(0) => true, + Some(3) => false, + _ => panic!( + "query child process: {}", + String::from_utf8_lossy(&output.stderr) + ), + } +} + +async fn wait_for_process_exit(pid: u32) { + tokio::time::timeout(Duration::from_secs(5), async { + while process_is_alive(pid).await { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("owned child must exit promptly"); +} + struct FakeCli { _dir: TempDir, script_path: PathBuf, @@ -776,6 +1066,29 @@ process.stdin.on("data", chunk => { processBuffer(); }); process.stdin.resume(); +if (["ignore-shutdown", "shutdown-error", "shutdown-output"].includes(behavior)) { + const keepAlive = setInterval(() => {}, 1000); + process.stdin.on("end", () => { + if (behavior === "ignore-shutdown") return; + if (behavior === "shutdown-output") { + const body = JSON.stringify({ + jsonrpc: "2.0", + method: "shutdown.output", + params: { output: "x".repeat(1024 * 1024) }, + }); + process.stdout.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`, error => { + if (error) throw error; + fs.writeFileSync(captureFile.replace(/\.json$/, ".cleanup"), "flushed"); + clearInterval(keepAlive); + }); + return; + } + setTimeout(() => { + fs.writeFileSync(captureFile.replace(/\.json$/, ".cleanup"), "flushed"); + clearInterval(keepAlive); + }, 100); + }); +} function processBuffer() { while (true) { @@ -804,6 +1117,11 @@ function handleMessage(message) { writeResponse(message.id, { ok: true, protocolVersion: 3, version: "fake" }); return; } + if (message.method === "runtime.shutdown" && behavior === "ignore-shutdown") return; + if (message.method === "runtime.shutdown" && behavior === "shutdown-error") { + writeMessage({ jsonrpc: "2.0", id: message.id, error: { code: -32000, message: "shutdown rejected" } }); + return; + } if (message.method === "ping") { writeResponse(message.id, { message: "pong", protocolVersion: 3, timestamp: Date.now() }); return; @@ -864,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/copilot_request_handler.rs b/rust/tests/e2e/copilot_request_handler.rs index 4e4004b9cf..9fd7af5ae8 100644 --- a/rust/tests/e2e/copilot_request_handler.rs +++ b/rust/tests/e2e/copilot_request_handler.rs @@ -15,6 +15,9 @@ //! transport error rather than hanging the turn. //! - `observes_runtime_driven_cancel` — a handler that blocks until the consumer //! aborts observes the runtime-driven cancellation via `ctx.cancel`. +//! - `withdrawn_running_turn_prompt_leaves_persisted_history` — taking back +//! the prompt of a turn the model has not answered removes it from the +//! persisted session, so a resume does not bring it back. use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -24,7 +27,7 @@ use async_trait::async_trait; use bytes::Bytes; use futures_util::{SinkExt, StreamExt}; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::rpc::{SendMode, SendRequest}; +use github_copilot_sdk::rpc::{QueueWithdrawMessageRequest, SendMode, SendRequest}; use github_copilot_sdk::session_events::{AssistantMessageData, UserMessageData}; use github_copilot_sdk::{ CopilotHttpRequest, CopilotHttpResponse, CopilotRequestContext, CopilotRequestError, @@ -947,3 +950,103 @@ async fn observes_runtime_driven_cancel() { }) .await; } + +// --------------------------------------------------------------------------- +// Scenario 3c: take-back of a running turn's prompt. While the handler holds +// the only model request, the withdrawal interrupts the turn and must remove +// its events from the persisted session, not only from the live one. +// --------------------------------------------------------------------------- + +/// Event types a withdrawn turn records before any model output, all of which +/// the removal must take with the prompt. +const WITHDRAWN_TURN_EVENT_TYPES: &[&str] = &[ + "user.message", + "assistant.turn_start", + "assistant.turn_end", + "abort", +]; + +#[tokio::test] +async fn withdrawn_running_turn_prompt_leaves_persisted_history() { + if super::support::skip_inprocess("LLM inference providers are process-global in-process") { + return; + } + with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let handler = Arc::new(CancellingHandler::default()); + let client = ctx.start_llm_client(handler.clone(), &[]).await; + let session = client + .create_session(ctx.approve_all_session_config()) + .await + .expect("create session"); + let session_id = session.id().clone(); + + let options = say_ok(); + let prompt = options.prompt.clone(); + let message_id = session.send(options).await.expect("send"); + wait_for_flag(&handler.inference_entered, "inference entered").await; + let result = session + .rpc() + .queue() + .withdraw_message(QueueWithdrawMessageRequest { + message_id, + expected_prompt: prompt.clone(), + }) + .await + .expect("withdraw the running turn's prompt"); + assert!( + result.removed && result.interrupted, + "expected the running turn's prompt to be withdrawn, got {result:?}" + ); + let live_prompts = session + .get_events() + .await + .expect("live events") + .iter() + .filter_map(|event| event.typed_data::()) + .map(|data| data.content) + .collect::>(); + assert!( + !live_prompts.contains(&prompt), + "the withdrawn prompt is still in the live session" + ); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + + // A resume replays this log, so the prompt must not be in it. + let log_path = ctx + .home_dir() + .join("session-state") + .join(session_id.as_str()) + .join("events.jsonl"); + let log = std::fs::read_to_string(&log_path).expect("read persisted events"); + let persisted = log + .lines() + .map(|line| serde_json::from_str::(line).expect("persisted event JSON")) + .collect::>(); + assert!( + persisted + .iter() + .any(|event| event["type"] == "session.start"), + "expected {} to hold the session", + log_path.display() + ); + // The whole turn is removed, not only its prompt: an orphaned turn + // event left behind would be replayed by a resume too. + let persisted_turn_events = persisted + .iter() + .filter(|event| { + event["type"] + .as_str() + .is_some_and(|kind| WITHDRAWN_TURN_EVENT_TYPES.contains(&kind)) + }) + .collect::>(); + assert!( + persisted_turn_events.is_empty(), + "the withdrawn turn is still persisted: {persisted_turn_events:?}" + ); + }) + }) + .await; +} 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/pending_work_resume.rs b/rust/tests/e2e/pending_work_resume.rs index 317e5308f3..0423db7018 100644 --- a/rust/tests/e2e/pending_work_resume.rs +++ b/rust/tests/e2e/pending_work_resume.rs @@ -123,6 +123,15 @@ async fn should_continue_pending_external_tool_request_after_resume() { }) .await .expect("complete pending tool"); + if !result.success { + let event_types = session2.get_events().await.map(|events| { + events + .iter() + .map(|event| format!("{:?}", event.parsed_type())) + .collect::>() + }); + panic!("pending external tool response was rejected; events: {event_types:?}"); + } assert!(result.success); phase("wait for resumed assistant answer"); assistant.await; 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 2d8b2a2475..0000000000 --- a/rust/tests/e2e/rpc_session_state.rs +++ /dev/null @@ -1,1264 +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] -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 20a56e6882..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_factory_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.factory.run", - FactoryRunResult { - attempt: Some(2), - result: Some(json!({"artifact": "factory-output"})), - run_id: "run-123".to_string(), - ..Default::default() - }, - ); - results.insert( - "session.factory.resume", - FactoryResumeResult { - factory_name: "coverage-factory".to_string(), - run: FactoryRunResult { - run_id: "run-123".to_string(), - ..Default::default() - }, - }, - ); - results.insert_default::("session.factory.getRun"); - results.insert_default::("session.factory.listRuns"); - results.insert_default::("session.factory.getRunDetail"); - results.insert_default::("session.factory.getRunProgress"); - results.insert_default::("session.factory.cancel"); - results.insert_default::("session.factory.pause"); - results.insert_default::("session.factory.log"); - results.insert_default::("session.factory.agent"); - results.insert_default::("session.factory.journal.get"); - results.insert_default::("session.factory.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().factory().run(FactoryRunRequest { - args: json!({"mode": "offline", "count": 2}), - name: "coverage-factory".to_string(), - options: None, - })); - assert_eq!(run.run_id, "run-123"); - assert_eq!(run.attempt, Some(2)); - assert_eq!(run.result, Some(json!({"artifact": "factory-output"}))); - let resumed = rpc_ok!(session.rpc().factory().resume(FactoryResumeRequest { - run_id: "run-123".to_string(), - notify_on_complete: Some(false), - ..Default::default() - })); - assert_eq!(resumed.factory_name, "coverage-factory"); - assert_eq!(resumed.run.run_id, "run-123"); - rpc_ok!( - session - .rpc() - .factory() - .get_run(FactoryGetRunRequest::default()) - ); - rpc_ok!( - session - .rpc() - .factory() - .list_runs(FactoryListRunsRequest::default()) - ); - rpc_ok!( - session - .rpc() - .factory() - .get_run_detail(FactoryGetRunRequest::default()) - ); - rpc_ok!( - session - .rpc() - .factory() - .get_run_progress(FactoryGetRunProgressRequest::default()) - ); - rpc_ok!( - session - .rpc() - .factory() - .cancel(FactoryCancelRequest::default()) - ); - rpc_ok!( - session - .rpc() - .factory() - .pause(FactoryPauseRequest::default()) - ); - rpc_ok!(session.rpc().factory().log(FactoryLogRequest::default())); - rpc_ok!( - session - .rpc() - .factory() - .agent(FactoryAgentRequest::default()) - ); - rpc_ok!( - session - .rpc() - .factory() - .journal() - .get(FactoryJournalGetRequest::default()) - ); - rpc_ok!( - session - .rpc() - .factory() - .journal() - .put(FactoryJournalPutRequest::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.factory.run", - "session.factory.resume", - "session.factory.getRun", - "session.factory.listRuns", - "session.factory.getRunDetail", - "session.factory.getRunProgress", - "session.factory.cancel", - "session.factory.pause", - "session.factory.log", - "session.factory.agent", - "session.factory.journal.get", - "session.factory.journal.put", - "session.fleet.start", - "session.history.compact", - "session.history.compact", - "session.history.clearContext", - "session.limitPrediction.predict", - "session.limitPrediction.predict", - ]); - fake.assert_params( - "session.factory.run", - 0, - json!({ - "sessionId": "rpc-surface-session", - "args": {"mode": "offline", "count": 2}, - "name": "coverage-factory" - }), - ); - 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 48145970ca..0000000000 --- a/rust/tests/e2e/rpc_workspace_checkpoints.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::path::Path; -use std::process::Command; - -use github_copilot_sdk::rpc::{ - WorkspaceDiffFileChangeType, WorkspaceDiffMode, WorkspacesDiffRequest, - WorkspacesReadCheckpointRequest, WorkspacesReadFileRequest, WorkspacesSaveLargePasteRequest, -}; - -#[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('\\', "/") -} - -fn init_git_repository(path: &Path) { - let status = Command::new("git") - .arg("init") - .arg("--quiet") - .current_dir(path) - .status() - .expect("run git init"); - assert!(status.success(), "git init should succeed"); -} -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 new file mode 100644 index 0000000000..92e4ff26b2 --- /dev/null +++ b/rust/tests/e2e/runtime_host_callbacks.rs @@ -0,0 +1,564 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use async_trait::async_trait; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::hooks::{HookContext, PreToolUseInput, PreToolUseOutput, SessionHooks}; +use github_copilot_sdk::session::Session; +use github_copilot_sdk::tool::ToolHandler; +use github_copilot_sdk::{ + AhpSessionRequest, AhpSessionResumeRequest, Error, SystemMessageConfig, Tool, ToolInvocation, + ToolResult, +}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; + +use super::*; + +const TOOL_SNAPSHOT: &str = "both_clients_see_tool_request_and_completion_events"; +const TOOL_PROMPT: &str = "Use the magic_number tool with seed 'hello' and tell me the result"; +const MARKER: &str = "RUST_APPLICATION_OWNED_AHP_PROMPT"; + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn callback_resume_composes_tools_after_application_session_runtime_restart() { + run_snapshot("runtime_host", "app_resume_callback_composes_tools_after_history", |ctx| { + Box::pin(async move { + let first_owner = start(ctx).await; + let old_pid = first_owner.pid().unwrap(); + let first_callbacks = Arc::new(AppCallbacks::default()); + let (options, mut created, mut first_released) = options_with_app(first_callbacks.clone()); + let first = first_owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&first).await; + // Restore the persisted client's tools, not dynamically register a new tool on resume. + let client_id = ahp.client_id.clone(); + let client_tools = json!([{ + "name":"client_echo","description":"Echoes text from the AHP client", + "inputSchema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]} + }]); + let (uri, chat, subscription) = create_with_tools(&ahp, ctx, client_tools.clone()).await; + let original = deadline(created.recv()).await.unwrap(); + assert!(created.try_recv().is_err()); + let session_id = original.id(); + assert_eq!(session_id.as_str(), uri.strip_prefix("ahp-session:/").unwrap()); + topology(&first, &first_owner, &home(ctx).join("ahp/sessions")); + turn(&ahp, &chat, subscription).await; + assert_eq!(first_callbacks.tools.load(Ordering::SeqCst), 0); + first.dispose().await.unwrap(); + stopped(&first, &ahp, &first_owner).await; + assert!(Arc::ptr_eq(&original, &deadline(first_released.recv()).await.unwrap())); + assert!(!original.get_events().await.unwrap().is_empty()); + original.disconnect().await.unwrap(); + ahp.client.shutdown().await; + first_owner.stop().await.unwrap(); + reaped(old_pid).await; + + let owner = start(ctx).await; + let callbacks = Arc::new(AppCallbacks { + expected_session_id: Some(session_id.clone()), + ..Default::default() + }); + let app = callbacks.clone(); + let resume_calls = Arc::new(AtomicUsize::new(0)); + let resumes = resume_calls.clone(); + let create_calls = Arc::new(AtomicUsize::new(0)); + let creates = create_calls.clone(); + let (resumed_tx, mut resumed) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let expected_id = session_id.clone(); + let expected_directory = ctx.work_dir().canonicalize().unwrap(); + 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()) } + }) + .with_resume_session(move |request: AhpSessionResumeRequest, client: Client| { + let app = app.clone(); + let resumed_tx = resumed_tx.clone(); + assert_eq!(request.config.session_id, expected_id); + assert_eq!(request.config.continue_pending_work, Some(false)); + assert_eq!(request.config.working_directory.as_ref().unwrap().canonicalize().unwrap(), expected_directory); + assert!(!request.cancellation_token.is_cancelled()); + resumes.fetch_add(1, Ordering::SeqCst); + async move { + let tool = Tool::new("magic_number") + .with_description("Returns a magic number") + .with_parameters(json!({ + "type":"object", + "properties":{"seed":{"type":"string","description":"A seed value"}}, + "required":["seed"] + })) + .with_handler(app.clone()); + let original = Arc::new(client.resume_session( + request.config + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_system_message(SystemMessageConfig::new().with_mode("append").with_content(MARKER)) + .with_tools(vec![tool]).with_hooks(app) + ).await?); + resumed_tx.send(original.clone()).unwrap(); + Ok(original) + } + }) + .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.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(); + assert_eq!(restored.id(), session_id); + assert!(!Arc::ptr_eq(&original, &restored)); + let (session, _) = ahp.client.subscribe(uri.clone()).await.unwrap(); + let session = serde_json::to_value(session).unwrap(); + let chat_uri = session["snapshot"]["state"]["defaultChat"].as_str().unwrap(); + let (history, subscription) = ahp.client.subscribe(chat_uri.into()).await.unwrap(); + let history = serde_json::to_value(history).unwrap(); + let turns = history["snapshot"]["state"]["turns"].as_array().unwrap(); + assert_eq!(turns.len(), 1); + assert_eq!(turns[0]["message"]["text"], PROMPT); + ahp.client.dispatch(uri.clone(), serde_json::from_value(json!({ + "type":"session/activeClientSet", + "activeClient":{"clientId":ahp.client_id, "displayName":"Resumed tool owner", + "tools":client_tools} + })).unwrap()).await.unwrap(); + deadline(async { + loop { + let (session, _) = ahp.client.subscribe(uri.clone()).await.unwrap(); + let state = serde_json::to_value(session).unwrap(); + if state["snapshot"]["state"]["activeClients"].as_array().unwrap().iter() + .any(|entry| entry["clientId"] == ahp.client_id) { break; } + tokio::task::yield_now().await; + } + }).await; + let client_calls = AtomicUsize::new(0); + let client_tool = |name: &str, input: Value| { + assert_eq!(name, "client_echo"); + assert_eq!(input, json!({"text":"ping"})); + client_calls.fetch_add(1, Ordering::SeqCst); + "CLIENT_ECHO_ping".to_owned() + }; + turn_with_client_tools(&ahp, chat_uri, subscription, + "Call magic_number with seed 'hello' and client_echo with text 'ping', then report both results", + "Both tools ran: MAGIC_hello_42 and CLIENT_ECHO_ping", Some(&client_tool)).await; + assert_eq!(callbacks.tools.load(Ordering::SeqCst), 1); + assert_eq!(first_callbacks.tools.load(Ordering::SeqCst), 0); + assert!(callbacks.hooks.load(Ordering::SeqCst) > 0); + assert_eq!(client_calls.load(Ordering::SeqCst), 1); + let exchanges = ctx.exchanges(); + let composed: Vec<_> = exchanges.iter().filter(|exchange| + exchange["request"]["messages"].to_string().contains("Call magic_number")).collect(); + assert_eq!(composed.len(), 2); + for exchange in &composed { + let request = &exchange["request"]; + let tools = request["tools"].as_array().unwrap(); + for name in ["magic_number", "client_echo"] { + assert_eq!(tools.iter().filter(|tool| tool["function"]["name"] == name).count(), 1); + } + let messages = request["messages"].to_string(); + assert!(messages.contains(MARKER)); + assert!(messages.contains(PROMPT)); + } + let mut results: Vec<_> = composed.last().unwrap()["request"]["messages"].as_array().unwrap() + .iter().filter(|message| message["role"] == "tool") + .map(|message| message["content"].as_str().unwrap()).collect(); + results.sort(); + assert_eq!(results, ["CLIENT_ECHO_ping", "MAGIC_hello_42"]); + ahp.client.subscribe(uri.clone()).await.unwrap(); + assert_eq!(resume_calls.load(Ordering::SeqCst), 1); + assert_eq!(create_calls.load(Ordering::SeqCst), 0); + assert!(released.try_recv().is_err()); + host.dispose().await.unwrap(); + stopped(&host, &ahp, &owner).await; + assert!(Arc::ptr_eq(&restored, &deadline(released.recv()).await.unwrap())); + assert!(!restored.get_events().await.unwrap().is_empty()); + host.dispose().await.unwrap(); + assert!(released.try_recv().is_err()); + ahp.client.shutdown().await; + restored.disconnect().await.unwrap(); + owner.stop().await.unwrap(); + }) + }).await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn callback_owner_drop_reclaims_original_closes_listener_and_reaps_runtime() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let runtime_pid = owner.pid().unwrap(); + let (options, mut created, _) = options_with_app(Arc::default()); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let host = owner + .start_ahp_host(options.with_on_session_released(move |session| { + released_tx.send(Arc::downgrade(&session)).unwrap(); + })) + .await + .unwrap(); + let ahp = connect(&host).await; + create(&ahp, ctx).await; + let original = deadline(created.recv()).await.unwrap(); + let session = Arc::downgrade(&original); + drop(original); + drop(created); + drop(owner); + deadline(released.recv()).await.unwrap(); + deadline(async { + while session.upgrade().is_some() { + tokio::task::yield_now().await; + } + }) + .await; + listener_closed(&host, &ahp).await; + reaped(runtime_pid).await; + assert!(released.recv().await.is_none()); + ahp.client.shutdown().await; + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn callback_pending_disposal_releases_without_transport_drain_timeout() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let (ready_tx, mut ready) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let finish = CancellationToken::new(); + let gate = finish.clone(); + let options = local_options() + .with_create_session(move |request: AhpSessionRequest, client: Client| { + let ready_tx = ready_tx.clone(); + let gate = gate.clone(); + async move { + let original = Arc::new(client.create_session(request.config).await?); + ready_tx + .send((original.clone(), request.cancellation_token)) + .unwrap(); + gate.cancelled().await; + Ok(original) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + let host = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + authenticate(&ahp).await; + let params = + create_params(&ahp, ctx, &format!("ahp-session:/{}", uuid::Uuid::new_v4())); + let creation = ahp.client.request::<_, Value>("createSession", params); + let check = async { + let (original, cancellation) = deadline(ready.recv()).await.unwrap(); + host.dispose().await.unwrap(); + deadline(cancellation.cancelled()).await; + finish.cancel(); + assert!(Arc::ptr_eq( + &original, + &deadline(released.recv()).await.unwrap() + )); + original.get_events().await.unwrap(); + original.disconnect().await.unwrap(); + }; + let (result, ()) = tokio::join!(creation, check); + assert!(result.is_err()); + stopped(&host, &ahp, &owner).await; + let disposal = host.dispose().await; + assert!(released.try_recv().is_err()); + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + disposal.expect("listener disposal must cancel pending handoff before SDK drain"); + }) + }) + .await; +} + +#[derive(Default)] +struct AppCallbacks { + tools: AtomicUsize, + hooks: AtomicUsize, + expected_session_id: Option, +} + +#[async_trait] +impl ToolHandler for AppCallbacks { + async fn call(&self, invocation: ToolInvocation) -> Result { + assert_eq!(invocation.tool_name, "magic_number"); + if let Some(session_id) = &self.expected_session_id { + assert_eq!(&invocation.session_id, session_id); + } + assert_eq!(invocation.arguments["seed"], "hello"); + self.tools.fetch_add(1, Ordering::SeqCst); + Ok(ToolResult::Text("MAGIC_hello_42".into())) + } +} + +#[async_trait] +impl SessionHooks for AppCallbacks { + async fn on_pre_tool_use( + &self, + _input: PreToolUseInput, + _ctx: HookContext, + ) -> Option { + self.hooks.fetch_add(1, Ordering::SeqCst); + None + } +} + +fn options_with_app( + callbacks: Arc, +) -> ( + AhpHostOptions, + mpsc::UnboundedReceiver>, + mpsc::UnboundedReceiver>, +) { + let (created_tx, created) = mpsc::unbounded_channel(); + let (released_tx, released) = mpsc::unbounded_channel(); + let options = local_options() + .with_create_session(move |request: AhpSessionRequest, client: Client| { + let callbacks = callbacks.clone(); + let created_tx = created_tx.clone(); + async move { + assert!(!request.cancellation_token.is_cancelled()); + let tool = Tool::new("magic_number") + .with_description("Returns a magic number") + .with_parameters(json!({ + "type": "object", + "properties": {"seed": {"type": "string", "description": "A seed value"}}, + "required": ["seed"] + })) + .with_handler(callbacks.clone()); + let original = Arc::new( + client + .create_session( + request + .config + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_system_message( + SystemMessageConfig::new() + .with_mode("append") + .with_content(MARKER), + ) + .with_tools(vec![tool]) + .with_hooks(callbacks), + ) + .await?, + ); + created_tx.send(original.clone()).unwrap(); + Ok(original) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + (options, created, released) +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn callback_handoff_preserves_tools_hooks_prompt_and_original_arc() { + run_snapshot("multi_client", TOOL_SNAPSHOT, |ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let callbacks = Arc::new(AppCallbacks::default()); + let (options, mut created, mut released) = options_with_app(callbacks.clone()); + let host = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + let (uri, chat, subscription) = create(&ahp, ctx).await; + let original = deadline(created.recv()).await.unwrap(); + assert_eq!( + original.id().as_str(), + uri.strip_prefix("ahp-session:/").unwrap() + ); + topology(&host, &owner, &home(ctx).join("ahp/sessions")); + turn_with_prompt(&ahp, &chat, subscription, TOOL_PROMPT, "MAGIC_hello_42").await; + assert_eq!(callbacks.tools.load(Ordering::SeqCst), 1); + assert!(callbacks.hooks.load(Ordering::SeqCst) > 0); + let exchanges = ctx.exchanges(); + assert!(serde_json::to_string(&exchanges).unwrap().contains(MARKER)); + assert!(released.try_recv().is_err()); + host.dispose().await.unwrap(); + stopped(&host, &ahp, &owner).await; + assert!(Arc::ptr_eq( + &original, + &deadline(released.recv()).await.unwrap() + )); + assert!(!original.get_events().await.unwrap().is_empty()); + host.dispose().await.unwrap(); + assert!(released.try_recv().is_err()); + ahp.client.shutdown().await; + original.disconnect().await.unwrap(); + owner.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn callback_attach_failure_releases_original_without_losing_app_tool() { + run_snapshot("multi_client", TOOL_SNAPSHOT, |ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let callbacks = Arc::new(AppCallbacks::default()); + let (options, mut created, mut released) = options_with_app(callbacks.clone()); + let host = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + authenticate(&ahp).await; + let uri = format!("ahp-session:/{}", uuid::Uuid::new_v4()); + let mut params = create_params(&ahp, ctx, &uri); + params["activeClient"]["tools"] = json!([{ + "name":"magic_number", "description":"clashing participant", + "inputSchema":{"type":"object","properties":{}} + }]); + let error = ahp + .client + .request::<_, Value>("createSession", params) + .await + .unwrap_err(); + assert!(error.to_string().contains("attach failed"), "{error}"); + let original = deadline(created.recv()).await.unwrap(); + assert!(Arc::ptr_eq( + &original, + &deadline(released.recv()).await.unwrap() + )); + let response = original.send_and_wait(TOOL_PROMPT).await.unwrap().unwrap(); + assert!( + response.data["content"] + .as_str() + .unwrap() + .contains("MAGIC_hello_42") + ); + assert_eq!(callbacks.tools.load(Ordering::SeqCst), 1); + host.dispose().await.unwrap(); + assert!(released.try_recv().is_err()); + ahp.client.shutdown().await; + original.disconnect().await.unwrap(); + owner.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn callback_disposal_and_owner_termination_release_once() { + run(|ctx| { + Box::pin(async move { + for owner_loss in [false, true] { + let port = unused_port(); + let runtime = Client::start(options(ctx).with_transport(Transport::Tcp { + port, + connection_token: Some(RUNTIME_TOKEN.into()), + })) + .await + .unwrap(); + let owner = Client::start(options(ctx).with_transport(Transport::External { + host: "127.0.0.1".into(), + port, + connection_token: Some(RUNTIME_TOKEN.into()), + })) + .await + .unwrap(); + let (options, mut created, mut released) = options_with_app(Arc::default()); + let host = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + create(&ahp, ctx).await; + let original = deadline(created.recv()).await.unwrap(); + if owner_loss { + owner.force_stop(); + } else { + host.dispose().await.unwrap(); + } + assert!(Arc::ptr_eq( + &original, + &deadline(released.recv()).await.unwrap() + )); + if owner_loss { + stopped_after_owner_disconnect(&host, &ahp, &runtime).await; + } else { + stopped(&host, &ahp, &runtime).await; + } + if !owner_loss { + original.get_events().await.unwrap(); + original.disconnect().await.unwrap(); + host.dispose().await.unwrap(); + owner.stop().await.unwrap(); + } + assert!(released.try_recv().is_err()); + ahp.client.shutdown().await; + runtime.stop().await.unwrap(); + } + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn callback_cancellation_releases_late_session_after_host_disposal() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let (ready_tx, mut ready) = mpsc::unbounded_channel(); + let (released_tx, mut released) = mpsc::unbounded_channel(); + let finish = CancellationToken::new(); + let gate = finish.clone(); + let options = local_options() + .with_create_session(move |request: AhpSessionRequest, client: Client| { + let ready_tx = ready_tx.clone(); + let gate = gate.clone(); + async move { + let session = Arc::new(client.create_session(request.config).await?); + ready_tx + .send((session.clone(), request.cancellation_token)) + .unwrap(); + gate.cancelled().await; + Ok(session) + } + }) + .with_on_session_released(move |session| { + released_tx.send(session).unwrap(); + }); + let host = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + authenticate(&ahp).await; + let params = + create_params(&ahp, ctx, &format!("ahp-session:/{}", uuid::Uuid::new_v4())); + let creation = ahp.client.request::<_, Value>("createSession", params); + let check = async { + let (original, cancellation) = deadline(ready.recv()).await.unwrap(); + let disposal = host.dispose(); + let finish_callback = async { + deadline(cancellation.cancelled()).await; + assert!(released.try_recv().is_err()); + finish.cancel(); + assert!(Arc::ptr_eq( + &original, + &deadline(released.recv()).await.unwrap() + )); + original.get_events().await.unwrap(); + original.disconnect().await.unwrap(); + }; + let (result, ()) = tokio::join!(disposal, finish_callback); + result.unwrap(); + }; + let (result, ()) = tokio::join!(creation, check); + assert!(result.is_err()); + stopped(&host, &ahp, &owner).await; + host.dispose().await.unwrap(); + assert!(released.try_recv().is_err()); + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + }) + }) + .await; +} diff --git a/rust/tests/e2e/runtime_host_candidate.mts b/rust/tests/e2e/runtime_host_candidate.mts new file mode 100644 index 0000000000..67992f9890 --- /dev/null +++ b/rust/tests/e2e/runtime_host_candidate.mts @@ -0,0 +1,4 @@ +// Reuse the Node E2E's candidate attestation and real runtime materializer. +import { candidateHostArtifacts } from "../../../nodejs/test/e2e/harness/runtimeHostCandidate.js"; + +console.log(JSON.stringify(candidateHostArtifacts(process.argv[2]))); diff --git a/rust/tests/e2e/runtime_host_support.rs b/rust/tests/e2e/runtime_host_support.rs new file mode 100644 index 0000000000..e38ed6ff70 --- /dev/null +++ b/rust/tests/e2e/runtime_host_support.rs @@ -0,0 +1,601 @@ +use std::collections::BTreeMap; +use std::future::Future; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::{Arc, LazyLock, Mutex}; +use std::time::Duration; + +use ahp::{Client as AhpClient, ClientConfig, SessionSubscription, SubscriptionEvent}; +use ahp_types::version::PROTOCOL_VERSION; +use ahp_ws::WebSocketTransport; +use futures_util::FutureExt; +use github_copilot_sdk::{ + AhpHost, AhpHostExit, AhpHostOptions, CliProgram, Client, ClientOptions, Transport, +}; +use serde::Deserialize; +use serde_json::{Value, json}; + +use super::support::{DEFAULT_TEST_TOKEN, E2eContext}; + +pub const PROMPT: &str = "What is 2+2?"; +pub const SNAPSHOT: &str = + "sendandwait_blocks_until_session_idle_and_returns_final_assistant_message"; +pub const ROOT: &str = "ahp-root://"; +pub const RUNTIME_TOKEN: &str = "rust-runtime-host-e2e-owner"; +const DEADLINE: Duration = Duration::from_secs(30); + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Artifacts { + runtime_path: PathBuf, + provider_path: PathBuf, + bundled: bool, +} + +static ARTIFACTS: LazyLock = LazyLock::new(|| { + if let Ok(manifest) = std::env::var("COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST") { + let sdk = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); + let output = Command::new(sdk.join("nodejs/node_modules/.bin/tsx")) + .arg(sdk.join("rust/tests/e2e/runtime_host_candidate.mts")) + .arg(manifest) + .output() + .expect("run existing Node candidate materializer"); + assert!( + output.status.success(), + "candidate materializer: {}", + String::from_utf8_lossy(&output.stderr) + ); + return serde_json::from_slice(&output.stdout).expect("candidate artifact metadata"); + } + fn artifact(name: &str) -> PathBuf { + let path = PathBuf::from( + std::env::var(name).unwrap_or_else(|_| panic!("set {name} to a local build")), + ); + assert!(path.is_absolute(), "{name} must be absolute"); + let path = path.canonicalize().expect("local build exists"); + assert!( + !path.to_string_lossy().contains("/node_modules/"), + "no released runtime packages" + ); + assert!(path.is_file()); + path + } + Artifacts { + runtime_path: artifact("COPILOT_CLI_PATH"), + provider_path: artifact("COPILOT_RUNTIME_PROVIDER_LIB"), + bundled: false, + } +}); + +pub async fn run(test: F) +where + F: for<'a> FnOnce(&'a E2eContext) -> std::pin::Pin + 'a>>, +{ + run_snapshot("session", SNAPSHOT, test).await; +} + +pub async fn run_snapshot(group: &str, snapshot: &str, test: F) +where + F: for<'a> FnOnce(&'a E2eContext) -> std::pin::Pin + 'a>>, +{ + assert_eq!( + std::env::var("COPILOT_RUNTIME_HOST_E2E").as_deref(), + Ok("1") + ); + assert_eq!( + std::env::var("GITHUB_ACTIONS").as_deref(), + Ok("true"), + "canonical snapshot must be read-only" + ); + let work = PathBuf::from( + std::env::var_os("TMPDIR").expect("set TMPDIR to a workspace-local scratch directory"), + ); + assert!(work.is_absolute() && !work.starts_with("/tmp") && !work.starts_with("/var/tmp")); + let mut ctx = E2eContext::new_with_cli(group, snapshot, Some(ARTIFACTS.runtime_path.clone())) + .await + .unwrap(); + let outcome = + std::panic::AssertUnwindSafe(tokio::time::timeout(Duration::from_secs(180), test(&ctx))) + .catch_unwind() + .await; + ctx.cleanup(true) + .await + .expect("stop existing replay proxy without recording"); + match outcome { + Ok(result) => result.expect("runtime host E2E deadline"), + Err(panic) => std::panic::resume_unwind(panic), + } +} + +pub fn options(ctx: &E2eContext) -> ClientOptions { + let mut options = ctx + .client_options() + .with_program(CliProgram::Path(ARTIFACTS.runtime_path.clone())) + .with_transport(Transport::Stdio); + if ARTIFACTS.bundled { + options.env.retain(|(name, _)| { + !["COPILOT_CLI_PATH", "COPILOT_RUNTIME_PROVIDER_LIB"] + .iter() + .any(|key| name == key) + }); + options = options.with_env_remove(["COPILOT_CLI_PATH", "COPILOT_RUNTIME_PROVIDER_LIB"]); + } else { + options.env.extend([( + "COPILOT_RUNTIME_PROVIDER_LIB".into(), + ARTIFACTS.provider_path.as_os_str().to_owned(), + )]); + } + options +} + +pub async fn start(ctx: &E2eContext) -> Client { + Client::start(options(ctx)) + .await + .expect("start local runtime") +} + +pub fn options_with_base_directory(ctx: &E2eContext, base: &Path) -> ClientOptions { + let mut options = options(ctx).with_base_directory(base); + // The shared harness sets COPILOT_HOME explicitly; Rust's explicit env + // overrides typed defaults. Remove that conflict to exercise base_directory. + options.env.retain(|(name, _)| name != "COPILOT_HOME"); + options +} + +pub fn home(ctx: &E2eContext) -> PathBuf { + ctx.client_options() + .env + .iter() + .find(|(key, _)| key == "COPILOT_HOME") + .map(|(_, value)| PathBuf::from(value)) + .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 = local_options().with_on_exit(move |exit| { + seen.lock().unwrap().push(exit); + }); + (options, exits) +} + +pub async fn exit(exits: &Arc>>) -> AhpHostExit { + deadline(async { + loop { + if let Some(exit) = exits.lock().unwrap().first().cloned() { + return exit; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await +} + +/// 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 { + pub client: AhpClient, + pub client_id: String, +} + +pub async fn connect(host: &AhpHost) -> Ahp { + 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 { + connect_url_as(url, token, uuid::Uuid::new_v4().to_string()).await +} + +pub async fn connect_url_as(url: &str, token: Option<&str>, client_id: String) -> Ahp { + let mut url = reqwest::Url::parse(url).unwrap(); + if let Some(token) = token { + url.query_pairs_mut().append_pair("tkn", token); + } + let transport = deadline(WebSocketTransport::connect(url.as_str())) + .await + .expect("AHP websocket"); + let client = AhpClient::connect( + transport, + ClientConfig { + default_request_timeout: Some(Duration::from_secs(15)), + ..Default::default() + }, + ) + .await + .unwrap(); + let initialized = client + .initialize(client_id.clone(), vec![PROTOCOL_VERSION.into()], vec![]) + .await + .unwrap(); + assert_eq!(initialized.protocol_version, "0.9.0"); + Ahp { client, client_id } +} + +pub async fn authenticate(ahp: &Ahp) { + let (root, _) = ahp.client.subscribe(ROOT.into()).await.unwrap(); + let root = serde_json::to_value(root).unwrap(); + let resource = root["snapshot"]["state"]["agents"] + .as_array() + .unwrap() + .iter() + .find(|agent| agent["provider"] == "copilot") + .unwrap()["protectedResources"] + .as_array() + .unwrap() + .iter() + .find(|resource| resource["resource_name"] == "GitHub API") + .unwrap()["resource"] + .clone(); + let _: Value = ahp + .client + .request( + "authenticate", + json!({ + "channel": ROOT, "resource": resource, "token": DEFAULT_TEST_TOKEN, + }), + ) + .await + .expect("authenticate GitHub AHP resource"); +} + +pub fn create_params(ahp: &Ahp, ctx: &E2eContext, uri: &str) -> Value { + json!({ + "channel": uri, + "provider": "copilot", + "workingDirectories": [reqwest::Url::from_directory_path(ctx.work_dir()).unwrap().as_str()], + "activeClient": {"clientId": ahp.client_id, "displayName": "Rust runtime host E2E", "tools": []}, + }) +} + +pub async fn create(ahp: &Ahp, ctx: &E2eContext) -> (String, String, SessionSubscription) { + create_with_tools(ahp, ctx, json!([])).await +} + +pub async fn create_with_tools( + ahp: &Ahp, + ctx: &E2eContext, + tools: Value, +) -> (String, String, SessionSubscription) { + authenticate(ahp).await; + let uri = format!("ahp-session:/{}", uuid::Uuid::new_v4()); + let mut params = create_params(ahp, ctx, &uri); + params["activeClient"]["tools"] = tools; + let _: Value = ahp + .client + .request("createSession", params) + .await + .expect("create AHP session"); + let (session, _) = ahp.client.subscribe(uri.clone()).await.unwrap(); + let session = serde_json::to_value(session).unwrap(); + let chat = session["snapshot"]["state"]["defaultChat"] + .as_str() + .expect("default AHP chat") + .to_string(); + let (_, subscription) = ahp.client.subscribe(chat.clone()).await.unwrap(); + (uri, chat, subscription) +} + +pub async fn turn(ahp: &Ahp, chat: &str, subscription: SessionSubscription) { + turn_with_prompt(ahp, chat, subscription, PROMPT, "4").await; +} + +pub async fn turn_with_prompt( + ahp: &Ahp, + chat: &str, + subscription: SessionSubscription, + prompt: &str, + expected: &str, +) { + turn_with_client_tools(ahp, chat, subscription, prompt, expected, None).await; +} + +type ClientToolHandler<'a> = &'a dyn Fn(&str, Value) -> String; + +pub async fn turn_with_client_tools( + ahp: &Ahp, + chat: &str, + mut subscription: SessionSubscription, + prompt: &str, + expected: &str, + on_client_tool: Option>, +) { + let id = uuid::Uuid::new_v4().to_string(); + let action = serde_json::from_value(json!({ + "type": "chat/turnStarted", "turnId": id, "startedAt": "2026-09-21T00:00:00.000Z", + "message": {"text": prompt, "origin": {"kind": "user"}, "model": {"id": "claude-sonnet-5"}} + })) + .unwrap(); + ahp.client.dispatch(chat.into(), action).await.unwrap(); + deadline(async { + let mut parts = BTreeMap::::new(); + let mut client_calls = BTreeMap::::new(); + let mut completed = std::collections::BTreeSet::new(); + let mut deltas = 0; + while let Some(event) = subscription.recv().await { + let SubscriptionEvent::Action(envelope) = event else { + continue; + }; + let envelope = serde_json::to_value(envelope).unwrap(); + assert!(envelope["rejectionReason"].is_null(), "{envelope}"); + let action = &envelope["action"]; + if action["turnId"] != id { + continue; + } + match action["type"].as_str().unwrap_or_default() { + "chat/toolCallStart" + if action["contributor"]["kind"] == "client" && on_client_tool.is_some() => + { + assert_eq!(action["contributor"]["clientId"], ahp.client_id); + let call_id = action["toolCallId"].as_str().unwrap().to_owned(); + assert!(!completed.contains(&call_id)); + assert!( + client_calls + .insert(call_id, action["toolName"].as_str().unwrap().into()) + .is_none() + ); + } + "chat/toolCallReady" + if client_calls.contains_key(action["toolCallId"].as_str().unwrap()) => + { + let call_id = action["toolCallId"].as_str().unwrap(); + let name = client_calls.remove(call_id).unwrap(); + assert!(completed.insert(call_id.to_owned())); + assert_eq!(action["confirmed"], "not-needed"); + let input = + serde_json::from_str(action["toolInput"].as_str().unwrap()).unwrap(); + let text = on_client_tool.unwrap()(&name, input); + ahp.client.dispatch(chat.into(), serde_json::from_value(json!({ + "type": "chat/toolCallComplete", "turnId": id, "toolCallId": call_id, + "result": {"success": true, "pastTenseMessage": format!("Ran {name}"), + "content": [{"type": "text", "text": text}]} + })).unwrap()).await.unwrap(); + } + "chat/responsePart" if action["part"]["kind"] == "markdown" => { + parts.insert( + action["part"]["id"].as_str().unwrap().into(), + action["part"]["content"].as_str().unwrap().into(), + ); + } + "chat/delta" => { + deltas += 1; + parts + .entry(action["partId"].as_str().unwrap().into()) + .or_default() + .push_str(action["content"].as_str().unwrap()); + } + "chat/error" => panic!("AHP turn error: {action}"), + "chat/turnComplete" => { + assert!( + client_calls.is_empty(), + "all client tool calls must complete" + ); + assert!(deltas > 0, "must stream real deltas"); + assert!( + parts + .values() + .cloned() + .collect::() + .contains(expected) + ); + return; + } + _ => {} + } + } + panic!("subscription ended before turn completion"); + }) + .await; +} + +pub async fn resume(ahp: &Ahp, uri: &str, excluded_sdk_id: Option<&str>) { + authenticate(ahp).await; + let listed: Value = ahp + .client + .request("listSessions", json!({"channel": ROOT})) + .await + .unwrap(); + let resources: Vec<_> = listed["items"] + .as_array() + .unwrap() + .iter() + .map(|item| item["resource"].as_str().unwrap()) + .collect(); + assert!(resources.contains(&uri)); + if let Some(id) = excluded_sdk_id { + assert!(!resources.contains(&format!("ahp-session:/{id}").as_str())); + } + let (session, _) = ahp + .client + .subscribe(uri.into()) + .await + .expect("resume durable AHP session"); + let session = serde_json::to_value(session).unwrap(); + assert_eq!(session["snapshot"]["state"]["lifecycle"], "ready"); + let chat_uri = session["snapshot"]["state"]["defaultChat"] + .as_str() + .unwrap(); + let (chat, _) = ahp.client.subscribe(chat_uri.into()).await.unwrap(); + let chat = serde_json::to_value(chat).unwrap(); + let turn = chat["snapshot"]["state"]["turns"] + .as_array() + .unwrap() + .iter() + .find(|turn| turn["message"]["text"] == PROMPT) + .unwrap(); + assert_eq!(turn["state"], "complete"); + assert!( + turn["responseParts"] + .as_array() + .unwrap() + .iter() + .filter(|part| part["kind"] == "markdown") + .map(|part| part["content"].as_str().unwrap()) + .collect::() + .contains('4') + ); +} + +pub fn topology(host: &AhpHost, owner: &Client, catalog: &Path) { + let runtime = owner.pid().expect("owned out-of-process runtime"); + assert!(catalog.is_dir(), "host must create the resolved catalog"); + let command = std::fs::read_to_string(format!("/proc/{runtime}/cmdline")).unwrap(); + let args: Vec<_> = command.split('\0').collect(); + assert!(!command.contains("copilotd"), "no companion host process"); + if let Some(token) = &host.token { + assert!( + !args.iter().any(|argument| argument.contains(token)), + "listener tokens must not enter runtime argv" + ); + } + assert_eq!( + std::fs::read_link(format!("/proc/{runtime}/exe")).unwrap(), + ARTIFACTS.runtime_path + ); + let maps = std::fs::read_to_string(format!("/proc/{runtime}/maps")).unwrap(); + assert!( + maps.contains(ARTIFACTS.provider_path.to_str().unwrap()), + "runtime must load selected provider" + ); + let mut descendants = vec![runtime]; + while let Some(parent) = descendants.pop() { + let mut children = std::collections::BTreeSet::new(); + let tasks = match std::fs::read_dir(format!("/proc/{parent}/task")) { + Ok(tasks) => tasks, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => panic!("read descendant tasks: {error}"), + }; + for task in tasks { + let task = task.unwrap(); + match std::fs::read_to_string(task.path().join("children")) { + Ok(list) => children.extend(list.split_whitespace().map(str::to_owned)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => panic!("read runtime children: {error}"), + } + } + for child in children { + let inspect = || -> std::io::Result<_> { + Ok(( + std::fs::read_to_string(format!("/proc/{child}/cmdline"))?, + std::fs::read_link(format!("/proc/{child}/exe"))?, + std::fs::read_to_string(format!("/proc/{child}/maps"))?, + )) + }; + let (command, executable, maps) = match inspect() { + Ok(details) => details, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => panic!("inspect runtime descendant: {error}"), + }; + assert!( + !command.contains("copilotd") && !command.contains("copilot-runtime"), + "no companion host or second runtime" + ); + assert_ne!(executable, ARTIFACTS.runtime_path); + assert!(!maps.contains(ARTIFACTS.provider_path.to_str().unwrap())); + descendants.push(child.parse::().unwrap()); + } + } + let sockets: std::collections::BTreeSet<_> = std::fs::read_dir(format!("/proc/{runtime}/fd")) + .unwrap() + .filter_map(|entry| match std::fs::read_link(entry.unwrap().path()) { + Ok(target) => Some(target.to_string_lossy().into_owned()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => panic!("read runtime socket: {error}"), + }) + .collect(); + 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}")) + .unwrap() + .lines() + .skip(1) + .any(|line| { + let fields: Vec<_> = line.split_whitespace().collect(); + fields.len() > 9 + && fields[3] == "0A" + && fields[1] + .rsplit(':') + .next() + .and_then(|value| u16::from_str_radix(value, 16).ok()) + == Some(port) + && sockets.contains(&format!("socket:[{}]", fields[9])) + }) + }), + "runtime must own the listening TCP socket" + ); + if ARTIFACTS.bundled { + let env = std::fs::read_to_string(format!("/proc/{runtime}/environ")).unwrap(); + assert!( + !env.split('\0') + .any(|entry| entry.starts_with("COPILOT_RUNTIME_PROVIDER_LIB=")), + "candidate must use adjacent assets" + ); + } +} + +pub async fn reaped(pid: u32) { + deadline(async { + while Path::new(&format!("/proc/{pid}")).exists() { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await; +} + +pub async fn stopped(host: &AhpHost, ahp: &Ahp, runtime: &Client) { + listener_closed(host, ahp).await; + assert!( + Path::new(&format!("/proc/{}", runtime.pid().expect("owned runtime"))).exists(), + "owning runtime must survive" + ); +} + +pub async fn stopped_after_owner_disconnect(host: &AhpHost, ahp: &Ahp, runtime: &Client) { + // Local owner-loss callbacks do not acknowledge the runtime's asynchronous cleanup. + tokio::time::timeout(DEADLINE, async { + while ahp.client.ping().await.is_ok() { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("existing AHP client must disconnect after owner loss"); + stopped(host, ahp, runtime).await; +} + +pub async fn listener_closed(host: &AhpHost, ahp: &Ahp) { + assert!( + deadline(ahp.client.ping()).await.is_err(), + "existing AHP client must disconnect" + ); + 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(), + ))) + .await; + assert_eq!( + result.unwrap_err().kind(), + std::io::ErrorKind::ConnectionRefused + ); +} diff --git a/rust/tests/e2e/session_fs.rs b/rust/tests/e2e/session_fs.rs index 8b13789179..479a07d935 100644 --- a/rust/tests/e2e/session_fs.rs +++ b/rust/tests/e2e/session_fs.rs @@ -1 +1,324 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use base64::Engine; +use github_copilot_sdk::session_fs::{FsError, SessionFsBinaryProvider}; +use github_copilot_sdk::{ + Client, DirEntry, DirEntryKind, FileInfo, SessionFsCapabilities, SessionFsConfig, + SessionFsConventions, SessionFsProvider, +}; + +struct BinaryFileProvider { + root: PathBuf, + read_paths: Mutex>, +} + +impl BinaryFileProvider { + fn path(&self, path: &str) -> PathBuf { + self.root.join(path.trim_start_matches('/')) + } +} + +#[async_trait] +impl SessionFsBinaryProvider for BinaryFileProvider { + async fn read_file_bytes(&self, path: &str) -> Result, FsError> { + self.read_paths.lock().unwrap().push(path.to_string()); + tokio::fs::read(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn write_file_bytes( + &self, + path: &str, + content: &[u8], + _mode: Option, + ) -> Result<(), FsError> { + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::write(target, content) + .await + .map_err(FsError::from) + } +} + +#[async_trait] +impl SessionFsProvider for BinaryFileProvider { + fn binary(&self) -> Option<&dyn SessionFsBinaryProvider> { + Some(self) + } + + async fn read_file(&self, path: &str) -> Result { + tokio::fs::read_to_string(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn write_file( + &self, + path: &str, + content: &str, + _mode: Option, + ) -> Result<(), FsError> { + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::write(target, content) + .await + .map_err(FsError::from) + } + + async fn append_file( + &self, + path: &str, + content: &str, + _mode: Option, + ) -> Result<(), FsError> { + use tokio::io::AsyncWriteExt; + + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(target) + .await + .map_err(FsError::from)?; + file.write_all(content.as_bytes()) + .await + .map_err(FsError::from) + } + + async fn exists(&self, path: &str) -> Result { + tokio::fs::try_exists(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn stat(&self, path: &str) -> Result { + let metadata = tokio::fs::metadata(self.path(path)) + .await + .map_err(FsError::from)?; + Ok(FileInfo::new( + metadata.is_file(), + metadata.is_dir(), + metadata.len() as i64, + "1970-01-01T00:00:00Z", + "1970-01-01T00:00:00Z", + )) + } + + async fn mkdir(&self, path: &str, _recursive: bool, _mode: Option) -> Result<(), FsError> { + tokio::fs::create_dir_all(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn readdir(&self, path: &str) -> Result, FsError> { + let mut dir = tokio::fs::read_dir(self.path(path)) + .await + .map_err(FsError::from)?; + let mut names = Vec::new(); + while let Some(entry) = dir.next_entry().await.map_err(FsError::from)? { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + Ok(names) + } + + async fn readdir_with_types(&self, path: &str) -> Result, FsError> { + let mut dir = tokio::fs::read_dir(self.path(path)) + .await + .map_err(FsError::from)?; + let mut entries = Vec::new(); + while let Some(entry) = dir.next_entry().await.map_err(FsError::from)? { + let kind = if entry.file_type().await.map_err(FsError::from)?.is_dir() { + DirEntryKind::Directory + } else { + DirEntryKind::File + }; + entries.push(DirEntry::new(entry.file_name().to_string_lossy(), kind)); + } + Ok(entries) + } + + async fn rm(&self, path: &str, recursive: bool, force: bool) -> Result<(), FsError> { + let target = self.path(path); + let result = if recursive { + tokio::fs::remove_dir_all(target).await + } else { + tokio::fs::remove_file(target).await + }; + match result { + Err(err) if force && err.kind() == std::io::ErrorKind::NotFound => Ok(()), + result => result.map_err(FsError::from), + } + } + + async fn rename(&self, src: &str, dest: &str) -> Result<(), FsError> { + let target = self.path(dest); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::rename(self.path(src), target) + .await + .map_err(FsError::from) + } +} + +#[tokio::test] +async fn should_route_file_operations_through_the_session_fs_provider() { + super::support::with_e2e_context( + "session_fs", + "should_route_file_operations_through_the_session_fs_provider", + |ctx| { + Box::pin(async move { + let provider = Arc::new(BinaryFileProvider { + root: ctx.work_dir().join("text-provider"), + read_paths: Mutex::new(Vec::new()), + }); + let session_state_path = if cfg!(windows) { + "/session-state".to_string() + } else { + ctx.work_dir() + .join("session-state") + .to_string_lossy() + .into_owned() + }; + let client = Client::start( + ctx.client_options().with_session_fs( + SessionFsConfig::new( + "/", + session_state_path.clone(), + SessionFsConventions::Posix, + ) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)), + ), + ) + .await + .expect("start session client"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_session_fs_provider(provider.clone()), + ) + .await + .expect("create session"); + + let answer = session + .send_and_wait("What is 100 + 200?") + .await + .expect("send arithmetic request") + .expect("final assistant message"); + assert!( + answer.data["content"] + .as_str() + .is_some_and(|content| content.contains("300")), + "unexpected assistant response: {:?}", + answer.data + ); + session.disconnect().await.expect("disconnect session"); + let events_path = provider.path(&format!("{session_state_path}/events.jsonl")); + let events = tokio::fs::read_to_string(&events_path) + .await + .unwrap_or_else(|err| { + panic!("read provider events {}: {err}", events_path.display()) + }); + assert!( + events.contains("300"), + "provider events did not contain the response" + ); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_view_an_image_that_exists_only_in_the_binary_session_fs_provider() { + super::support::with_e2e_context( + "session_fs", + "should_view_an_image_that_exists_only_in_the_binary_session_fs_provider", + |ctx| { + Box::pin(async move { + let image_path = "/sdk-provider-image.png"; + let image_bytes = base64::engine::general_purpose::STANDARD + .decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==") + .unwrap(); + let provider = Arc::new(BinaryFileProvider { + root: ctx.work_dir().join("binary-provider"), + read_paths: Mutex::new(Vec::new()), + }); + let session_state_path = if cfg!(windows) { + "/session-state".to_string() + } else { + ctx.work_dir() + .join("session-state") + .to_string_lossy() + .into_owned() + }; + let client = Client::start( + ctx.client_options().with_session_fs( + SessionFsConfig::new("/", session_state_path, SessionFsConventions::Posix) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)), + ), + ) + .await + .expect("start binary session client"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_session_fs_provider(provider.clone()), + ) + .await + .expect("create binary session"); + let stored_path = provider.path(image_path); + tokio::fs::create_dir_all(stored_path.parent().unwrap()) + .await + .unwrap(); + tokio::fs::write(stored_path, &image_bytes).await.unwrap(); + assert!(!Path::new(image_path).exists()); + + let answer = session + .send_and_wait( + "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + ) + .await + .expect("send image view request") + .expect("final assistant message"); + assert!( + answer.data["content"] + .as_str() + .is_some_and(|content| content.contains("SDK_PROVIDER_IMAGE_DONE")), + "unexpected assistant response: {:?}", + answer.data + ); + assert!( + provider.read_paths.lock().unwrap().iter().any(|p| p == image_path), + "view did not request provider image bytes" + ); + let events = session.get_events().await.expect("get session events"); + assert!( + events.iter().any(|event| { + event.event_type == "session.binary_asset" + && event.data["mimeType"] == "image/png" + && event.data["data"] + == base64::engine::general_purpose::STANDARD.encode(&image_bytes) + }), + "missing image asset with exact provider bytes" + ); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} diff --git a/rust/tests/e2e/set_tools.rs b/rust/tests/e2e/set_tools.rs new file mode 100644 index 0000000000..c8ebb29bc9 --- /dev/null +++ b/rust/tests/e2e/set_tools.rs @@ -0,0 +1,316 @@ +use std::sync::Arc; + +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::tool::ToolHandler; +use github_copilot_sdk::{Error, ResumeSessionConfig, Tool, ToolInvocation, ToolResult}; +use serde_json::json; + +use super::support::assistant_message_content; + +// Shared with the other SDKs' set_tools E2E tests, which replay the same +// snapshots. +const FRUIT_PROMPT: &str = "Use lookup_fruit to find the fruit for code 42."; +const FRUIT_AND_VEGETABLE_PROMPT: &str = "Use lookup_fruit to find the fruit for code 42 again, and use lookup_vegetable to find the vegetable for code 7."; +const VEGETABLE_PROMPT: &str = "Use lookup_vegetable to find the vegetable for code 7."; + +/// Answers with a fixed result and records the codes it was called with. +struct LookupTool { + result: &'static str, + calls: Arc>>, +} + +#[async_trait::async_trait] +impl ToolHandler for LookupTool { + async fn call(&self, invocation: ToolInvocation) -> Result { + if let Some(code) = invocation + .arguments + .get("code") + .and_then(|code| code.as_i64()) + { + self.calls.lock().push(code); + } + Ok(ToolResult::Text(self.result.to_string())) + } +} + +type Calls = Arc>>; + +fn lookup_tool( + name: &str, + description: &str, + code_description: &str, + result: &'static str, +) -> (Tool, Calls) { + let calls = Calls::default(); + let tool = Tool::new(name) + .with_description(description) + .with_parameters(json!({ + "type": "object", + "properties": { + "code": { "type": "integer", "description": code_description } + }, + "required": ["code"] + })) + .with_handler(Arc::new(LookupTool { + result, + calls: calls.clone(), + })); + (tool, calls) +} + +fn lookup_fruit(fruit: &'static str) -> (Tool, Calls) { + lookup_tool( + "lookup_fruit", + "Looks up the fruit for a numeric code", + "Fruit code", + fruit, + ) +} + +fn lookup_vegetable() -> (Tool, Calls) { + lookup_tool( + "lookup_vegetable", + "Looks up the vegetable for a numeric code", + "Vegetable code", + "carrot", + ) +} + +fn calls(calls: &Calls) -> Vec { + calls.lock().clone() +} + +/// A tool with no parameters that answers with a fixed result. +fn plain_tool(name: &str, description: &str, result: &'static str) -> Tool { + Tool::new(name) + .with_description(description) + .with_handler(Arc::new(LookupTool { + result, + calls: Calls::default(), + })) +} + +/// Index of the first model request that carries `prompt` as a user message. +fn index_of_prompt(exchanges: &[serde_json::Value], prompt: &str) -> Option { + exchanges.iter().position(|exchange| { + exchange["request"]["messages"] + .as_array() + .into_iter() + .flatten() + .any(|message| { + let content = &message["content"]; + message["role"] == "user" + && content.as_str().map_or_else( + || content.to_string().contains(prompt), + |text| text.contains(prompt), + ) + }) + }) +} + +/// Asserts that every model request offered the tools in `offered` and none of +/// the tools in `not_offered`. +fn assert_offered(exchanges: &[serde_json::Value], offered: &[&str], not_offered: &[&str]) { + for exchange in exchanges { + let tools: Vec<&str> = exchange["request"]["tools"] + .as_array() + .into_iter() + .flatten() + .filter_map(|tool| tool["function"]["name"].as_str()) + .collect(); + for name in offered { + assert!(tools.contains(name), "offered {tools:?}, expected {name}"); + } + for name in not_offered { + assert!( + !tools.contains(name), + "offered {tools:?}, expected no {name}" + ); + } + } +} + +#[tokio::test] +async fn replaces_tools_on_a_created_session() { + super::support::with_shared_e2e_context( + &E2E, + "set_tools", + "replaces_tools_on_a_created_session", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (original, original_calls) = lookup_fruit("apple"); + let retired = plain_tool("retired_lookup", "Looks up a retired value", "retired"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_tools(vec![original, retired]), + ) + .await + .expect("create session"); + + let first = session + .send_and_wait(FRUIT_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&first).contains("apple")); + + let (replacement, replacement_calls) = lookup_fruit("dragonfruit"); + let (vegetable, vegetable_calls) = lookup_vegetable(); + session + .set_tools([replacement, vegetable]) + .await + .expect("replace tools"); + + let second = session + .send_and_wait(FRUIT_AND_VEGETABLE_PROMPT) + .await + .expect("send") + .expect("assistant message"); + let content = assistant_message_content(&second); + assert!(content.contains("dragonfruit"), "{content}"); + assert!(content.contains("carrot"), "{content}"); + assert_eq!(calls(&original_calls), [42]); + assert_eq!(calls(&replacement_calls), [42]); + assert_eq!(calls(&vegetable_calls), [7]); + + // Model requests after the replacement offer exactly the new tool set. + let exchanges = ctx.exchanges(); + let replaced_from = index_of_prompt(&exchanges, FRUIT_AND_VEGETABLE_PROMPT) + .expect("a model request carries the replacement prompt"); + assert!( + replaced_from > 0, + "expected model requests before the replacement" + ); + let (before, after) = exchanges.split_at(replaced_from); + assert_offered( + before, + &["lookup_fruit", "retired_lookup"], + &["lookup_vegetable"], + ); + assert_offered( + after, + &["lookup_fruit", "lookup_vegetable"], + &["retired_lookup"], + ); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn replaces_tools_on_a_resumed_session() { + super::support::with_shared_e2e_context( + &E2E, + "set_tools", + "replaces_tools_on_a_resumed_session", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (created_tool, created_calls) = lookup_fruit("apple"); + let created = client + .create_session( + ctx.approve_all_session_config() + .with_tools(vec![created_tool]), + ) + .await + .expect("create session"); + let session_id = created.id().clone(); + let first = created + .send_and_wait(FRUIT_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&first).contains("apple")); + assert_eq!(calls(&created_calls), [42]); + created.disconnect().await.expect("disconnect session"); + + let (fruit, fruit_calls) = lookup_fruit("apple"); + let resumed = client + .resume_session( + ResumeSessionConfig::new(session_id) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_tools(vec![fruit]), + ) + .await + .expect("resume session"); + let (vegetable, vegetable_calls) = lookup_vegetable(); + resumed.set_tools([vegetable]).await.expect("replace tools"); + + let answer = resumed + .send_and_wait(VEGETABLE_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&answer).contains("carrot")); + assert_eq!(calls(&vegetable_calls), [7]); + assert!(calls(&fruit_calls).is_empty()); + + let exchanges = ctx.exchanges(); + let replaced_from = index_of_prompt(&exchanges, VEGETABLE_PROMPT) + .expect("a model request carries the replacement prompt"); + assert!( + replaced_from > 0, + "expected model requests before the replacement" + ); + let (_, after) = exchanges.split_at(replaced_from); + assert_offered(after, &["lookup_vegetable"], &["lookup_fruit"]); + + resumed.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn keeps_the_previous_tools_when_a_replacement_is_rejected() { + super::support::with_shared_e2e_context( + &E2E, + "set_tools", + "keeps_the_previous_tools_when_a_replacement_is_rejected", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (original, original_calls) = lookup_fruit("apple"); + let session = client + .create_session(ctx.approve_all_session_config().with_tools(vec![original])) + .await + .expect("create session"); + + let (replacement, replacement_calls) = lookup_fruit("dragonfruit"); + let invalid = plain_tool("invalid.tool", "Has a name the runtime rejects", "never"); + session + .set_tools([replacement, invalid]) + .await + .expect_err("the runtime rejects an invalid tool name"); + + let answer = session + .send_and_wait(FRUIT_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&answer).contains("apple")); + assert_eq!(calls(&original_calls), [42]); + assert!(calls(&replacement_calls).is_empty()); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +static E2E: super::support::SharedE2eGroup = + super::support::SharedE2eGroup::standard("set_tools", 3); diff --git a/rust/tests/e2e/skill_provider.rs b/rust/tests/e2e/skill_provider.rs new file mode 100644 index 0000000000..2bbbac7814 --- /dev/null +++ b/rust/tests/e2e/skill_provider.rs @@ -0,0 +1,641 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::rpc::SkillSource; +use github_copilot_sdk::session_events::{SessionEventType, ToolExecutionCompleteData}; +use github_copilot_sdk::{ + CloudSessionOptions, Error, ErrorKind, ResumeSessionConfig, SessionConfig, SessionEvent, + SkillProvider, SkillProviderDescriptor, +}; +use tokio::sync::Notify; + +use super::support::{assistant_message_content, collect_until_idle}; + +static E2E: super::support::SharedE2eGroup = + super::support::SharedE2eGroup::standard("skill_provider", 5); + +#[tokio::test] +async fn should_load_provider_skill_lazily_through_skill_tool() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_load_provider_skill_lazily_through_skill_tool", + |ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + let skills = session.rpc().skills().list().await.expect("list skills"); + let listed = skills + .skills + .iter() + .find(|skill| skill.name == "provider-lookup") + .expect("provider skill"); + assert_eq!(listed.source, SkillSource::Sdk); + assert!(listed.enabled); + assert_eq!(listed.path.as_deref().unwrap_or_default(), ""); + assert_eq!(provider.reads(), Vec::::new()); + + let message = session + .send_and_wait( + "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(provider.reads(), vec!["provider-lookup"]); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("TANGERINE_QUARTZ_19")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_load_provider_and_file_based_skills_together() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_load_provider_and_file_based_skills_together", + |ctx| { + Box::pin(async move { + let skills_dir = write_file_notes_skill(ctx.work_dir()); + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_directories([skills_dir]) + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + let skills = session.rpc().skills().list().await.expect("list skills"); + let file_skill = skills + .skills + .iter() + .find(|skill| skill.name == "file-notes") + .expect("file skill"); + let provider_skill = skills + .skills + .iter() + .find(|skill| skill.name == "provider-audit") + .expect("provider skill"); + assert_ne!(file_skill.source, SkillSource::Sdk); + assert!(file_skill.path.as_deref().is_some_and(|path| !path.is_empty())); + assert_eq!(provider_skill.source, SkillSource::Sdk); + + let message = session + .send_and_wait( + "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(provider.reads(), vec!["provider-audit"]); + // Validate the final assistant response arrived (guards against truncated captures) + let content = assistant_message_content(&message); + assert!(content.contains("MAPLE_FALCON_27")); + assert!(content.contains("COBALT_HERON_58")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_rebind_skill_provider_on_resume() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_rebind_skill_provider_on_resume", + |ctx| { + Box::pin(async move { + let original = Arc::new(TestSkillProvider::new(vec![skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n", + )])); + let replacement = Arc::new(TestSkillProvider::new(vec![skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n", + )])); + let client = ctx.start_client().await; + let first = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(original.clone()), + ) + .await + .expect("create session"); + let session_id = first.id().clone(); + let ready = first + .send_and_wait( + "Without using any tools or skills, reply with exactly REBIND_READY.", + ) + .await + .expect("send readiness turn") + .expect("assistant message"); + assert!(assistant_message_content(&ready).contains("REBIND_READY")); + first.disconnect().await.expect("disconnect first session"); + assert_eq!(original.reads(), Vec::::new()); + let original_calls_before_resume = original.calls().len(); + + let session = client + .resume_session( + ResumeSessionConfig::new(session_id) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_skill_provider(replacement.clone()), + ) + .await + .expect("resume session"); + + let message = session + .send_and_wait( + "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(replacement.reads(), vec!["rebind-check"]); + assert_eq!(original.calls().len(), original_calls_before_resume); + // Validate the final assistant response arrived (guards against truncated captures) + let content = assistant_message_content(&message); + assert!(content.contains("BRONZE_BETA_22")); + assert!(!content.contains("AMBER_ALPHA_11")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_report_provider_read_failure_without_leaking_details() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_report_provider_read_failure_without_leaking_details", + |ctx| { + Box::pin(async move { + let secret = "PROVIDER_SECRET_7F3A9C"; + let provider = Arc::new(TestSkillProvider::new(vec![ProvidedSkill::new( + "broken-lookup", + "Reports the broken lookup verification word.", + SkillRead::Error(format!("database unavailable: {secret}")), + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let events = session.subscribe(); + + let message = session + .send_and_wait( + "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + ) + .await + .expect("send") + .expect("assistant message"); + let observed = collect_until_idle(events).await; + + assert!(provider.reads().contains(&"broken-lookup".to_string())); + let failures = failed_tool_executions(&observed); + assert_eq!(failures.len(), 1, "expected one failed tool execution"); + assert_no_secret(&observed, secret); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("LOAD_FAILED")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_report_missing_provider_skill_as_not_found() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_report_missing_provider_skill_as_not_found", + |ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![ProvidedSkill::new( + "vanished-lookup", + "Reports the vanished lookup verification word.", + SkillRead::Missing, + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let events = session.subscribe(); + + let message = session + .send_and_wait( + "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + ) + .await + .expect("send") + .expect("assistant message"); + let observed = collect_until_idle(events).await; + + assert!(provider.reads().contains(&"vanished-lookup".to_string())); + let failures = failed_tool_executions(&observed); + assert_eq!(failures.len(), 1, "expected one failed tool execution"); + let failure = serde_json::to_string(&failures[0]).expect("serialize failure"); + assert!( + failure.to_ascii_lowercase().contains("not found"), + "expected not found failure, got {failure}" + ); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("LOAD_FAILED")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_keep_provider_dormant_when_skills_disabled() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "dormant-lookup", + "Never listed.", + "Never read.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_enable_skills(false) + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + session + .rpc() + .skills() + .ensure_loaded() + .await + .expect("ensure skills loaded"); + let skills = session.rpc().skills().list().await.expect("list skills"); + + assert_no_sdk_skills(&skills.skills); + assert_eq!(provider.calls(), Vec::::new()); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +#[tokio::test] +async fn should_unbind_provider_when_resumed_without_one() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "unbound-lookup", + "Reports the unbound lookup word.", + "Unbound.\n", + )])); + let client = ctx.start_client().await; + let first = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let before = first.rpc().skills().list().await.expect("list skills"); + assert!( + before + .skills + .iter() + .any(|skill| skill.name == "unbound-lookup") + ); + let calls_before_resume = provider.calls().len(); + + let session = client + .resume_session( + ResumeSessionConfig::new(first.id().clone()) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN), + ) + .await + .expect("resume session"); + + session + .rpc() + .skills() + .reload() + .await + .expect("reload skills"); + let skills = session.rpc().skills().list().await.expect("list skills"); + + assert_no_sdk_skills(&skills.skills); + assert_eq!(provider.calls().len(), calls_before_resume); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + drop(first); + }) + }) + .await; +} + +#[tokio::test] +async fn should_cancel_a_blocked_provider_call_when_the_session_disconnects() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(Arc::new(BlockingSkillProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await + .expect("create session"); + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + let rpc = session.rpc(); + let skills = rpc.skills(); + let list = skills.list(); + tokio::pin!(list); + tokio::select! { + _ = entered.notified() => {} + _ = &mut list => panic!("skills.list finished before the provider was called"), + _ = tokio::time::sleep(Duration::from_secs(30)) => { + panic!("provider list_skills was not called"); + } + } + + session.disconnect().await.expect("disconnect session"); + tokio::time::timeout(Duration::from_secs(10), dropped.notified()) + .await + .expect("provider future was not dropped after disconnect"); + + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +#[tokio::test] +async fn should_reject_skill_provider_for_cloud_sessions() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "cloud-lookup", + "Never listed.", + "Never read.\n", + )])); + let client = ctx.start_client().await; + + let result = client + .create_session( + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_cloud(CloudSessionOptions::default()) + .with_skill_provider(provider.clone()), + ) + .await; + + match result { + Ok(session) => { + let _ = session.disconnect().await; + panic!("cloud session with skill provider unexpectedly succeeded"); + } + Err(error) => { + assert!( + error + .to_string() + .contains("Skill providers are not supported for cloud sessions."), + "unexpected error: {error}" + ); + } + } + assert_eq!(provider.calls(), Vec::::new()); + + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +struct ProvidedSkill { + descriptor: SkillProviderDescriptor, + read: SkillRead, +} + +impl ProvidedSkill { + fn new(name: &str, description: &str, read: SkillRead) -> Self { + Self { + descriptor: descriptor(name, description), + read, + } + } +} + +enum SkillRead { + Markdown(&'static str), + Missing, + Error(String), +} + +struct TestSkillProvider { + skills: Vec, + calls: Mutex>, +} + +impl TestSkillProvider { + fn new(skills: Vec) -> Self { + Self { + skills, + calls: Mutex::new(Vec::new()), + } + } + + fn calls(&self) -> Vec { + self.calls.lock().expect("provider call log").clone() + } + + fn reads(&self) -> Vec { + self.calls() + .into_iter() + .filter_map(|call| call.strip_prefix("read:").map(str::to_string)) + .collect() + } + + fn push_call(&self, call: impl Into) { + self.calls + .lock() + .expect("provider call log") + .push(call.into()); + } +} + +#[async_trait] +impl SkillProvider for TestSkillProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + self.push_call("list"); + Ok(self + .skills + .iter() + .map(|skill| skill.descriptor.clone()) + .collect()) + } + + async fn read_skill(&self, name: &str) -> std::result::Result, Error> { + self.push_call(format!("read:{name}")); + let Some(skill) = self + .skills + .iter() + .find(|skill| skill.descriptor.name == name) + else { + return Ok(None); + }; + + match &skill.read { + SkillRead::Markdown(markdown) => Ok(Some((*markdown).to_string())), + SkillRead::Missing => Ok(None), + SkillRead::Error(message) => Err(Error::with_message( + ErrorKind::InvalidConfig, + message.clone(), + )), + } + } +} + +fn skill(name: &str, description: &str, markdown: &'static str) -> ProvidedSkill { + ProvidedSkill::new(name, description, SkillRead::Markdown(markdown)) +} + +struct NotifyOnDrop(Arc); + +impl Drop for NotifyOnDrop { + fn drop(&mut self) { + self.0.notify_one(); + } +} + +/// Blocks `list_skills` until the SDK drops its future. +struct BlockingSkillProvider { + entered: Arc, + dropped: Arc, +} + +#[async_trait] +impl SkillProvider for BlockingSkillProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + Ok(None) + } +} + +fn descriptor(name: &str, description: &str) -> SkillProviderDescriptor { + SkillProviderDescriptor { + name: name.to_string(), + description: description.to_string(), + ..Default::default() + } +} + +fn write_file_notes_skill(work_dir: &Path) -> std::path::PathBuf { + let skills_dir = work_dir.join("file-skills"); + let skill_dir = skills_dir.join("file-notes"); + std::fs::create_dir_all(&skill_dir).expect("create skill directory"); + std::fs::write( + skill_dir.join("SKILL.md"), + "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n", + ) + .expect("write file skill"); + skills_dir +} + +fn failed_tool_executions(events: &[SessionEvent]) -> Vec { + events + .iter() + .filter(|event| event.parsed_type() == SessionEventType::ToolExecutionComplete) + .filter_map(|event| event.typed_data::()) + .filter(|data| !data.success) + .collect() +} + +fn assert_no_secret(events: &[SessionEvent], secret: &str) { + let events_json = serde_json::to_string(events).expect("serialize events"); + assert!( + !events_json.contains(secret), + "provider secret leaked into events: {events_json}" + ); +} + +fn assert_no_sdk_skills(skills: &[github_copilot_sdk::rpc::Skill]) { + assert!( + skills.iter().all(|skill| skill.source != SkillSource::Sdk), + "expected no SDK skills, got {skills:?}" + ); +} diff --git a/rust/tests/e2e/subagent_hooks.rs b/rust/tests/e2e/subagent_hooks.rs index 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 4f058c12ef..fa945bf584 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 @@ -423,8 +403,19 @@ pub struct E2eContext { impl E2eContext { async fn new(category: &str, snapshot_name: &str) -> std::io::Result { + Self::new_with_cli(category, snapshot_name, None).await + } + + pub async fn new_with_cli( + category: &str, + snapshot_name: &str, + cli_override: Option, + ) -> std::io::Result { let repo_root = repo_root(); - let cli_path = cli_path(&repo_root)?; + let cli_path = match cli_override { + Some(path) => path, + None => cli_path(&repo_root)?, + }; let home_dir = tempfile::tempdir()?; let work_dir = tempfile::tempdir()?; let proxy_root = repo_root.clone(); @@ -481,6 +472,11 @@ impl E2eContext { self.work_dir.path() } + /// The runtime's `COPILOT_HOME`, which holds `session-state/`. + pub fn home_dir(&self) -> &Path { + self.home_dir.path() + } + pub fn proxy_url(&self) -> &str { self.proxy().url() } @@ -575,6 +571,7 @@ impl E2eContext { ) { let mut user = json!({ "login": login, + "id": 12345, "copilot_plan": "individual_pro", "endpoints": { "api": self.proxy_url(), @@ -1070,6 +1067,44 @@ struct InProcessEnvGuard { previous_cwd: PathBuf, } +pub struct InProcessHostGuard { + saved: [(OsString, Option); 2], +} + +pub fn set_inprocess_host_for_test( + copilot_host: &str, + gh_host: &str, +) -> Option { + if !is_inprocess_default() { + return None; + } + let saved = [ + ( + "COPILOT_GH_HOST".into(), + std::env::var_os("COPILOT_GH_HOST"), + ), + ("GH_HOST".into(), std::env::var_os("GH_HOST")), + ]; + // SAFETY: in-process E2E tests hold the suite's single concurrency permit. + unsafe { + std::env::set_var("COPILOT_GH_HOST", copilot_host); + std::env::set_var("GH_HOST", gh_host); + } + Some(InProcessHostGuard { saved }) +} + +impl Drop for InProcessHostGuard { + fn drop(&mut self) { + for (key, previous) in &self.saved { + // SAFETY: this guard is dropped before releasing the in-process permit. + match previous { + Some(value) => unsafe { std::env::set_var(key, value) }, + None => unsafe { std::env::remove_var(key) }, + } + } + } +} + impl InProcessEnvGuard { /// Returns `Some` guard (having applied the env) when in-process, else `None`. fn activate(ctx: &E2eContext) -> Option { @@ -1192,8 +1227,7 @@ fn cli_path(repo_root: &Path) -> std::io::Result { } } - let npm = if cfg!(windows) { "npm.cmd" } else { "npm" }; - let output = std::process::Command::new(npm) + let output = std::process::Command::new(npm_program()) .args(["run", "--silent", "prepare:runtime", "--", "--print-path"]) .current_dir(repo_root.join("nodejs")) .output()?; @@ -1266,14 +1300,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(); @@ -1530,8 +1570,26 @@ fn node_program() -> &'static str { if cfg!(windows) { "node.exe" } else { "node" } } -fn npx_program() -> &'static str { - if cfg!(windows) { "npx.cmd" } else { "npx" } +fn npm_program() -> &'static str { + if cfg!(windows) { + windows_program(&["npm.cmd", "npm.exe", "npm"]) + } else { + "npm" + } +} + +fn windows_program(candidates: &[&'static str]) -> &'static str { + candidates + .iter() + .copied() + .find(|candidate| program_exists_on_path(candidate)) + .unwrap_or(candidates[0]) +} + +fn program_exists_on_path(program: &str) -> bool { + std::env::var_os("PATH").is_some_and(|path| { + std::env::split_paths(&path).any(|directory| directory.join(program).is_file()) + }) } #[test] diff --git a/rust/tests/e2e/telemetry.rs b/rust/tests/e2e/telemetry.rs index 386cc2f560..fe83a1e2ea 100644 --- a/rust/tests/e2e/telemetry.rs +++ b/rust/tests/e2e/telemetry.rs @@ -146,6 +146,152 @@ async fn should_export_file_telemetry_for_sdk_interactions() { .await; } +#[tokio::test] +async fn should_export_per_request_subagent_chat_spans() { + if super::support::skip_inprocess("telemetry configuration is not honored in-process") { + return; + } + with_e2e_context( + "telemetry", + "should_export_per_request_subagent_chat_spans", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let telemetry_path = ctx.work_dir().join("rust-subagent-telemetry.jsonl"); + let source_name = "rust-sdk-subagent-telemetry-e2e"; + let prompt = "Use the task tool in sync mode to ask a task agent to read subagent-otel.txt with the view tool. Then reply with SUBAGENT_OTEL_DONE."; + std::fs::write(ctx.work_dir().join("subagent-otel.txt"), "SUBAGENT_OTEL_FILE_CONTENT") + .expect("write child file"); + + let client = Client::start(ctx.client_options().with_telemetry( + TelemetryConfig::new() + .with_file_path(&telemetry_path) + .with_exporter_type(OtelExporterType::File) + .with_source_name(source_name) + .with_capture_content(true), + )) + .await + .expect("start client"); + let session = client + .create_session( + SessionConfig::default() + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await + .expect("create session"); + let answer = session + .send_and_wait(prompt) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&answer).contains("SUBAGENT_OTEL_DONE")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + let entries = read_telemetry_entries(&telemetry_path); + let spans: Vec<_> = entries + .iter() + .filter(|entry| string_property(entry, "type") == Some("span")) + .collect(); + assert!(spans.iter().all(|span| { + span.get("instrumentationScope") + .and_then(|scope| string_property(scope, "name")) + == Some(source_name) + && status_code(span) != Some(2) + })); + let invocations: Vec<_> = spans + .iter() + .copied() + .filter(|span| { + string_attribute(span, "gen_ai.operation.name").as_deref() + == Some("invoke_agent") + }) + .collect(); + assert_eq!(invocations.len(), 2, "invocations: {invocations:?}"); + let roots: Vec<_> = invocations + .iter() + .copied() + .filter(|span| is_root_span(span)) + .collect(); + assert_eq!(roots.len(), 1, "roots: {roots:?}"); + let root = roots[0]; + assert_eq!( + string_attribute(root, "gen_ai.conversation.id").as_deref(), + Some(session.id().as_str()) + ); + let root_id = string_property(root, "spanId").expect("root span id"); + let trace_id = string_property(root, "traceId").expect("root trace id"); + let tasks: Vec<_> = spans + .iter() + .copied() + .filter(|span| { + string_attribute(span, "gen_ai.operation.name").as_deref() + == Some("execute_tool") + && string_attribute(span, "gen_ai.tool.name").as_deref() == Some("task") + }) + .collect(); + assert_eq!(tasks.len(), 1, "task spans: {tasks:?}"); + let task = tasks[0]; + assert_eq!(string_property(task, "parentSpanId"), Some(root_id)); + let children: Vec<_> = invocations + .iter() + .copied() + .filter(|span| string_property(span, "parentSpanId") == string_property(task, "spanId")) + .collect(); + assert_eq!(children.len(), 1, "child spans: {children:?}"); + let child = children[0]; + assert_eq!(string_property(task, "traceId"), Some(trace_id)); + assert_eq!(string_property(child, "traceId"), Some(trace_id)); + + let chats: Vec<_> = spans + .iter() + .copied() + .filter(|span| { + string_attribute(span, "gen_ai.operation.name").as_deref() == Some("chat") + }) + .collect(); + assert_eq!(chats.len(), 4, "chats: {chats:?}"); + let parent_chats: Vec<_> = chats + .iter() + .copied() + .filter(|span| string_property(span, "parentSpanId") == Some(root_id)) + .collect(); + assert_eq!(parent_chats.len(), 2, "parent chats: {parent_chats:?}"); + assert!(parent_chats.iter().all(|span| string_property(span, "traceId") == Some(trace_id))); + let child_chats: Vec<_> = chats + .iter() + .copied() + .filter(|span| string_property(span, "parentSpanId") == string_property(child, "spanId")) + .collect(); + assert_eq!(child_chats.len(), 2, "child chats: {child_chats:?}"); + assert!(child_chats.iter().all(|span| { + string_property(span, "traceId") == Some(trace_id) + && string_attribute(span, "github.copilot.initiator").as_deref() + == Some("sub-agent") + })); + let view_chats: Vec<_> = child_chats + .iter() + .copied() + .filter(|span| string_attribute(span, "gen_ai.output.messages").is_some_and(|output| output.contains(r#""view""#))) + .collect(); + assert_eq!(view_chats.len(), 1, "view chats: {view_chats:?}"); + assert!(!string_attribute(view_chats[0], "gen_ai.input.messages") + .is_some_and(|input| input.contains("SUBAGENT_OTEL_FILE_CONTENT"))); + let final_chats: Vec<_> = child_chats + .iter() + .copied() + .filter(|span| string_attribute(span, "gen_ai.output.messages").is_some_and(|output| output.contains("SUBAGENT_OTEL_CHILD_DONE"))) + .collect(); + assert_eq!(final_chats.len(), 1, "final chats: {final_chats:?}"); + assert!(string_attribute(final_chats[0], "gen_ai.input.messages") + .is_some_and(|input| input.contains("SUBAGENT_OTEL_FILE_CONTENT"))); + }) + }, + ) + .await; +} + struct EchoTelemetryTool; #[async_trait] 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/fixtures/installation_confirmation.json b/rust/tests/fixtures/installation_confirmation.json new file mode 100644 index 0000000000..5a5e6721d8 --- /dev/null +++ b/rust/tests/fixtures/installation_confirmation.json @@ -0,0 +1,52 @@ +{ + "confirmationId": "confirmation-a", + "operationId": "operation-a", + "policySessionId": "original-session", + "expiresAt": "2026-09-24T03:00:00Z", + "reviewFingerprint": "fingerprint-a", + "review": { + "resource": "mcp", + "review": { + "action": "install", + "identity": { + "canonicalName": "io.example/server", + "serverName": "example" + }, + "provenance": { + "authority": "cards.example.test", + "validatedAt": "2026-09-24T02:59:00Z", + "cardDigest": { + "algorithm": "sha256-rfc8785", + "value": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + }, + "mediaType": "application/mcp-server-card+json" + }, + "target": { "scope": "user", "configKey": "example" }, + "policy": { "decision": "allowed", "source": "none" }, + "selectedChoice": { + "choiceId": "remote-choice", + "installMethod": "remote", + "endpoint": "https://example.test/mcp", + "transport": "streamable-http", + "requiredValues": [], + "secretPlaceholders": [] + }, + "configurationChange": { + "operation": "add", + "scope": "user", + "configKey": "example", + "changedFields": ["type", "url", "headers", "tools"], + "secretReferences": [] + }, + "inputs": [], + "suppliedSecrets": [], + "secretStorage": "keychain", + "effectiveConfiguration": { + "transport": "streamable-http", + "url": "https://example.test/mcp", + "headers": { "X-Region": "eu" }, + "tools": ["*"] + } + } + } +} diff --git a/rust/tests/installation_confirmation_test.rs b/rust/tests/installation_confirmation_test.rs new file mode 100644 index 0000000000..6b407c5542 --- /dev/null +++ b/rust/tests/installation_confirmation_test.rs @@ -0,0 +1,829 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![allow(clippy::unwrap_used)] + +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::extension_launch_provider::{ + ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use github_copilot_sdk::installation_confirmation::{ + InstallationConfirmationContext, InstallationConfirmationHandler, + InstallationConfirmationRequest, InstallationDecision, InstallationReview, + McpInstallationReview, +}; +use github_copilot_sdk::rpc::SkillInstallationReview; +use github_copilot_sdk::{ + CliProgram, Client, ClientOptions, CopilotHttpRequest, CopilotHttpResponse, + CopilotRequestContext, CopilotRequestError, CopilotRequestHandler, Error, ErrorKind, Result, + Transport, +}; +use serde_json::{Value, json}; + +fn mcp_review(review: &InstallationReview) -> &McpInstallationReview { + let InstallationReview::Mcp(review) = review else { + panic!("expected MCP installation review"); + }; + &review.review +} + +#[test] +fn skill_confirmation_review_deserializes_as_typed_skill_review() { + let request: InstallationConfirmationRequest = serde_json::from_value(json!({ + "policySessionId": "original-session", + "confirmationId": "challenge-skill", + "operationId": "operation-skill", + "expiresAt": "2026-09-24T03:00:00Z", + "reviewFingerprint": "fingerprint-skill", + "review": { + "resource": "skill", + "review": { + "action": "install", + "name": "demo", + "description": "Demo Skill", + "catalogue": { + "resourceId": "123", + "displayName": "Demo Skill", + "description": "Demo Skill", + "publisher": "Octo", + "source": "https://agentfinder.github.com" + }, + "source": { + "resourceId": "123", + "catalogRevisionId": "1", + "repositoryId": "42", + "repository": "octo/demo", + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "root": "", + "descriptorDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "target": { + "scope": "personal", + "displayLabel": "~/.copilot/skills/demo", + "relativePath": "skills/demo", + "diagnosticsAbsolutePath": "/home/skills/demo" + }, + "installsDisabled": true, + "files": [{ + "path": "SKILL.md", + "sizeBytes": 12, + "mediaType": "text/markdown", + "executable": false, + "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333" + }], + "totalBytes": 12, + "entrypointPath": "SKILL.md", + "entrypointContent": "---\\nname: demo\\n---" + } + } + })) + .unwrap(); + + let InstallationReview::Skill(review) = request.review else { + panic!("expected Skill installation review"); + }; + let SkillInstallationReview::Install(install) = review.review else { + panic!("expected Skill install review"); + }; + assert_eq!(install.name, "demo"); + assert!(install.installs_disabled); +} +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, duplex}; +use tokio::net::TcpListener; +use tokio::sync::{Notify, mpsc, oneshot}; +use tokio::time::timeout; + +const WAIT: Duration = Duration::from_secs(2); + +async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), value: Value) { + let body = serde_json::to_vec(&value).unwrap(); + writer + .write_all(format!("Content-Length: {}\r\n\r\n", body.len()).as_bytes()) + .await + .unwrap(); + writer.write_all(&body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Value { + timeout(WAIT, read_frame_untimed(reader)).await.unwrap() +} + +async fn read_frame_untimed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let mut header = Vec::new(); + while !header.ends_with(b"\r\n\r\n") { + header.push(reader.read_u8().await.unwrap()); + } + let header = String::from_utf8(header).unwrap(); + let length = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut body = vec![0; length]; + reader.read_exact(&mut body).await.unwrap(); + serde_json::from_slice(&body).unwrap() +} + +fn request(operation: &str) -> Value { + let mut value: Value = + serde_json::from_str(include_str!("fixtures/installation_confirmation.json")).unwrap(); + value["operationId"] = json!(operation); + value["confirmationId"] = json!(format!("challenge-{operation}")); + value["reviewFingerprint"] = json!(format!("fingerprint-{operation}")); + value +} + +#[test] +fn confirmation_review_preserves_optional_unrecognised_trust() { + for trust in [ + json!(42), + json!({"schemaVersion": "v2"}), + json!({"status": "future-status", "unknown": "x".repeat(4097)}), + ] { + let mut wire = request("a"); + wire["review"]["review"]["catalogueTrust"] = trust; + let request: InstallationConfirmationRequest = + serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(request).unwrap(), wire); + } +} + +async fn confirm(writer: &mut (impl AsyncWrite + Unpin), id: u64, params: Value) { + write_frame( + writer, + json!({ + "jsonrpc": "2.0", "id": id, "method": "installations.confirm", "params": params + }), + ) + .await; +} + +async fn cancel(writer: &mut (impl AsyncWrite + Unpin), id: Value) { + write_frame( + writer, + json!({ + "jsonrpc": "2.0", "method": "$/cancelRequest", "params": { "id": id } + }), + ) + .await; +} + +struct Review { + request: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + decision: oneshot::Sender>, +} + +struct ControlledHandler(mpsc::UnboundedSender); + +#[async_trait] +impl InstallationConfirmationHandler for ControlledHandler { + async fn confirm( + &self, + request: InstallationConfirmationRequest, + context: InstallationConfirmationContext, + ) -> Result { + let (decision, receive) = oneshot::channel(); + self.0 + .send(Review { + request, + context, + decision, + }) + .unwrap_or_else(|_| panic!("review receiver closed")); + receive.await.unwrap() + } +} + +struct Peer { + client: Client, + reader: DuplexStream, + writer: DuplexStream, + reviews: mpsc::UnboundedReceiver, + _directory: tempfile::TempDir, +} + +impl Peer { + fn new(configured: bool, start_router: bool) -> Self { + let (client_writer, reader) = duplex(32768); + let (writer, client_reader) = duplex(32768); + let directory = tempfile::tempdir().unwrap(); + let (send, reviews) = mpsc::unbounded_channel(); + let client = if configured { + Client::from_streams_with_installation_confirmation_handler( + client_reader, + client_writer, + directory.path().into(), + Arc::new(ControlledHandler(send)), + ) + } else { + Client::from_streams(client_reader, client_writer, directory.path().into()) + } + .unwrap(); + if start_router { + client.start_router_for_test(); + } + Self { + client, + reader, + writer, + reviews, + _directory: directory, + } + } + + async fn review(&mut self) -> Review { + timeout(WAIT, self.reviews.recv()).await.unwrap().unwrap() + } +} + +impl Drop for Peer { + fn drop(&mut self) { + self.client.force_stop(); + } +} + +#[tokio::test] +async fn same_session_reviews_finish_out_of_order_without_blocking_other_rpcs() { + let mut peer = Peer::new(true, true); + confirm(&mut peer.writer, 101, request("a")).await; + confirm(&mut peer.writer, 102, request("b")).await; + let first = peer.review().await; + let second = peer.review().await; + let (a, b) = if first.request.operation_id == "a" { + (first, second) + } else { + (second, first) + }; + assert_eq!( + a.request.policy_session_id.as_deref(), + Some("original-session") + ); + assert_eq!(a.request.policy_session_id, b.request.policy_session_id); + assert_eq!(a.request.expires_at, "2026-09-24T03:00:00Z"); + let McpInstallationReview::Install(review) = mcp_review(&a.request.review) else { + panic!("expected typed install review"); + }; + assert_eq!( + review.effective_configuration.as_ref().unwrap().headers["X-Region"], + "eu" + ); + assert_eq!(serde_json::to_value(&a.request).unwrap(), request("a")); + + write_frame( + &mut peer.writer, + json!({ + "jsonrpc": "2.0", "id": 103, "method": "gitHubToken.getToken", + "params": {"registrationId": "unknown", "host": "github.com", "reason": "initial"} + }), + ) + .await; + assert_eq!(read_frame(&mut peer.reader).await["id"], 103); + + b.decision.send(Ok(InstallationDecision::Decline)).unwrap(); + let response = read_frame(&mut peer.reader).await; + assert_eq!(response["id"], 102); + assert_eq!( + response["result"], + json!({ + "confirmationId": "challenge-b", "reviewFingerprint": "fingerprint-b", "decision": "decline" + }) + ); + a.decision.send(Ok(InstallationDecision::Confirm)).unwrap(); + let response = read_frame(&mut peer.reader).await; + assert_eq!(response["id"], 101); + assert_eq!(response["result"]["confirmationId"], "challenge-a"); + assert_eq!(response["result"]["decision"], "confirm"); +} + +#[tokio::test] +async fn request_then_deadline_cancellation_before_router_start_never_opens_review() { + let mut peer = Peer::new(true, false); + let client = peer.client.clone(); + let barrier = tokio::spawn(async move { client.call("barrier", None).await }); + let outbound = read_frame(&mut peer.reader).await; + confirm(&mut peer.writer, 201, request("expired")).await; + cancel(&mut peer.writer, json!(201)).await; + write_frame( + &mut peer.writer, + json!({ + "jsonrpc": "2.0", "id": outbound["id"], "result": {} + }), + ) + .await; + timeout(WAIT, barrier).await.unwrap().unwrap().unwrap(); + peer.client.start_router_for_test(); + let response = read_frame(&mut peer.reader).await; + assert_eq!(response["id"], 201); + assert_eq!(response["error"]["code"], -32800); + assert!(peer.reviews.try_recv().is_err()); +} + +#[tokio::test] +async fn cancelled_a_cannot_approve_or_cancel_successor_b_on_the_same_session() { + let mut peer = Peer::new(true, true); + confirm(&mut peer.writer, 301, request("a")).await; + let a = peer.review().await; + cancel(&mut peer.writer, json!(301)).await; + assert_eq!(read_frame(&mut peer.reader).await["error"]["code"], -32800); + assert!(a.context.cancellation().is_cancelled()); + assert!(a.decision.send(Ok(InstallationDecision::Confirm)).is_err()); + + confirm(&mut peer.writer, 302, request("b")).await; + let b = peer.review().await; + cancel(&mut peer.writer, json!(301)).await; + cancel(&mut peer.writer, json!(999)).await; + cancel(&mut peer.writer, json!("302")).await; + write_frame( + &mut peer.writer, + json!({ + "jsonrpc": "2.0", "id": 303, "method": "gitHubToken.getToken", + "params": {"registrationId": "unknown", "host": "github.com", "reason": "initial"} + }), + ) + .await; + assert_eq!(read_frame(&mut peer.reader).await["id"], 303); + assert!(!b.context.cancellation().is_cancelled()); + b.decision.send(Ok(InstallationDecision::Confirm)).unwrap(); + let response = read_frame(&mut peer.reader).await; + assert_eq!(response["id"], 302); + assert_eq!(response["result"]["confirmationId"], "challenge-b"); +} + +#[tokio::test] +async fn cancellation_and_decision_ready_together_rejects_cancellation() { + let mut peer = Peer::new(true, true); + confirm(&mut peer.writer, 350, request("simultaneous")).await; + let review = peer.review().await; + cancel(&mut peer.writer, json!(350)).await; + let _ = review.decision.send(Ok(InstallationDecision::Confirm)); + let response = read_frame(&mut peer.reader).await; + assert_eq!(response["id"], 350); + assert_eq!(response["error"]["code"], -32800); + assert!(review.context.cancellation().is_cancelled()); +} + +#[tokio::test] +async fn peer_closure_cancels_all_reviews_without_retiring_another_connection() { + let mut a = Peer::new(true, true); + let mut b = Peer::new(true, true); + confirm(&mut a.writer, 401, request("a1")).await; + confirm(&mut a.writer, 402, request("a2")).await; + confirm(&mut b.writer, 401, request("b")).await; + let review_a1 = a.review().await; + let review_a2 = a.review().await; + let review_b = b.review().await; + a.writer.shutdown().await.unwrap(); + timeout(WAIT, review_a1.context.cancellation().cancelled()) + .await + .unwrap(); + timeout(WAIT, review_a2.context.cancellation().cancelled()) + .await + .unwrap(); + assert!(!review_b.context.cancellation().is_cancelled()); + review_b + .decision + .send(Ok(InstallationDecision::Cancel)) + .unwrap(); + assert_eq!( + read_frame(&mut b.reader).await["result"]["decision"], + "cancel" + ); +} + +#[tokio::test] +async fn dropping_the_client_retires_pending_review_without_waiting_for_peer_close() { + let (client_writer, _reader) = duplex(32768); + let (mut writer, client_reader) = duplex(32768); + let directory = tempfile::tempdir().unwrap(); + let (send, mut reviews) = mpsc::unbounded_channel(); + let client = Client::from_streams_with_installation_confirmation_handler( + client_reader, + client_writer, + directory.path().into(), + Arc::new(ControlledHandler(send)), + ) + .unwrap(); + client.start_router_for_test(); + confirm(&mut writer, 450, request("a")).await; + let review = timeout(WAIT, reviews.recv()).await.unwrap().unwrap(); + drop(client); + timeout(WAIT, review.context.cancellation().cancelled()) + .await + .unwrap(); +} + +#[tokio::test] +async fn missing_handler_and_invalid_review_return_errors_without_user_invocation() { + let mut missing = Peer::new(false, true); + confirm(&mut missing.writer, 501, request("a")).await; + assert_eq!( + read_frame(&mut missing.reader).await["error"]["code"], + -32601 + ); + + let mut peer = Peer::new(true, true); + let mut unknown_resource = request("a"); + unknown_resource["review"]["resource"] = json!("future-resource"); + let mut unknown_action = request("a"); + unknown_action["review"]["review"]["action"] = json!("future-action"); + let mut unknown_choice = request("a"); + unknown_choice["review"]["review"]["selectedChoice"]["installMethod"] = json!("future"); + let mut missing_url = request("a"); + missing_url["review"]["review"]["effectiveConfiguration"] + .as_object_mut() + .unwrap() + .remove("url"); + for (index, params) in [ + Value::Null, + unknown_resource, + unknown_action, + unknown_choice, + missing_url, + ] + .into_iter() + .enumerate() + { + confirm(&mut peer.writer, 510 + index as u64, params).await; + assert_eq!(read_frame(&mut peer.reader).await["error"]["code"], -32602); + } + assert!(peer.reviews.try_recv().is_err()); +} + +#[tokio::test] +async fn legacy_optional_fields_are_not_replaced_by_inferred_context() { + let mut peer = Peer::new(true, true); + let mut params = request("legacy"); + params.as_object_mut().unwrap().remove("policySessionId"); + params["review"]["review"] + .as_object_mut() + .unwrap() + .remove("effectiveConfiguration"); + confirm(&mut peer.writer, 601, params).await; + let review = peer.review().await; + assert!(review.request.policy_session_id.is_none()); + let McpInstallationReview::Install(install) = mcp_review(&review.request.review) else { + panic!("expected install"); + }; + assert!(install.effective_configuration.is_none()); + review + .decision + .send(Ok(InstallationDecision::Decline)) + .unwrap(); + assert_eq!( + read_frame(&mut peer.reader).await["result"]["decision"], + "decline" + ); +} + +#[tokio::test] +async fn handler_errors_and_unknown_decisions_never_become_approval() { + let mut peer = Peer::new(true, true); + for (id, decision) in [ + ( + 701, + Err(Error::with_message( + ErrorKind::InvalidConfig, + "review unavailable", + )), + ), + (702, Ok(InstallationDecision::Unknown)), + ] { + confirm(&mut peer.writer, id, request("a")).await; + peer.review().await.decision.send(decision).unwrap(); + let response = read_frame(&mut peer.reader).await; + assert_eq!(response["error"]["code"], -32603); + assert!(response.get("result").is_none()); + } +} + +#[tokio::test] +async fn real_client_start_installs_global_receiver_without_registration_or_session_rpc() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let (send, mut reviews) = mpsc::unbounded_channel(); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let (mut reader, mut writer) = stream.into_split(); + let connect = read_frame(&mut reader).await; + assert_eq!(connect["method"], "connect"); + write_frame( + &mut writer, + json!({ + "jsonrpc": "2.0", "id": connect["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test" } + }), + ) + .await; + confirm(&mut writer, 801, request("startup")).await; + read_frame(&mut reader).await + }); + let client = Client::start( + ClientOptions::new() + .with_program(CliProgram::Path("unused-external-transport".into())) + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: None, + }) + .with_installation_confirmation_handler(ControlledHandler(send)), + ) + .await + .unwrap(); + let review = timeout(WAIT, reviews.recv()).await.unwrap().unwrap(); + review + .decision + .send(Ok(InstallationDecision::Confirm)) + .unwrap(); + let response = timeout(WAIT, server).await.unwrap().unwrap(); + assert_eq!(response["id"], 801); + assert_eq!(response["result"]["decision"], "confirm"); + client.force_stop(); +} + +/// Never returns until released, standing in for a hung host callback. +struct BlockedLaunchProvider { + entered: mpsc::UnboundedSender<()>, + release: Arc, +} + +#[async_trait] +impl ExtensionLaunchProvider for BlockedLaunchProvider { + async fn resolve( + &self, + _request: ExtensionLaunchProviderResolveRequest, + ) -> Result { + self.entered.send(()).unwrap(); + self.release.notified().await; + Ok(ExtensionLaunchProviderResolveResult { launch: None }) + } +} + +#[tokio::test] +async fn blocked_global_callback_does_not_delay_confirmation_or_its_cancellation() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let (send, mut reviews) = mpsc::unbounded_channel(); + let (entered, mut resolve_entered) = mpsc::unbounded_channel(); + let release = Arc::new(Notify::new()); + let (to_server, mut server_commands) = mpsc::unbounded_channel::(); + let (from_server, mut server_frames) = mpsc::unbounded_channel::(); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let (mut reader, mut writer) = stream.into_split(); + let connect = read_frame(&mut reader).await; + assert_eq!(connect["method"], "connect"); + write_frame( + &mut writer, + json!({ + "jsonrpc": "2.0", "id": connect["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test" } + }), + ) + .await; + let register = read_frame(&mut reader).await; + assert_eq!(register["method"], "registerExtensionLaunchProvider"); + write_frame( + &mut writer, + json!({ "jsonrpc": "2.0", "id": register["id"], "result": {} }), + ) + .await; + let forward = tokio::spawn(async move { + loop { + let frame = read_frame_untimed(&mut reader).await; + if from_server.send(frame).is_err() { + break; + } + } + }); + while let Some(frame) = server_commands.recv().await { + write_frame(&mut writer, frame).await; + } + forward.abort(); + }); + let client = Client::start( + ClientOptions::new() + .with_program(CliProgram::Path("unused-external-transport".into())) + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: None, + }) + .with_extension_launch_provider(BlockedLaunchProvider { + entered, + release: release.clone(), + }) + .with_installation_confirmation_handler(ControlledHandler(send)), + ) + .await + .unwrap(); + + to_server + .send(json!({ + "jsonrpc": "2.0", "id": 901, "method": "extensionLaunchProvider.resolve", + "params": { + "id": "project:blocked", "modulePath": "/extensions/blocked/index.js", + "name": "Blocked", "source": "project" + } + })) + .unwrap(); + timeout(WAIT, resolve_entered.recv()) + .await + .unwrap() + .unwrap(); + to_server + .send(json!({ + "jsonrpc": "2.0", "id": 902, "method": "installations.confirm", + "params": request("behind-blocked-callback") + })) + .unwrap(); + let review = timeout(WAIT, reviews.recv()).await.unwrap().unwrap(); + assert_eq!(review.request.operation_id, "behind-blocked-callback"); + + to_server + .send(json!({ + "jsonrpc": "2.0", "method": "$/cancelRequest", "params": { "id": 902 } + })) + .unwrap(); + timeout(WAIT, review.context.cancellation().cancelled()) + .await + .unwrap(); + let response = timeout(WAIT, server_frames.recv()).await.unwrap().unwrap(); + assert_eq!(response["id"], 902); + assert_eq!(response["error"]["code"], -32800); + assert!( + review + .decision + .send(Ok(InstallationDecision::Confirm)) + .is_err() + ); + + // The blocked callback still completes in order, and 902 is never approved late. + release.notify_one(); + let response = timeout(WAIT, server_frames.recv()).await.unwrap().unwrap(); + assert_eq!(response["id"], 901); + assert_eq!(response["result"], json!({})); + drop(to_server); + timeout(WAIT, server).await.unwrap().unwrap(); + client.force_stop(); +} + +/// A host inference handler that never completes, so any await on it would stall routing. +struct HungRequestHandler(mpsc::UnboundedSender<()>); + +#[async_trait] +impl CopilotRequestHandler for HungRequestHandler { + async fn send_request( + &self, + _request: CopilotHttpRequest, + _context: &CopilotRequestContext, + ) -> std::result::Result { + self.0.send(()).unwrap(); + std::future::pending().await + } +} + +#[tokio::test] +async fn hung_llm_inference_request_does_not_delay_confirmation_or_its_cancellation() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let (send, mut reviews) = mpsc::unbounded_channel(); + let (entered, mut handler_entered) = mpsc::unbounded_channel(); + let (to_server, mut server_commands) = mpsc::unbounded_channel::(); + let (from_server, mut server_frames) = mpsc::unbounded_channel::(); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let (mut reader, mut writer) = stream.into_split(); + for method in ["connect", "llmInference.setProvider"] { + let message = read_frame(&mut reader).await; + assert_eq!(message["method"], method); + let result = if method == "connect" { + json!({ "ok": true, "protocolVersion": 3, "version": "test" }) + } else { + json!({ "success": true }) + }; + write_frame( + &mut writer, + json!({ "jsonrpc": "2.0", "id": message["id"], "result": result }), + ) + .await; + } + let forward = tokio::spawn(async move { + loop { + let frame = read_frame_untimed(&mut reader).await; + if from_server.send(frame).is_err() { + break; + } + } + }); + while let Some(frame) = server_commands.recv().await { + write_frame(&mut writer, frame).await; + } + forward.abort(); + }); + let client = Client::start( + ClientOptions::new() + .with_program(CliProgram::Path("unused-external-transport".into())) + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: None, + }) + .with_request_handler(HungRequestHandler(entered)) + .with_installation_confirmation_handler(ControlledHandler(send)), + ) + .await + .unwrap(); + + for (id, method, params) in [ + ( + 911, + "llmInference.httpRequestStart", + json!({ "requestId": "hung", "method": "GET", "url": "https://example.test/", "headers": {} }), + ), + ( + 912, + "llmInference.httpRequestChunk", + json!({ "requestId": "hung", "data": "", "end": true }), + ), + ] { + to_server + .send(json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })) + .unwrap(); + let ack = timeout(WAIT, server_frames.recv()).await.unwrap().unwrap(); + assert_eq!(ack["id"], id); + } + timeout(WAIT, handler_entered.recv()) + .await + .unwrap() + .unwrap(); + + to_server + .send(json!({ + "jsonrpc": "2.0", "id": 913, "method": "installations.confirm", + "params": request("behind-hung-inference") + })) + .unwrap(); + let review = timeout(WAIT, reviews.recv()).await.unwrap().unwrap(); + assert_eq!(review.request.operation_id, "behind-hung-inference"); + to_server + .send(json!({ + "jsonrpc": "2.0", "method": "$/cancelRequest", "params": { "id": 913 } + })) + .unwrap(); + timeout(WAIT, review.context.cancellation().cancelled()) + .await + .unwrap(); + let response = timeout(WAIT, server_frames.recv()).await.unwrap().unwrap(); + assert_eq!(response["id"], 913); + assert_eq!(response["error"]["code"], -32800); + assert!( + review + .decision + .send(Ok(InstallationDecision::Confirm)) + .is_err() + ); + + drop(to_server); + timeout(WAIT, server).await.unwrap().unwrap(); + client.force_stop(); +} + +#[test] +fn install_and_uninstall_reviews_both_round_trip() { + let install = request("install-review")["review"]["review"].clone(); + let mut uninstall = install.clone(); + let object = uninstall.as_object_mut().unwrap(); + for key in [ + "selectedChoice", + "configurationChange", + "inputs", + "suppliedSecrets", + "secretStorage", + "effectiveConfiguration", + ] { + object.remove(key); + } + object.insert("action".into(), json!("uninstall")); + object.insert("installationId".into(), json!("a".repeat(32))); + object.insert("restoresPreviousConfiguration".into(), json!(false)); + object.insert("ownedSecretCount".into(), json!(0)); + object.insert("preservesSharedAuthentication".into(), json!(true)); + + let parsed: McpInstallationReview = serde_json::from_value(install.clone()).unwrap(); + assert!(matches!(parsed, McpInstallationReview::Install(_))); + assert_eq!(serde_json::to_value(&parsed).unwrap(), install); + + let parsed: McpInstallationReview = serde_json::from_value(uninstall.clone()).unwrap(); + let McpInstallationReview::Uninstall(review) = &parsed else { + panic!("expected typed uninstall review"); + }; + assert_eq!(review.installation_id, "a".repeat(32)); + assert_eq!(serde_json::to_value(&parsed).unwrap(), uninstall); +} diff --git a/rust/tests/legacy_options_test.rs b/rust/tests/legacy_options_test.rs new file mode 100644 index 0000000000..9479be6e1e --- /dev/null +++ b/rust/tests/legacy_options_test.rs @@ -0,0 +1,121 @@ +// Existing request structs keep their published shape; inputs added later are only +// reachable through the private-field options types, which serialise flat. + +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::Error; +use github_copilot_sdk::rpc::{ + CatalogClientContract, CatalogSearchOptions, CatalogSearchRequest, CatalogSearchResult, + ClientRpcCatalog, ClientRpcMcp, McpPlanInstallOptions, McpPlanInstallRequest, + McpPlanInstallResult, McpPlanInstallSource, McpPlanInstallSourceCandidate, + McpPlanInstallSourceCandidateKind, McpPlanScope, +}; +use serde_json::json; + +fn contract() -> CatalogClientContract { + CatalogClientContract { + protocol_version: 3, + required_capabilities: vec!["mcp-install-planning".to_string()], + } +} + +fn candidate() -> McpPlanInstallSource { + McpPlanInstallSource::Candidate(McpPlanInstallSourceCandidate { + candidate_handle: "candidate".to_string(), + kind: McpPlanInstallSourceCandidateKind::Candidate, + search_id: "search".to_string(), + }) +} + +#[test] +fn existing_exhaustive_literals_keep_their_wire_shape() { + let plan = McpPlanInstallRequest { + contract: contract(), + scope: Some(McpPlanScope::User), + source: candidate(), + }; + let search = CatalogSearchRequest { + contract: contract(), + kinds: None, + limit: Some(4), + page: None, + query: "catalogue query".to_string(), + }; + + let plan = serde_json::to_value(plan).unwrap(); + let search = serde_json::to_value(search).unwrap(); + assert_eq!(plan["scope"], "user"); + assert!(plan.get("policySessionId").is_none()); + assert_eq!(search["limit"], 4); + assert!(search.get("policySessionId").is_none()); +} + +#[test] +fn options_serialise_the_legacy_request_and_additions_at_one_level() { + let plan = McpPlanInstallOptions::new(contract(), candidate()) + .scope(McpPlanScope::User) + .policy_session_id("session"); + let search = CatalogSearchOptions::new(contract(), "catalogue query".to_string()) + .limit(4) + .policy_session_id("session"); + + assert_eq!( + serde_json::to_value(plan).unwrap(), + json!({ + "contract": {"protocolVersion": 3, "requiredCapabilities": ["mcp-install-planning"]}, + "source": {"kind": "candidate", "candidateHandle": "candidate", "searchId": "search"}, + "scope": "user", + "policySessionId": "session", + }) + ); + assert_eq!( + serde_json::to_value(search).unwrap(), + json!({ + "contract": {"protocolVersion": 3, "requiredCapabilities": ["mcp-install-planning"]}, + "query": "catalogue query", + "limit": 4, + "policySessionId": "session", + }) + ); +} + +#[test] +fn options_omit_unset_inputs() { + let plan = serde_json::to_value(McpPlanInstallOptions::new(contract(), candidate())).unwrap(); + assert!(plan.get("scope").is_none()); + assert!(plan.get("policySessionId").is_none()); + assert!(plan.get("legacy").is_none()); +} + +// Both entry points exist on the same namespace with unchanged signatures. +#[allow(dead_code)] +async fn entry_points_compile( + mcp: ClientRpcMcp<'_>, + catalog: ClientRpcCatalog<'_>, +) -> Result<(McpPlanInstallResult, CatalogSearchResult), Error> { + mcp.plan_install(McpPlanInstallRequest { + contract: contract(), + scope: None, + source: candidate(), + }) + .await?; + catalog + .search(CatalogSearchRequest { + contract: contract(), + kinds: None, + limit: None, + page: None, + query: "catalogue query".to_string(), + }) + .await?; + let plan = mcp + .plan_install_with_options(McpPlanInstallOptions::new(contract(), candidate())) + .await?; + let search = catalog + .search_with_options(CatalogSearchOptions::new( + contract(), + "catalogue query".to_string(), + )) + .await?; + Ok((plan, search)) +} diff --git a/rust/tests/mcp_plan_transport_choice_test.rs b/rust/tests/mcp_plan_transport_choice_test.rs new file mode 100644 index 0000000000..ceb1bd1451 --- /dev/null +++ b/rust/tests/mcp_plan_transport_choice_test.rs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::rpc::{ + McpInstallPlan, McpPlanPackageInstallMethod, McpPlanRemoteInstallMethod, + McpPlanRemoteTransport, McpPlanTransportChoice, McpPlanTransportChoicePackage, + McpPlanTransportChoiceRemote, +}; +use serde_json::{Value, json}; + +fn package_choice() -> Value { + json!({ + "choiceId": "package-choice", + "installMethod": "package", + "packageIdentifier": "@example/mcp-server", + "packageType": "npm", + "transport": "stdio", + "requiredValues": [], + "secretPlaceholders": [] + }) +} + +fn remote_choice() -> Value { + json!({ + "choiceId": "remote-choice", + "installMethod": "remote", + "endpoint": "https://example.test/mcp", + "transport": "streamable-http", + "requiredValues": [], + "secretPlaceholders": [] + }) +} + +#[test] +fn plan_exposes_typed_package_and_remote_choices_without_changing_wire_shape() { + let wire = json!([package_choice(), remote_choice()]); + let choices: Vec = serde_json::from_value(wire.clone()).unwrap(); + let plan = McpInstallPlan { + transport_choices: choices, + ..Default::default() + }; + + let McpPlanTransportChoice::Package(package) = &plan.transport_choices[0] else { + panic!("expected package choice"); + }; + assert_eq!(package.choice_id, "package-choice"); + assert_eq!(package.package_identifier, "@example/mcp-server"); + assert_eq!(package.install_method, McpPlanPackageInstallMethod::Package); + + let McpPlanTransportChoice::Remote(remote) = &plan.transport_choices[1] else { + panic!("expected remote choice"); + }; + assert_eq!(remote.choice_id, "remote-choice"); + assert_eq!(remote.endpoint, "https://example.test/mcp"); + assert_eq!(remote.install_method, McpPlanRemoteInstallMethod::Remote); + assert_eq!(remote.transport, McpPlanRemoteTransport::StreamableHttp); + assert!(remote.required_values.is_empty()); + assert!(remote.secret_placeholders.is_empty()); + assert_eq!(serde_json::to_value(&plan.transport_choices).unwrap(), wire); +} + +#[test] +fn choices_preserve_declared_values_and_secret_placeholders() { + let mut wire = remote_choice(); + wire["requiredValues"] = json!([{ + "kind": "enum", + "key": "region", + "category": "url-variable", + "valueType": "enum", + "enumValues": ["eu", "us"], + "required": true, + "isRepeated": false, + "defaultValue": "eu", + "title": "Region" + }]); + wire["secretPlaceholders"] = json!([{ + "key": "token", + "placeholder": "${secret:opaque-id}", + "title": "Access token" + }]); + let choice: McpPlanTransportChoice = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(choice).unwrap(), wire); +} + +#[test] +fn unknown_missing_null_and_wrongly_typed_discriminators_are_rejected() { + for original in [package_choice(), remote_choice()] { + for discriminator in [ + None, + Some(Value::Null), + Some(json!("future-install-method")), + Some(json!(false)), + Some(json!(1)), + Some(json!({})), + ] { + let mut wire = original.clone(); + if let Some(discriminator) = discriminator { + wire["installMethod"] = discriminator; + } else { + wire.as_object_mut().unwrap().remove("installMethod"); + } + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "unexpectedly accepted {wire}" + ); + } + } +} + +#[test] +fn mismatched_discriminators_and_incomplete_payloads_are_rejected() { + let mut package = package_choice(); + package["installMethod"] = json!("remote"); + assert!(serde_json::from_value::(package.clone()).is_err()); + assert!(serde_json::from_value::(package).is_err()); + + let mut remote = remote_choice(); + remote["installMethod"] = json!("package"); + assert!(serde_json::from_value::(remote.clone()).is_err()); + assert!(serde_json::from_value::(remote).is_err()); + + for (original, fields) in [ + (package_choice(), vec!["packageType", "packageIdentifier"]), + (remote_choice(), vec!["endpoint"]), + ] { + for field in fields.into_iter().chain([ + "choiceId", + "transport", + "requiredValues", + "secretPlaceholders", + ]) { + for replacement in [None, Some(Value::Null), Some(json!(42))] { + let mut wire = original.clone(); + if let Some(replacement) = replacement { + wire[field] = replacement; + } else { + wire.as_object_mut().unwrap().remove(field); + } + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "unexpectedly accepted {wire}" + ); + } + } + } +} + +#[test] +fn unconstrained_enums_retain_forward_compatibility() { + assert_eq!( + serde_json::from_value::(json!("future-method")).unwrap(), + McpPlanPackageInstallMethod::Unknown + ); + assert_eq!( + serde_json::from_value::(json!("future-method")).unwrap(), + McpPlanRemoteInstallMethod::Unknown + ); + + let mut wire = remote_choice(); + wire["transport"] = json!("future-transport"); + let McpPlanTransportChoice::Remote(remote) = + serde_json::from_value::(wire).unwrap() + else { + panic!("expected remote choice"); + }; + assert_eq!(remote.transport, McpPlanRemoteTransport::Unknown); +} + +#[test] +fn extra_fields_retain_existing_struct_compatibility() { + let original = remote_choice(); + let mut wire = original.clone(); + wire["futureMetadata"] = json!({"version": 2}); + let choice: McpPlanTransportChoice = serde_json::from_value(wire).unwrap(); + assert_eq!(serde_json::to_value(choice).unwrap(), original); +} 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/protocol_version_test.rs b/rust/tests/protocol_version_test.rs index cd8563f87b..8903f6fe49 100644 --- a/rust/tests/protocol_version_test.rs +++ b/rust/tests/protocol_version_test.rs @@ -3,6 +3,204 @@ use github_copilot_sdk::Client; use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt, duplex}; +#[tokio::test] +async fn dropping_external_client_closes_its_streams() { + let (client_write, mut server_read) = duplex(8192); + let (_server_write, client_read) = duplex(8192); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + client + .register_request_handler("host.shutdown", |_, client| async move { + client.call("ping", None).await + }) + .unwrap(); + drop(client); + let mut byte = [0]; + let count = tokio::time::timeout( + std::time::Duration::from_secs(2), + server_read.read(&mut byte), + ) + .await + .unwrap() + .unwrap(); + assert_eq!(count, 0); +} + +#[tokio::test] +async fn connection_handler_can_await_sdk_replies_before_responding() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + client + .register_request_handler("host.shutdown", |_, client| async move { + client.call("ping", None).await + }) + .unwrap(); + let request = serde_json::json!({"jsonrpc":"2.0","id":41,"method":"host.shutdown","params":{}}); + write_framed(&mut server_write, &serde_json::to_vec(&request).unwrap()).await; + let ping = tokio::time::timeout( + std::time::Duration::from_secs(2), + read_framed(&mut server_read), + ) + .await + .unwrap(); + assert_eq!(ping["method"], "ping"); + let response = serde_json::json!({"jsonrpc":"2.0","id":ping["id"],"result":{"drained":true}}); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + let shutdown = tokio::time::timeout( + std::time::Duration::from_secs(2), + read_framed(&mut server_read), + ) + .await + .unwrap(); + assert_eq!(shutdown["id"], 41); + assert_eq!(shutdown["result"], serde_json::json!({"drained":true})); + drop(client); + let mut byte = [0]; + assert_eq!( + tokio::time::timeout( + std::time::Duration::from_secs(2), + server_read.read(&mut byte), + ) + .await + .unwrap() + .unwrap(), + 0 + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn synchronous_handler_setup_does_not_block_the_reader() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let client = Client::from_streams(client_read, client_write, ".".into()).unwrap(); + let (started_tx, mut started) = tokio::sync::mpsc::unbounded_channel(); + let (unblock, gate) = std::sync::mpsc::channel(); + let gate = std::sync::Mutex::new(gate); + client + .register_request_handler("blocking.setup", move |_, _| { + started_tx.send(()).unwrap(); + gate.lock() + .unwrap() + .recv_timeout(std::time::Duration::from_secs(10)) + .unwrap(); + async { Ok(serde_json::json!({})) } + }) + .unwrap(); + let request = serde_json::json!({"jsonrpc":"2.0","id":44,"method":"blocking.setup"}); + write_framed(&mut server_write, &serde_json::to_vec(&request).unwrap()).await; + tokio::time::timeout(std::time::Duration::from_secs(2), started.recv()) + .await + .unwrap() + .unwrap(); + let ping = tokio::spawn({ + let client = client.clone(); + async move { client.call("ping", None).await } + }); + let request = read_framed(&mut server_read).await; + let response = serde_json::json!({"jsonrpc":"2.0","id":request["id"],"result":{}}); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + tokio::time::timeout(std::time::Duration::from_secs(2), ping) + .await + .unwrap() + .unwrap() + .unwrap(); + unblock.send(()).unwrap(); + assert_eq!(read_framed(&mut server_read).await["id"], 44); + client.force_stop(); +} + +#[tokio::test] +async fn connection_handlers_reject_duplicate_registration_and_report_errors() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + client + .register_request_handler("host.shutdown", |_, _| async { + Err(github_copilot_sdk::Error::with_message( + github_copilot_sdk::ErrorKind::InvalidConfig, + "drain failed", + )) + }) + .unwrap(); + assert!( + client + .register_request_handler("host.shutdown", |_, _| async { Ok(serde_json::json!({})) }) + .is_err() + ); + assert!( + client + .register_request_handler("", |_, _| async { Ok(serde_json::json!({})) }) + .is_err() + ); + let request = serde_json::json!({"jsonrpc":"2.0","id":42,"method":"host.shutdown","params":{}}); + write_framed(&mut server_write, &serde_json::to_vec(&request).unwrap()).await; + let response = tokio::time::timeout( + std::time::Duration::from_secs(2), + read_framed(&mut server_read), + ) + .await + .unwrap(); + assert_eq!(response["error"]["code"], -32603); + assert_eq!(response["error"]["message"], "drain failed"); + client.force_stop(); +} + +#[tokio::test] +async fn connection_eof_cancels_inbound_handlers() { + use std::sync::Arc; + + use tokio::sync::Notify; + + struct OnDrop(Arc); + impl Drop for OnDrop { + fn drop(&mut self) { + self.0.notify_one(); + } + } + let (client_write, _server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + let started = Arc::new(Notify::new()); + let cancelled = Arc::new(Notify::new()); + let callback_started = started.clone(); + let callback_cancelled = cancelled.clone(); + client + .register_request_handler("host.shutdown", move |_, client| { + let started = callback_started.clone(); + let cancelled = callback_cancelled.clone(); + async move { + let _guard = OnDrop(cancelled); + let _client = client; + started.notify_one(); + std::future::pending().await + } + }) + .unwrap(); + let request = serde_json::json!({"jsonrpc":"2.0","id":43,"method":"host.shutdown","params":{}}); + write_framed(&mut server_write, &serde_json::to_vec(&request).unwrap()).await; + tokio::time::timeout(std::time::Duration::from_secs(2), started.notified()) + .await + .unwrap(); + drop(client); + server_write.shutdown().await.unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(2), cancelled.notified()) + .await + .unwrap(); +} + +#[cfg(not(feature = "runtime"))] +#[tokio::test] +async fn external_stream_build_cannot_launch_or_discover_a_runtime() { + let error = Client::start(github_copilot_sdk::ClientOptions::default()) + .await + .unwrap_err(); + assert!( + error + .to_string() + .contains("requires the `runtime` Cargo feature") + ); +} + async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), body: &[u8]) { let header = format!("Content-Length: {}\r\n\r\n", body.len()); writer.write_all(header.as_bytes()).await.unwrap(); diff --git a/rust/tests/runtime_host_e2e.rs b/rust/tests/runtime_host_e2e.rs new file mode 100644 index 0000000000..fab6e8724d --- /dev/null +++ b/rust/tests/runtime_host_e2e.rs @@ -0,0 +1,567 @@ +//! Real runtime-supervised AHP listener tests. See scripts/runtime-host-e2e.md. +#![cfg(all(feature = "test-support", target_os = "linux"))] +#![allow(clippy::unwrap_used)] + +#[path = "e2e/runtime_host_support.rs"] +mod host_support; +#[allow(dead_code)] +#[path = "e2e/support.rs"] +mod support; + +#[path = "e2e/runtime_host_callbacks.rs"] +mod callbacks; + +use std::time::Duration; + +use ahp_ws::WebSocketTransport; +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}; + +#[tokio::test] +#[ignore = "requires final locally built runtime/provider; source and candidate instructions in scripts/runtime-host-e2e.md"] +#[serial_test::serial] +async fn streams_real_ahp_turn_beside_sdk_session_on_same_runtime() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let sdk = owner + .create_session( + ctx.approve_all_session_config() + .with_model("claude-sonnet-5") + .with_streaming(true), + ) + .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")); + let (_, response) = + tokio::join!(turn(&ahp, &chat, subscription), sdk.send_and_wait(PROMPT)); + assert!( + response.unwrap().unwrap().data["content"] + .as_str() + .unwrap() + .contains('4') + ); + let id = uri.strip_prefix("ahp-session:/").unwrap(); + // Turn completion is not a persistence acknowledgement: event writes + // are debounced, and list_sessions lists persisted sessions. + let sessions = tokio::time::timeout(Duration::from_secs(10), async { + loop { + let sessions = owner.list_sessions(None).await.unwrap(); + if sessions + .iter() + .any(|session| session.session_id.as_str() == id) + && sessions + .iter() + .any(|session| session.session_id == sdk.id()) + { + return sessions; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("persisted session list must include both completed AHP and SDK sessions"); + assert!( + sessions + .iter() + .any(|session| session.session_id.as_str() == id) + ); + assert!( + sessions + .iter() + .any(|session| session.session_id == sdk.id()) + ); + let observer = owner + .resume_session( + ResumeSessionConfig::new(SessionId::from(id)).with_permission_handler( + std::sync::Arc::new(github_copilot_sdk::handler::ApproveAllHandler), + ), + ) + .await + .unwrap(); + assert!(observer.get_events().await.unwrap().iter().any(|event| { + event.event_type == "assistant.message" + && event.data["content"] + .as_str() + .is_some_and(|text| text.contains('4')) + })); + host.dispose().await.unwrap(); + stopped(&host, &ahp, &owner).await; + assert!(!sdk.get_events().await.unwrap().is_empty()); + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn durable_catalog_resumes_after_repeated_listener_disposal() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let sdk = owner + .create_session(ctx.approve_all_session_config()) + .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; + host.dispose().await.unwrap(); + stopped(&host, &ahp, &owner).await; + ahp.client.shutdown().await; + let (options, exits) = exit_observer(); + let replacement = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&replacement).await; + resume(&ahp, &uri, Some(sdk.id().as_str())).await; + replacement.dispose().await.unwrap(); + assert_eq!(exit(&exits).await.reason, HostExitReason::Disposed); + stopped(&replacement, &ahp, &owner).await; + ahp.client.shutdown().await; + 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(); + recovered.dispose().await.unwrap(); + stopped(&recovered, &ahp, &owner).await; + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn explicit_base_directory_survives_runtime_restart_and_excludes_other_writer() { + run(|ctx| { + Box::pin(async move { + let base = home(ctx).join("explicit-base"); + std::fs::create_dir_all(&base).unwrap(); + let first = Client::start(options_with_base_directory(ctx, &base)) + .await + .unwrap(); + let second = Client::start(options_with_base_directory(ctx, &base)) + .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; + turn(&ahp, &chat, subscription).await; + let sdk = second + .create_session(ctx.approve_all_session_config()) + .await + .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"), + "{error}" + ); + sdk.get_events().await.unwrap(); + ahp.client.ping().await.unwrap(); + host.dispose().await.unwrap(); + stopped(&host, &ahp, &first).await; + ahp.client.shutdown().await; + let old_pid = first.pid().unwrap(); + first.stop().await.unwrap(); + second.stop().await.unwrap(); + reaped(old_pid).await; + let restarted = Client::start(options_with_base_directory(ctx, &base)) + .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(); + stopped(&host, &ahp, &restarted).await; + ahp.client.shutdown().await; + restarted.stop().await.unwrap(); + }) + }) + .await; +} + +fn unused_port() -> u16 { + std::net::TcpListener::bind(("127.0.0.1", 0)) + .unwrap() + .local_addr() + .unwrap() + .port() +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn same_catalog_and_other_owner_rejected_disconnect_cleans_up_without_stopping_sdk() { + run(|ctx| { + Box::pin(async move { + let port = unused_port(); + let owner = Client::start(options(ctx).with_transport(Transport::Tcp { + port, + connection_token: Some(RUNTIME_TOKEN.into()), + })) + .await + .unwrap(); + let sdk = owner + .create_session(ctx.approve_all_session_config()) + .await + .unwrap(); + // Startup resolves the program before selecting the external transport; + // use the attested artifact, not ambient CLI discovery. + let other = Client::start(options(ctx).with_transport(Transport::External { + host: "127.0.0.1".into(), + port, + connection_token: Some(RUNTIME_TOKEN.into()), + })) + .await + .unwrap(); + let (options, exits) = exit_observer(); + let host = other.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + 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(local_options()).await.err().unwrap(); + assert!( + error.to_string().contains("catalog") + && error.to_string().contains("already in use"), + "{error}" + ); + assert!( + owner + .rpc() + .host() + .dispose(HostDisposeRequest { + host_id: host.host_id.clone() + }) + .await + .is_err() + ); + ahp.client.ping().await.unwrap(); + assert_eq!(other.pid(), None); + // Abruptly close only the external owner's transport: no host.dispose, + // runtime.shutdown, or owned child process is involved. + other.force_stop(); + let observed = exit(&exits).await; + assert_eq!(observed.host_id, host.host_id); + assert_eq!(observed.reason, HostExitReason::OwnerDisconnected); + assert_eq!(observed.exit_code, None); + assert!( + observed + .error + .as_deref() + .unwrap() + .contains("runtime cleanup cannot be acknowledged") + ); + stopped_after_owner_disconnect(&host, &ahp, &owner).await; + ahp.client.shutdown().await; + sdk.get_events().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(); + stopped(&replacement, &ahp, &owner).await; + ahp.client.shutdown().await; + owner + .create_session(ctx.approve_all_session_config()) + .await + .unwrap(); + assert_eq!(exits.lock().unwrap().len(), 1); + owner.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn listener_startup_failure_recovers_and_preserves_owner_session() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let sdk = owner + .create_session(ctx.approve_all_session_config()) + .await + .unwrap(); + let (options, exits) = exit_observer(); + let blocker = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = blocker.local_addr().unwrap().port(); + assert!( + owner + .start_ahp_host(AhpHostOptions::default().with_local_server( + HostLocalServerOptions { + port: Some(port.into()), + ..Default::default() + } + )) + .await + .is_err() + ); + sdk.get_events().await.unwrap(); + drop(blocker); + // Listener recovery is not process isolation: it runs inside the runtime. + let host = owner + .start_ahp_host(options.with_local_server(HostLocalServerOptions { + port: Some(port.into()), + ..Default::default() + })) + .await + .unwrap(); + let ahp = connect(&host).await; + topology(&host, &owner, &home(ctx).join("ahp/sessions")); + host.dispose().await.unwrap(); + let observed = exit(&exits).await; + assert_eq!(observed.host_id, host.host_id); + assert_eq!(observed.reason, HostExitReason::Disposed); + assert_eq!(observed.exit_code, None); + stopped(&host, &ahp, &owner).await; + sdk.get_events().await.unwrap(); + host.dispose().await.unwrap(); + host.dispose().await.unwrap(); + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + assert_eq!(exits.lock().unwrap().len(), 1); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn direct_rpc_concurrent_repeated_dispose_closes_listener_and_notifies_once() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let sdk = owner + .create_session(ctx.approve_all_session_config()) + .await + .unwrap(); + let (options, exits) = exit_observer(); + let host = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + let request = HostDisposeRequest { + host_id: host.host_id.clone(), + }; + let rpc = owner.rpc().host(); + let (first, second) = + tokio::join!(rpc.dispose(request.clone()), rpc.dispose(request.clone())); + first.unwrap(); + second.unwrap(); + assert_eq!(exit(&exits).await.reason, HostExitReason::Disposed); + stopped(&host, &ahp, &owner).await; + rpc.dispose(request).await.unwrap(); + host.dispose().await.unwrap(); + rpc.dispose(HostDisposeRequest { + host_id: uuid::Uuid::new_v4().to_string(), + }) + .await + .unwrap(); + sdk.get_events().await.unwrap(); + owner + .create_session(ctx.approve_all_session_config()) + .await + .unwrap(); + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + assert_eq!(exits.lock().unwrap().len(), 1); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +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(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.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_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.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]")), + _ => assert!(matches!(url.host_str(), Some("127.0.0.1" | "[::1]"))), + } + assert!(url.port().unwrap() > 0); + if port != 0 { + assert_eq!(url.port(), Some(port)); + } + assert_eq!(host.token.as_deref(), Some("rust-supplied-token")); + if hostname == "0.0.0.0" { + url.set_host(Some("127.0.0.1")).unwrap(); + } + assert!( + deadline(WebSocketTransport::connect(url.as_str())) + .await + .is_err() + ); + let mut wrong_token = url.clone(); + wrong_token + .query_pairs_mut() + .append_pair("tkn", "wrong-token"); + assert!( + deadline(WebSocketTransport::connect(wrong_token.as_str())) + .await + .is_err() + ); + let ahp = connect_url(url.as_str(), host.token.as_deref()).await; + ahp.client.ping().await.unwrap(); + topology(&host, &owner, &home(ctx).join("ahp/sessions")); + host.dispose().await.unwrap(); + stopped(&host, &ahp, &owner).await; + ahp.client.shutdown().await; + } + for invalid in [ + json!({"token": ""}), + json!({"token": "token", "requireConnectionToken": false}), + json!({"port": -1}), + json!({"port": 65536}), + json!({"port": 1.5}), + json!({"hostname": ""}), + ] { + 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(local_options()).await.unwrap(); + host.dispose().await.unwrap(); + owner.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn disabled_connection_token_still_requires_ahp_resource_authentication() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let host = owner + .start_ahp_host(AhpHostOptions::default().with_local_server( + HostLocalServerOptions { + require_connection_token: Some(false), + ..Default::default() + }, + )) + .await + .unwrap(); + assert_eq!(host.token, None); + let ahp = connect(&host).await; + ahp.client.ping().await.unwrap(); + let file = ctx.work_dir().join("ahp-resource.txt"); + std::fs::write(&file, "workspace data").unwrap(); + let resource = json!({ + "channel": ROOT, + "uri": reqwest::Url::from_file_path(&file).unwrap().as_str() + }); + match ahp + .client + .request::<_, Value>("resourceRead", resource.clone()) + .await + { + Err(ahp::ClientError::Rpc(error)) => { + assert_eq!( + error.code, + ahp_types::errors::ahp_error_codes::AUTH_REQUIRED + ); + } + other => panic!("expected resourceRead AuthRequired, got {other:?}"), + } + let uri = format!("ahp-session:/{}", uuid::Uuid::new_v4()); + let unauthorized = ahp + .client + .request::<_, Value>("createSession", create_params(&ahp, ctx, &uri)) + .await; + match unauthorized { + Err(ahp::ClientError::Rpc(error)) => { + assert_eq!( + error.code, + ahp_types::errors::ahp_error_codes::AUTH_REQUIRED + ); + } + other => panic!("expected AHP resource authentication requirement, got {other:?}"), + } + let (_, chat, subscription) = create(&ahp, ctx).await; + let read: Value = ahp.client.request("resourceRead", resource).await.unwrap(); + assert_eq!(read["data"], "workspace data"); + turn(&ahp, &chat, subscription).await; + host.dispose().await.unwrap(); + stopped(&host, &ahp, &owner).await; + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +#[ignore = "requires local runtime host artifacts"] +#[serial_test::serial] +async fn graceful_runtime_shutdown_closes_attached_ahp_session_and_reaps_runtime() { + run(|ctx| { + Box::pin(async move { + let owner = start(ctx).await; + let (options, exits) = exit_observer(); + let host = owner.start_ahp_host(options).await.unwrap(); + let ahp = connect(&host).await; + create(&ahp, ctx).await; + let runtime_pid = owner.pid().unwrap(); + topology(&host, &owner, &home(ctx).join("ahp/sessions")); + deadline(owner.rpc().runtime().shutdown()).await.unwrap(); + let observed = exit(&exits).await; + assert_eq!(observed.reason, HostExitReason::RuntimeShutdown); + assert_eq!(observed.exit_code, None); + assert_eq!(observed.error, None); + listener_closed(&host, &ahp).await; + ahp.client.shutdown().await; + owner.stop().await.unwrap(); + reaped(runtime_pid).await; + tokio::time::sleep(Duration::from_millis(50)).await; + assert_eq!(exits.lock().unwrap().len(), 1); + }) + }) + .await; +} diff --git a/rust/tests/session_events_test.rs b/rust/tests/session_events_test.rs index aabdd7f4f4..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", @@ -128,6 +145,15 @@ fn sandbox_decision_variants_round_trip_full_event_envelopes() { "command": "cat fixtures/restricted.txt", "processName": "cat" }), + serde_json::json!({ + "kind": "access_recorded", + "control": "filesystem", + "outcome": "allowed", + "denialClass": "filesystem_write", + "permissiveSource": "policy", + "deniedResource": "fixtures/probe.txt", + "command": "touch fixtures/probe.txt" + }), serde_json::json!({ "kind": "bypass_decided", "control": "bypass", @@ -177,6 +203,7 @@ fn sandbox_decision_variants_round_trip_full_event_envelopes() { SandboxDecisionData::SpawnCompleted(_) => "spawn_completed", SandboxDecisionData::EnforcementState(_) => "enforcement_state", SandboxDecisionData::AccessDenied(_) => "access_denied", + SandboxDecisionData::AccessRecorded(_) => "access_recorded", SandboxDecisionData::BypassDecided(_) => "bypass_decided", SandboxDecisionData::PermissiveRetryDecided(_) => "permissive_retry_decided", SandboxDecisionData::PermissiveRetryCompleted(_) => "permissive_retry_completed", @@ -207,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 bca042b634..f8edcc3fe6 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -18,11 +18,12 @@ use github_copilot_sdk::handler::{ PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, }; use github_copilot_sdk::rpc::{ - CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - ConnectorAccountRequest, ConnectorAvailability, ConnectorCapabilities, ConnectorCatalogResult, - ConnectorCatalogStatus, ConnectorConnectRequest, ConnectorConnectResult, - ConnectorContinueRequest, ConnectorDisconnectResult, ConnectorMcpStatus, - ConnectorReconcileRequest, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance, + AuthInfoType, CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, + CanvasProviderOpenResult, ConnectorAccountRequest, ConnectorAvailability, + ConnectorCapabilities, ConnectorCatalogResult, ConnectorCatalogStatus, ConnectorConnectRequest, + ConnectorConnectResult, ConnectorContinueRequest, ConnectorDisconnectResult, + ConnectorMcpStatus, ConnectorReconcileOptions, ConnectorReconcileRequest, + ConnectorSessionAccount, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance, SendAgentMode, SendMode, SendRequest, SessionRpcConnectors, }; use github_copilot_sdk::session_events::{ @@ -1466,8 +1467,8 @@ async fn create_session_forwards_refresh_custom_instructions_only_when_set() { if let Some(refresh) = refresh { config = config.with_refresh_custom_instructions(refresh); } - let create = client.create_session(config); - let server = async { + let create = Box::pin(client.create_session(config)); + let server = Box::pin(async { let request = read_framed(&mut server_read).await; assert_eq!(request["method"], "session.create"); assert_eq!( @@ -1482,9 +1483,9 @@ async fn create_session_forwards_refresh_custom_instructions_only_when_set() { "result": { "sessionId": session_id }, }); write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; - }; + }); - let (session, ()) = timeout(TIMEOUT, async { tokio::join!(create, server) }) + let (session, ()) = timeout(TIMEOUT, Box::pin(async { tokio::join!(create, server) })) .await .unwrap(); session.unwrap(); @@ -1796,7 +1797,8 @@ async fn create_and_resume_send_managed_settings_permissions() { .with_disable_bypass_permissions_mode(DisableBypassPermissionsModes::ALLOW_AUTO_ONLY) .with_deny(vec!["shell(rm*)".to_string()]) .with_ask(vec!["write".to_string()]) - .with_allow(vec![]), + .with_allow(vec![]) + .with_limit_to(vec![]), ); let create_handle = tokio::spawn({ @@ -1822,6 +1824,7 @@ async fn create_and_resume_send_managed_settings_permissions() { assert_eq!(perms["deny"][0], "shell(rm*)"); assert_eq!(perms["ask"][0], "write"); assert_eq!(perms["allow"], serde_json::json!([])); + assert_eq!(perms["limitTo"], serde_json::json!([])); let id = request["id"].as_u64().unwrap(); let session_id = requested_session_id(&request).to_string(); @@ -5361,6 +5364,89 @@ async fn elicitation_requested_cancels_on_handler_error() { assert_eq!(rpc_call["params"]["result"]["action"], "cancel"); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn nested_handler_panics_still_send_cancellation_replies() { + struct PanickingHandler; + + #[async_trait] + impl ElicitationHandler for PanickingHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: ElicitationRequest, + ) -> ElicitationResult { + // Test cancellation after unwinding, not progress of the panic diagnostic sink. + std::panic::resume_unwind(Box::new("test elicitation handler panic")); + } + } + + #[async_trait] + impl McpAuthHandler for PanickingHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: McpAuthRequest, + ) -> McpAuthResult { + std::panic::resume_unwind(Box::new("test MCP-auth handler panic")); + } + } + + let (client, read, write) = make_client(); + let mut server = FakeServer { + read, + write, + session_id: String::new(), + }; + let create = tokio::spawn(async move { + client + .create_session( + SessionConfig::default() + .with_elicitation_handler(Arc::new(PanickingHandler)) + .with_mcp_auth_handler(Arc::new(PanickingHandler)), + ) + .await + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + server.session_id = requested_session_id(&request).to_string(); + server + .respond( + &request, + serde_json::json!({ "sessionId": server.session_id }), + ) + .await; + let interest = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(interest["method"], "session.eventLog.registerInterest"); + server.respond(&interest, serde_json::json!({})).await; + let session = timeout(TIMEOUT, create).await.unwrap().unwrap().unwrap(); + + for (event, data, method, result) in [ + ( + "elicitation.requested", + serde_json::json!({ "requestId": "panic-elicitation", "message": "Confirm" }), + "session.ui.handlePendingElicitation", + serde_json::json!({ "action": "cancel" }), + ), + ( + "mcp.oauth_required", + serde_json::json!({ + "requestId": "panic-mcp-auth", "reason": "initial", + "serverName": "panic-server", "serverUrl": "https://example.com/mcp", + }), + "session.mcp.oauth.handlePendingRequest", + serde_json::json!({ "kind": "cancelled" }), + ), + ] { + server.send_event(event, data).await; + let reply = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(reply["method"], method); + assert_eq!(reply["params"]["result"], result); + server.respond(&reply, serde_json::json!({})).await; + } + session.stop_event_loop().await; +} + #[tokio::test] async fn external_tool_requested_dispatches_to_handler_and_responds() { struct RunTestsTool; @@ -5677,185 +5763,627 @@ async fn external_tool_broadcast_for_unknown_tool_is_not_responded_to() { ); } -#[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 - // resolved by a hook, the client must not respond again. - let (_session, mut server) = create_session_pair_with_config(|cfg| { - cfg.with_permission_handler(Arc::new(ApproveAllHandler)) - }) - .await; - server - .send_event( - "permission.requested", - serde_json::json!({ - "requestId": "req-hooked", - "sessionId": server.session_id, - "resolvedByHook": true, - "permissionRequest": { "kind": "shell" }, - }), - ) - .await; +/// Answers every call with its label, so a test can tell which handler served it. +struct LabelTool(&'static str); - let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; - assert!( - res.is_err(), - "expected no RPC when resolvedByHook=true, got: {:?}", - res.ok() - ); +#[async_trait] +impl tool::ToolHandler for LabelTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + Ok(ToolResult::Text(self.0.to_string())) + } } -#[tokio::test] -async fn permission_broadcast_with_no_claiming_handler_is_not_responded_to() { - // Phase H: a handler that doesn't claim permission dispatch must not - // respond — the SDK lets other connected clients handle the request. - let (_session, mut server) = create_session_pair().await; +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( - "permission.requested", + "external_tool.requested", serde_json::json!({ - "requestId": "req-pending", + "requestId": request_id, "sessionId": server.session_id, - "permissionRequest": { "kind": "shell" }, + "toolCallId": format!("call-{request_id}"), + "toolName": tool_name, + "arguments": {}, }), ) .await; - - let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; - assert!( - res.is_err(), - "expected no RPC when handler doesn't claim permission dispatch, got: {:?}", - res.ok() - ); } -#[tokio::test] -async fn elicitation_broadcast_with_no_claiming_handler_is_not_responded_to() { - // Phase H: same gating for elicitation. The default handler doesn't - // claim elicitation, so broadcasts are silently dropped. - let (_session, mut server) = create_session_pair_with_config(|cfg| { - cfg.with_permission_handler(Arc::new(ApproveAllHandler)) - }) - .await; - server - .send_event( - "elicitation.requested", - serde_json::json!({ - "requestId": "elicit-silent", - "message": "should not be answered", - }), - ) - .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() +} - let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; - assert!( - res.is_err(), - "expected no RPC when handler doesn't claim elicitation, got: {:?}", - res.ok() - ); +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 capabilities_captured_from_create_response() { - let (client, mut server_read, mut server_write) = make_client(); +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 create_handle = tokio::spawn({ - let client = client.clone(); + let replace = tokio::spawn({ + let session = session.clone(); async move { - client - .create_session(SessionConfig::default()) + session + .set_tools([ + label_tool("new", "new handler"), + Tool::new("declared").with_description("Declared only"), + ]) .await - .unwrap() } }); + 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(); - let request = read_framed(&mut server_read).await; - let id = request["id"].as_u64().unwrap(); - let session_id = requested_session_id(&request); - let response = serde_json::json!({ - "jsonrpc": "2.0", - "id": id, - "result": { - "sessionId": session_id, - "capabilities": { - "ui": { "elicitation": true } - } - }, - }); - write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + request_external_tool(&mut server, "req-new", "new").await; + assert_eq!( + read_tool_result(&mut server, "req-new").await, + "new handler" + ); - let session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); - let caps = session.capabilities(); - assert_eq!(caps.ui.as_ref().unwrap().elicitation, Some(true)); + // 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; } -#[tokio::test] -async fn capabilities_changed_event_updates_session() { - let (session, mut server) = create_session_pair().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); - // Initially no capabilities (create_session_pair doesn't send them) - assert!(session.capabilities().ui.is_none()); + 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"); - // CLI sends capabilities.changed event - server - .send_event( - "capabilities.changed", - serde_json::json!({ - "ui": { "elicitation": true } - }), - ) - .await; + // 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; - // Poll until the event loop processes the notification - let caps = timeout(TIMEOUT, async { - loop { - let caps = session.capabilities(); - if caps.ui.is_some() { - return caps; + // 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(()); } - tokio::time::sleep(Duration::from_millis(5)).await; + self.release.notified().await; + Ok(ToolResult::Text("old lookup".to_string())) } - }) - .await - .expect("capabilities should update within timeout"); + } - assert_eq!(caps.ui.as_ref().unwrap().elicitation, Some(true)); -} + 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); -#[tokio::test] -async fn request_elicitation_sent_in_create_params() { - let (client, mut server_read, mut server_write) = make_client(); + request_external_tool(&mut server, "req-running", "lookup").await; + timeout(TIMEOUT, started_rx).await.unwrap().unwrap(); - let create_handle = tokio::spawn({ - let client = client.clone(); + let replace = tokio::spawn({ + let session = session.clone(); async move { - client - .create_session( - SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler)), - ) + session + .set_tools([label_tool("lookup", "new lookup")]) .await - .unwrap() } }); + 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(); - let request = read_framed(&mut server_read).await; - assert_eq!(request["method"], "session.create"); - // ApproveAllHandler claims permission dispatch only; no other handlers - // are installed, so the wire flags reflect that exact responsibility. - assert_eq!(request["params"]["requestPermission"], true); - assert_eq!(request["params"]["requestElicitation"], false); - assert_eq!(request["params"]["requestExitPlanMode"], false); - assert_eq!(request["params"]["requestAutoModeSwitch"], false); + request_external_tool(&mut server, "req-after", "lookup").await; + assert_eq!( + read_tool_result(&mut server, "req-after").await, + "new lookup" + ); - let id = request["id"].as_u64().unwrap(); - let session_id = requested_session_id(&request); - let response = serde_json::json!({ - "jsonrpc": "2.0", - "id": id, - "result": { "sessionId": session_id }, - }); - write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; - timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); + 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 + // resolved by a hook, the client must not respond again. + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "req-hooked", + "sessionId": server.session_id, + "resolvedByHook": true, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + + let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; + assert!( + res.is_err(), + "expected no RPC when resolvedByHook=true, got: {:?}", + res.ok() + ); +} + +#[tokio::test] +async fn permission_broadcast_with_no_claiming_handler_is_not_responded_to() { + // Phase H: a handler that doesn't claim permission dispatch must not + // respond — the SDK lets other connected clients handle the request. + let (_session, mut server) = create_session_pair().await; + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "req-pending", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + + let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; + assert!( + res.is_err(), + "expected no RPC when handler doesn't claim permission dispatch, got: {:?}", + res.ok() + ); +} + +#[tokio::test] +async fn elicitation_broadcast_with_no_claiming_handler_is_not_responded_to() { + // Phase H: same gating for elicitation. The default handler doesn't + // claim elicitation, so broadcasts are silently dropped. + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + server + .send_event( + "elicitation.requested", + serde_json::json!({ + "requestId": "elicit-silent", + "message": "should not be answered", + }), + ) + .await; + + let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; + assert!( + res.is_err(), + "expected no RPC when handler doesn't claim elicitation, got: {:?}", + res.ok() + ); +} + +#[tokio::test] +async fn capabilities_captured_from_create_response() { + let (client, mut server_read, mut server_write) = make_client(); + + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session(SessionConfig::default()) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + let id = request["id"].as_u64().unwrap(); + let session_id = requested_session_id(&request); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "sessionId": session_id, + "capabilities": { + "ui": { "elicitation": true } + } + }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + let session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); + let caps = session.capabilities(); + assert_eq!(caps.ui.as_ref().unwrap().elicitation, Some(true)); +} + +#[tokio::test] +async fn capabilities_changed_event_updates_session() { + let (session, mut server) = create_session_pair().await; + + // Initially no capabilities (create_session_pair doesn't send them) + assert!(session.capabilities().ui.is_none()); + + // CLI sends capabilities.changed event + server + .send_event( + "capabilities.changed", + serde_json::json!({ + "ui": { "elicitation": true } + }), + ) + .await; + + // Poll until the event loop processes the notification + let caps = timeout(TIMEOUT, async { + loop { + let caps = session.capabilities(); + if caps.ui.is_some() { + return caps; + } + tokio::time::sleep(Duration::from_millis(5)).await; + } + }) + .await + .expect("capabilities should update within timeout"); + + assert_eq!(caps.ui.as_ref().unwrap().elicitation, Some(true)); +} + +#[tokio::test] +async fn request_elicitation_sent_in_create_params() { + let (client, mut server_read, mut server_write) = make_client(); + + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.create"); + // ApproveAllHandler claims permission dispatch only; no other handlers + // are installed, so the wire flags reflect that exact responsibility. + assert_eq!(request["params"]["requestPermission"], true); + assert_eq!(request["params"]["requestElicitation"], false); + assert_eq!(request["params"]["requestExitPlanMode"], false); + assert_eq!(request["params"]["requestAutoModeSwitch"], false); + + let id = request["id"].as_u64().unwrap(); + let session_id = requested_session_id(&request); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "sessionId": session_id }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); } #[tokio::test] @@ -6370,10 +6898,94 @@ async fn hooks_invoke_returns_empty_for_unregistered_hook() { }), ) .await; - + + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], 301); + 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["id"], 301); - assert_eq!(response["result"]["output"], serde_json::json!({})); + 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( @@ -6573,7 +7185,9 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { "maxDeadlineMs": 60_000, "maxPollAttempts": 10, "maxPollIntervalMs": 5_000, - "opaqueAccountSelection": true + "opaqueAccountSelection": true, + "sessionAccountSelection": true, + "targetedReconcile": true }); let server_handle = tokio::spawn(async move { @@ -6583,6 +7197,19 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { serde_json::json!({ "sessionId": session_id }), capabilities, ), + ( + "session.connectors.getAccount", + serde_json::json!({ "sessionId": session_id }), + serde_json::json!({ + "accountId": "account-1", + "authInfo": { "type": "token", "host": "github.com", "login": "alice" }, + }), + ), + ( + "session.connectors.getAccount", + serde_json::json!({ "sessionId": session_id }), + serde_json::Value::Null, + ), ( "session.connectors.getStatus", serde_json::json!({ "sessionId": session_id }), @@ -6662,6 +7289,15 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { "refreshCatalog": true, "sessionId": session_id }), + status.clone(), + ), + ( + "session.connectors.reconcile", + serde_json::json!({ + "accountId": "account-1", + "forceConnectorName": "github", + "sessionId": session_id + }), status, ), ]; @@ -6679,6 +7315,16 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { let capabilities: ConnectorCapabilities = connectors.get_capabilities().await.unwrap(); assert_eq!(capabilities.availability, ConnectorAvailability::Enabled); assert_eq!(capabilities.max_poll_attempts, 10); + assert_eq!(capabilities.session_account_selection, Some(true)); + assert_eq!(capabilities.targeted_reconcile, Some(true)); + + let account: Option = connectors.get_account().await.unwrap(); + let account = account.unwrap(); + assert_eq!(account.account_id, "account-1"); + assert_eq!(account.auth_info.r#type, AuthInfoType::Token); + assert_eq!(account.auth_info.host, "github.com"); + assert_eq!(account.auth_info.login, "alice"); + assert!(connectors.get_account().await.unwrap().is_none()); let status: ConnectorStatus = connectors.get_status().await.unwrap(); assert_eq!(status.account_id.as_deref(), Some("account-1")); @@ -6765,6 +7411,14 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { .unwrap(); assert_eq!(reconciled.catalog.unwrap().revision, 4); + let targeted = connectors + .reconcile_with_options( + ConnectorReconcileOptions::new("account-1").force_connector_name("github"), + ) + .await + .unwrap(); + assert_eq!(targeted.catalog.unwrap().revision, 4); + timeout(TIMEOUT, server_handle).await.unwrap().unwrap(); } @@ -7179,11 +7833,58 @@ async fn command_execute_handler_error_propagates_to_ack() { // SessionFsProvider tests -------------------------------------------------- use github_copilot_sdk::session_fs::{ - DirEntry, DirEntryKind, FileInfo, FsError, FsErrorKind, SessionFsConventions, - SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, + DirEntry, DirEntryKind, FileInfo, FsError, FsErrorKind, SessionFsCapabilities, SessionFsConfig, + SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, SessionFsSqliteQueryType, SessionFsSqliteTransactionError, SessionFsSqliteTransactionStatement, }; +#[tokio::test] +async fn binary_capability_rejects_text_only_provider_on_create_and_resume() { + for resume in [false, true] { + let (client_write, mut server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let config = SessionFsConfig::new("/workspace", "/sessions", SessionFsConventions::Posix) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)); + let client = Client::from_streams_with_session_fs_config( + client_read, + client_write, + std::env::temp_dir(), + config, + ) + .unwrap(); + let provider = Arc::new(RecordingFsProvider::new()); + let result = timeout(TIMEOUT, async { + if resume { + client + .resume_session( + github_copilot_sdk::ResumeSessionConfig::new(SessionId::new( + "text-only-provider", + )) + .with_session_fs_provider(provider), + ) + .await + } else { + client + .create_session(SessionConfig::default().with_session_fs_provider(provider)) + .await + } + }) + .await + .expect("provider validation must complete before an RPC"); + let error = result.err().expect("text-only provider must be rejected"); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + assert!(error.to_string().contains("binary"), "{error}"); + assert_eq!(client.registered_session_count_for_test(), 0); + assert!( + timeout(Duration::from_millis(50), read_framed(&mut server_read)) + .await + .is_err(), + "invalid provider must not send a session RPC", + ); + drop(server_write); + } +} + struct RecordingFsProvider { files: parking_lot::Mutex>, blocked_stat: Option<(Arc, Arc)>, @@ -7555,6 +8256,493 @@ async fn failed_create_cancels_pre_response_session_fs_request() { timeout(TIMEOUT, dropped.notified()).await.unwrap(); } +#[derive(Default)] +struct StartupCallbackGate { + entered: Notify, + release: Notify, + dropped: Arc, + completed: std::sync::atomic::AtomicBool, +} + +impl StartupCallbackGate { + async fn wait(&self) { + let _on_drop = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + self.release.notified().await; + self.completed.store(true, Ordering::SeqCst); + } +} + +#[async_trait] +impl PermissionHandler for StartupCallbackGate { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: github_copilot_sdk::PermissionRequestData, + ) -> PermissionResult { + self.wait().await; + PermissionResult::approve_once() + } +} + +#[async_trait] +impl SessionFsProvider for StartupCallbackGate { + async fn stat(&self, _path: &str) -> Result { + self.wait().await; + Ok(FileInfo::new(true, false, 0, "", "")) + } +} + +#[async_trait] +impl ElicitationHandler for StartupCallbackGate { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: ElicitationRequest, + ) -> ElicitationResult { + self.wait().await; + ElicitationResult { + action: "accept".into(), + content: None, + } + } +} + +#[async_trait] +impl McpAuthHandler for StartupCallbackGate { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: McpAuthRequest, + ) -> McpAuthResult { + self.wait().await; + McpAuthResult::Cancelled + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum StartupRoute { + Local, + Deferred, + Resume, +} + +#[derive(Clone, Copy, Debug)] +enum StartupOutcome { + Failure, + Cancellation, + Success, +} + +async fn check_startup_callback_ownership( + route: StartupRoute, + phase: &str, + outcome: StartupOutcome, + callbacks_before_response: bool, +) { + let permission = Arc::new(StartupCallbackGate::default()); + let fs = Arc::new(StartupCallbackGate::default()); + let elicitation = Arc::new(StartupCallbackGate::default()); + let mcp_auth = Arc::new(StartupCallbackGate::default()); + let gates = [&permission, &fs, &elicitation, &mcp_auth]; + let (client, server_read, server_write) = make_client(); + let mut server = FakeServer { + read: server_read, + write: server_write, + session_id: "startup-callbacks".into(), + }; + let prepared = if route == StartupRoute::Resume { + client + .prepare_resume_session( + github_copilot_sdk::ResumeSessionConfig::new(SessionId::new(&server.session_id)) + .with_permission_handler(permission.clone()) + .with_session_fs_provider(fs.clone()) + .with_elicitation_handler(elicitation.clone()) + .with_mcp_auth_handler(mcp_auth.clone()) + .with_coauthor_enabled(false), + ) + .unwrap() + } else { + let mut config = SessionConfig::default() + .with_permission_handler(permission.clone()) + .with_session_fs_provider(fs.clone()) + .with_elicitation_handler(elicitation.clone()) + .with_mcp_auth_handler(mcp_auth.clone()) + .with_coauthor_enabled(false); + if route == StartupRoute::Deferred { + config.cloud = Some(CloudSessionOptions::with_repository( + CloudSessionRepository::new("octocat", "hello-world"), + )); + } else { + config.session_id = Some(SessionId::new(&server.session_id)); + } + client.prepare_session(config).unwrap() + }; + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + let mut request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!( + request["method"], + if route == StartupRoute::Resume { + "session.resume" + } else { + "session.create" + } + ); + if !callbacks_before_response { + server + .respond( + &request, + serde_json::json!({ "sessionId": server.session_id }), + ) + .await; + request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.eventLog.registerInterest"); + } + + server + .send_event( + "permission.requested", + serde_json::json!({ "requestId": "startup-permission", "kind": "read" }), + ) + .await; + server + .send_request( + 7700, + "sessionFs.stat", + serde_json::json!({ "sessionId": server.session_id, "path": "/workspace/meta" }), + ) + .await; + server + .send_event( + "elicitation.requested", + serde_json::json!({ + "requestId": "startup-elicitation", + "message": "Startup confirmation", + "requestedSchema": { "type": "object", "properties": {} }, + }), + ) + .await; + server + .send_event( + "mcp.oauth_required", + serde_json::json!({ + "requestId": "startup-mcp-auth", + "reason": "initial", + "serverName": "startup-server", + "serverUrl": "https://example.com/mcp", + }), + ) + .await; + for gate in gates { + timeout(TIMEOUT, gate.entered.notified()) + .await + .expect("startup callback must enter before setup proceeds"); + } + + while request["method"] != phase { + server + .respond( + &request, + serde_json::json!({ "sessionId": server.session_id }), + ) + .await; + request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert!( + matches!( + request["method"].as_str(), + Some( + "session.eventLog.registerInterest" + | "session.skills.reload" + | "session.options.update" + ) + ), + "unexpected startup RPC: {request}" + ); + } + let mut observed_drops = false; + match outcome { + StartupOutcome::Failure => { + server + .respond_error(&request, -32003, "startup rejected") + .await; + if phase == "session.options.update" { + let disconnect = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(disconnect["method"], "session.detach"); + // Failed setup must drop callbacks even while graceful disconnect is pending. + for gate in gates { + timeout(TIMEOUT, gate.dropped.notified()).await.expect( + "failed mode patch must cancel callbacks before disconnect completes", + ); + } + observed_drops = true; + server + .respond(&disconnect, serde_json::json!({ "success": true })) + .await; + } + let error = match timeout(TIMEOUT, start).await.unwrap().unwrap() { + Err(error) => error, + Ok(_) => panic!("{route:?} unexpectedly succeeded at {phase}"), + }; + assert_eq!(error.kind(), &ErrorKind::Rpc { code: -32003 }); + } + StartupOutcome::Cancellation => { + start.abort(); + match timeout(TIMEOUT, start).await.unwrap() { + Err(error) => assert!(error.is_cancelled()), + Ok(_) => panic!("{route:?} unexpectedly completed at {phase}"), + } + } + StartupOutcome::Success => { + if phase == "session.skills.reload" { + server + .respond_error(&request, -32003, "skills reload failed") + .await; + request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + } + assert_eq!(request["method"], "session.options.update"); + server + .respond(&request, serde_json::json!({ "success": true })) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + // Successful startup and established-session teardown must not abort these callbacks. + timeout(TIMEOUT, session.stop_event_loop()).await.unwrap(); + drop(session); + for gate in gates { + assert!(!gate.completed.load(Ordering::SeqCst)); + gate.release.notify_one(); + } + let mut methods = Vec::new(); + for _ in gates { + let reply = timeout(TIMEOUT, server.read_response()).await.unwrap(); + if let Some(method) = reply["method"].as_str() { + methods.push(method.to_string()); + server.respond(&reply, serde_json::json!({})).await; + } else { + assert_eq!(reply["id"], 7700); + assert_eq!(reply["result"]["isFile"], true); + } + } + methods.sort(); + assert_eq!( + methods, + [ + "session.mcp.oauth.handlePendingRequest", + PERMISSION_CONFIRMATION_METHOD, + "session.ui.handlePendingElicitation", + ] + ); + } + } + for gate in gates { + if !observed_drops { + timeout(TIMEOUT, gate.dropped.notified()) + .await + .unwrap_or_else(|_| { + panic!("{route:?} callback not dropped at {phase}: {outcome:?}") + }); + } + assert_eq!( + gate.completed.load(Ordering::SeqCst), + matches!(outcome, StartupOutcome::Success), + ); + gate.release.notify_one(); + } + assert_eq!(client.registered_session_count_for_test(), 0); + timeout(TIMEOUT, async { while events.recv().await.is_ok() {} }) + .await + .expect("startup event stream must close"); + + // Connection-health check only; the unit test joins both nested dispatch layers + // before its wire fence to prove that no fallback reply can follow it. + let fence = tokio::spawn(async move { client.call("ping", None).await }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "ping", "late callback reply: {request}"); + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, fence).await.unwrap().unwrap().unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn startup_callbacks_owned_until_resume_response() { + for outcome in [StartupOutcome::Failure, StartupOutcome::Cancellation] { + Box::pin(check_startup_callback_ownership( + StartupRoute::Resume, + "session.resume", + outcome, + true, + )) + .await; + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn startup_callbacks_owned_through_mcp_interest() { + for route in [StartupRoute::Local, StartupRoute::Resume] { + for outcome in [StartupOutcome::Failure, StartupOutcome::Cancellation] { + for before_response in [false, true] { + Box::pin(check_startup_callback_ownership( + route, + "session.eventLog.registerInterest", + outcome, + before_response, + )) + .await; + } + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn startup_callbacks_owned_during_deferred_create() { + for outcome in [StartupOutcome::Failure, StartupOutcome::Cancellation] { + Box::pin(check_startup_callback_ownership( + StartupRoute::Deferred, + "session.eventLog.registerInterest", + outcome, + false, + )) + .await; + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn startup_callbacks_owned_through_mode_patch() { + for route in [ + StartupRoute::Local, + StartupRoute::Deferred, + StartupRoute::Resume, + ] { + for outcome in [StartupOutcome::Failure, StartupOutcome::Cancellation] { + Box::pin(check_startup_callback_ownership( + route, + "session.options.update", + outcome, + false, + )) + .await; + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn startup_callbacks_owned_through_skills_reload() { + for outcome in [StartupOutcome::Cancellation, StartupOutcome::Success] { + Box::pin(check_startup_callback_ownership( + StartupRoute::Resume, + "session.skills.reload", + outcome, + true, + )) + .await; + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn startup_callbacks_survive_successful_setup() { + for route in [ + StartupRoute::Local, + StartupRoute::Deferred, + StartupRoute::Resume, + ] { + Box::pin(check_startup_callback_ownership( + route, + "session.options.update", + StartupOutcome::Success, + route != StartupRoute::Deferred, + )) + .await; + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn mode_patch_cleanup_retires_github_token_provider() { + for resume in [false, true] { + for cancel in [false, true] { + let (client, read, write) = make_client(); + let mut server = FakeServer { + read, + write, + session_id: "mode-patch-token".into(), + }; + let provider = Arc::new(|_args: GitHubTokenProviderArgs| async { + Ok(GitHubTokenProviderResult::Cancelled) + }); + let prepared = if resume { + client.prepare_resume_session( + github_copilot_sdk::ResumeSessionConfig::new(SessionId::new( + &server.session_id, + )) + .with_github_token_provider(provider) + .with_coauthor_enabled(false), + ) + } else { + client.prepare_session( + SessionConfig::default() + .with_session_id(server.session_id.as_str()) + .with_github_token_provider(provider) + .with_coauthor_enabled(false), + ) + } + .unwrap(); + let start = tokio::spawn(prepared.start()); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + let registration_id = request["params"]["gitHubTokenProviderRegistrationId"] + .as_str() + .unwrap() + .to_string(); + server + .respond( + &request, + serde_json::json!({ "sessionId": server.session_id }), + ) + .await; + if resume { + let reload = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(reload["method"], "session.skills.reload"); + server.respond(&reload, serde_json::json!({})).await; + } + let patch = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(patch["method"], "session.options.update"); + request_test_token(&mut server, 920, ®istration_id, "github.com").await; + let token = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(token["id"], 920); + assert_eq!(token["result"]["kind"], "cancelled"); + if cancel { + start.abort(); + match timeout(TIMEOUT, start).await.unwrap() { + Err(error) => assert!(error.is_cancelled()), + Ok(_) => panic!("mode patch startup must be cancelled"), + } + } else { + server + .respond_error(&patch, -32003, "mode patch rejected") + .await; + let detach = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(detach["method"], "session.detach"); + server + .respond(&detach, serde_json::json!({ "success": true })) + .await; + match timeout(TIMEOUT, start).await.unwrap().unwrap() { + Err(error) => assert_eq!(error.kind(), &ErrorKind::Rpc { code: -32003 }), + Ok(_) => panic!("mode patch startup must fail"), + } + } + request_test_token(&mut server, 921, ®istration_id, "github.com").await; + let retired = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(retired["id"], 921); + assert_eq!(retired["error"]["code"], -32603); + assert_eq!( + retired["error"]["message"], + "unknown GitHub token provider registration" + ); + assert_eq!(client.registered_session_count_for_test(), 0); + } + } +} + #[tokio::test] async fn session_fs_dispatches_read_file_to_provider() { let provider = Arc::new(RecordingFsProvider::new().with_file("/foo.txt", "hello world")); diff --git a/rust/tests/skill_provider_test.rs b/rust/tests/skill_provider_test.rs new file mode 100644 index 0000000000..71494ee65c --- /dev/null +++ b/rust/tests/skill_provider_test.rs @@ -0,0 +1,965 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![allow(clippy::unwrap_used)] + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::skill_provider::{SkillProvider, SkillProviderDescriptor}; +use github_copilot_sdk::types::{CloudSessionOptions, SessionConfig, SessionId, Tool}; +use github_copilot_sdk::{Client, ClientMode, Error, ErrorKind, ResumeSessionConfig}; +use serde_json::{Value, json}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, duplex}; +use tokio::sync::Notify; +use tokio::time::timeout; + +const TIMEOUT: Duration = Duration::from_secs(2); + +async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), value: &Value) { + let body = serde_json::to_vec(value).unwrap(); + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + writer.write_all(header.as_bytes()).await.unwrap(); + writer.write_all(&body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_framed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + timeout(TIMEOUT, read_framed_untimed(reader)).await.unwrap() +} + +async fn read_framed_untimed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + reader.read_exact(&mut byte).await.unwrap(); + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + + let length: usize = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut body = vec![0; length]; + reader.read_exact(&mut body).await.unwrap(); + serde_json::from_slice(&body).unwrap() +} + +fn make_client() -> (Client, FakeServer) { + make_client_with_mode(ClientMode::default()) +} + +fn make_client_with_mode(mode: ClientMode) -> (Client, FakeServer) { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_mode_for_test( + client_read, + client_write, + std::env::temp_dir(), + mode, + ) + .unwrap(); + ( + client, + FakeServer { + read: server_read, + write: server_write, + session_id: String::new(), + }, + ) +} + +struct FakeServer { + read: DuplexStream, + write: DuplexStream, + session_id: String, +} + +impl FakeServer { + async fn read_request(&mut self) -> Value { + read_framed(&mut self.read).await + } + + async fn respond(&mut self, request: &Value, result: Value) { + let response = json!({ + "jsonrpc": "2.0", + "id": request["id"].as_u64().unwrap(), + "result": result, + }); + write_framed(&mut self.write, &response).await; + } + + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let response = json!({ + "jsonrpc": "2.0", + "id": request["id"].as_u64().unwrap(), + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &response).await; + } + + async fn send_request(&mut self, id: u64, method: &str, params: Value) { + let request = json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + }); + write_framed(&mut self.write, &request).await; + } + + async fn send_notification(&mut self, method: &str, params: Value) { + let notification = json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + }); + write_framed(&mut self.write, ¬ification).await; + } + + async fn read_response(&mut self) -> Value { + read_framed(&mut self.read).await + } +} + +fn descriptor(name: &str, description: &str) -> SkillProviderDescriptor { + SkillProviderDescriptor { + name: name.to_string(), + description: description.to_string(), + ..Default::default() + } +} + +#[derive(Clone, Default)] +struct CallLog(Arc>>); + +impl CallLog { + fn push(&self, call: impl Into) { + self.0.lock().unwrap().push(call.into()); + } + + fn snapshot(&self) -> Vec { + self.0.lock().unwrap().clone() + } +} + +struct RecordingProvider { + skills: Vec, + markdown: HashMap, + calls: CallLog, +} + +impl RecordingProvider { + fn new() -> Self { + Self { + skills: vec![descriptor("review", "Reviews code")], + markdown: HashMap::from([("review".to_string(), "Review carefully.".to_string())]), + calls: CallLog::default(), + } + } + + fn empty() -> Self { + Self { + skills: Vec::new(), + markdown: HashMap::new(), + calls: CallLog::default(), + } + } +} + +#[async_trait] +impl SkillProvider for RecordingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + self.calls.push("list"); + Ok(self.skills.clone()) + } + + async fn read_skill(&self, name: &str) -> std::result::Result, Error> { + self.calls.push(format!("read:{name}")); + Ok(self.markdown.get(name).cloned()) + } +} + +struct FailingProvider { + fail_list: bool, + secret: &'static str, +} + +#[async_trait] +impl SkillProvider for FailingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + if self.fail_list { + Err(Error::with_message(ErrorKind::InvalidConfig, self.secret)) + } else { + Ok(vec![descriptor("review", "Reviews code")]) + } + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + Err(Error::with_message(ErrorKind::InvalidConfig, self.secret)) + } +} + +struct NotifyOnDrop(Arc); + +impl Drop for NotifyOnDrop { + fn drop(&mut self) { + self.0.notify_one(); + } +} + +/// Blocks every call until the SDK drops its future. +struct BlockingProvider { + entered: Arc, + dropped: Arc, +} + +#[async_trait] +impl SkillProvider for BlockingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } +} + +fn requested_session_id(request: &Value) -> &str { + request["params"]["sessionId"].as_str().unwrap() +} + +async fn create_session( + client: &Client, + server: &mut FakeServer, + config: SessionConfig, +) -> github_copilot_sdk::session::Session { + let handle = tokio::spawn({ + let client = client.clone(); + async move { client.create_session(config).await.unwrap() } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server.session_id = requested_session_id(&request).to_string(); + server + .respond( + &request, + json!({ + "sessionId": server.session_id, + "workspacePath": "/tmp/workspace", + }), + ) + .await; + timeout(TIMEOUT, handle).await.unwrap().unwrap() +} + +async fn respond_to_resume(server: &mut FakeServer) { + let request = server.read_request().await; + assert_eq!(request["method"], "session.resume"); + server + .respond( + &request, + json!({ + "sessionId": requested_session_id(&request), + "workspacePath": "/tmp/workspace", + }), + ) + .await; + + let reload = server.read_request().await; + assert_eq!(reload["method"], "session.skills.reload"); + server.respond(&reload, json!({})).await; +} + +async fn disconnect_session( + session: &github_copilot_sdk::session::Session, + server: &mut FakeServer, +) { + let disconnect = session.disconnect(); + tokio::pin!(disconnect); + let detach = tokio::select! { + request = server.read_request() => request, + result = &mut disconnect => panic!("disconnect finished before detach request: {result:?}"), + }; + assert_eq!(detach["method"], "session.detach"); + server + .respond(&detach, json!({ "success": true, "error": null })) + .await; + timeout(TIMEOUT, disconnect).await.unwrap().unwrap(); +} + +fn assert_no_provider(response: &Value, session_id: &str) { + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + format!("No skill provider for session: {session_id}") + ); + assert!(response["error"].get("data").is_none()); +} + +fn assert_session_not_found(response: &Value, session_id: &str) { + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + format!("Session not found: {session_id}") + ); +} + +#[test] +fn descriptor_omits_unset_optional_fields_and_config_debug_redacts_provider() { + let value = serde_json::to_value(descriptor("review", "Reviews code")).unwrap(); + assert_eq!( + value, + json!({ "name": "review", "description": "Reviews code" }) + ); + + let provider: Arc = Arc::new(RecordingProvider::new()); + let config = SessionConfig::default().with_skill_provider(provider.clone()); + let cloned = config.clone(); + assert!(Arc::ptr_eq( + config.skill_provider.as_ref().unwrap(), + cloned.skill_provider.as_ref().unwrap() + )); + let debug = format!("{config:?}"); + assert!(debug.contains("skill_provider")); + assert!(debug.contains("")); + + let resume = + ResumeSessionConfig::new(SessionId::new("resume-id")).with_skill_provider(provider.clone()); + let cloned_resume = resume.clone(); + assert!(Arc::ptr_eq( + resume.skill_provider.as_ref().unwrap(), + cloned_resume.skill_provider.as_ref().unwrap() + )); + assert!(format!("{resume:?}").contains("skill_provider")); +} + +#[tokio::test] +async fn create_and_resume_payloads_flag_only_when_provider_is_supplied() { + let (client, mut server) = make_client(); + + let session = create_session(&client, &mut server, SessionConfig::default()).await; + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session.id().clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + .unwrap() + } + }); + respond_to_resume(&mut server).await; + let _resumed = timeout(TIMEOUT, resume).await.unwrap().unwrap(); + + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + let provider = provider.clone(); + async move { + client + .create_session(SessionConfig::default().with_skill_provider(provider)) + .await + .unwrap() + } + }); + let create_request = server.read_request().await; + assert_eq!(create_request["params"]["hasSkillProvider"], true); + assert!(create_request["params"].get("skillProvider").is_none()); + server.session_id = requested_session_id(&create_request).to_string(); + server + .respond( + &create_request, + json!({ "sessionId": server.session_id, "workspacePath": "/tmp/workspace" }), + ) + .await; + let session_with_provider = timeout(TIMEOUT, create).await.unwrap().unwrap(); + + let resume_with_provider = tokio::spawn({ + let client = client.clone(); + let provider = Arc::new(RecordingProvider::new()); + let session_id = session_with_provider.id().clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id).with_skill_provider(provider)) + .await + .unwrap() + } + }); + let resume_request = server.read_request().await; + assert_eq!(resume_request["method"], "session.resume"); + assert_eq!(resume_request["params"]["hasSkillProvider"], true); + assert!(resume_request["params"].get("skillProvider").is_none()); + server + .respond( + &resume_request, + json!({ "sessionId": requested_session_id(&resume_request), "workspacePath": "/tmp/workspace" }), + ) + .await; + let reload = server.read_request().await; + server.respond(&reload, json!({})).await; + let _ = timeout(TIMEOUT, resume_with_provider) + .await + .unwrap() + .unwrap(); +} + +#[tokio::test] +async fn empty_mode_keeps_enable_skills_default_with_provider() { + let (client, mut server) = make_client_with_mode(ClientMode::Empty); + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_available_tools(Vec::::new()) + .with_tools(Vec::::new()) + .with_skill_provider(provider), + ) + .await + .unwrap() + } + }); + let request = server.read_request().await; + assert_eq!(request["params"]["enableSkills"], false); + assert_eq!(request["params"]["hasSkillProvider"], true); + server.session_id = requested_session_id(&request).to_string(); + server + .respond( + &request, + json!({ "sessionId": server.session_id, "workspacePath": "/tmp/workspace" }), + ) + .await; + let update = server.read_request().await; + assert_eq!(update["method"], "session.options.update"); + server.respond(&update, json!({ "success": true })).await; + let _ = timeout(TIMEOUT, create).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn serves_early_create_and_resume_callbacks() { + let (client, mut server) = make_client(); + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + let provider = provider.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("early-create") + .with_skill_provider(provider), + ) + .await + .unwrap() + } + }); + let create_request = server.read_request().await; + assert_eq!(create_request["method"], "session.create"); + server + .send_request( + 10, + "skillProvider.list", + json!({ "sessionId": "early-create" }), + ) + .await; + let list_response = server.read_response().await; + assert_eq!(list_response["result"]["skills"][0]["name"], "review"); + server + .respond( + &create_request, + json!({ "sessionId": "early-create", "workspacePath": "/tmp/workspace" }), + ) + .await; + let _ = timeout(TIMEOUT, create).await.unwrap().unwrap(); + + let resume_provider: Arc = Arc::new(RecordingProvider::new()); + let resume = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::new("early-resume")) + .with_skill_provider(resume_provider), + ) + .await + .unwrap() + } + }); + let resume_request = server.read_request().await; + assert_eq!(resume_request["method"], "session.resume"); + server + .send_request( + 11, + "skillProvider.list", + json!({ "sessionId": "early-resume" }), + ) + .await; + let list_response = server.read_response().await; + assert_eq!(list_response["result"]["skills"][0]["name"], "review"); + server + .respond( + &resume_request, + json!({ "sessionId": "early-resume", "workspacePath": "/tmp/workspace" }), + ) + .await; + let reload = server.read_request().await; + server.respond(&reload, json!({})).await; + let _ = timeout(TIMEOUT, resume).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn cloud_sessions_reject_provider_before_rpc() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let error = match client + .create_session( + SessionConfig::default() + .with_skill_provider(provider) + .with_cloud(CloudSessionOptions::default()), + ) + .await + { + Ok(_) => panic!("cloud session unexpectedly accepted a skill provider"), + Err(error) => error, + }; + + assert_eq!( + error.to_string(), + "Skill providers are not supported for cloud sessions." + ); + assert_eq!(calls.snapshot(), Vec::::new()); + assert!( + timeout(Duration::from_millis(100), server.read_request()) + .await + .is_err() + ); +} + +#[tokio::test] +async fn dispatches_list_read_empty_and_not_found() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider { + skills: vec![ + descriptor("review", "Reviews code"), + SkillProviderDescriptor { + name: "deploy".to_string(), + description: "Deploys service".to_string(), + argument_hint: Some("[environment]".to_string()), + disable_model_invocation: Some(true), + user_invocable: Some(false), + }, + ], + markdown: HashMap::from([("deploy".to_string(), "# deploy".to_string())]), + calls: CallLog::default(), + }); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + + server + .send_request(20, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!( + response["result"]["skills"][0], + json!({ + "name": "review", + "description": "Reviews code", + }) + ); + assert_eq!( + response["result"]["skills"][1]["argumentHint"], + "[environment]" + ); + assert_eq!( + response["result"]["skills"][1]["disableModelInvocation"], + true + ); + assert_eq!(response["result"]["skills"][1]["userInvocable"], false); + + server + .send_request( + 21, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "deploy" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "markdown": "# deploy" })); + + server + .send_request( + 22, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "missing" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "markdown": null })); + + let empty = Arc::new(RecordingProvider::empty()); + let empty_session = create_session( + &client, + &mut server, + SessionConfig::default() + .with_session_id("empty-catalog") + .with_skill_provider(empty), + ) + .await; + server + .send_request( + 23, + "skillProvider.list", + json!({ "sessionId": empty_session.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "skills": [] })); +} + +#[tokio::test] +async fn provider_errors_are_generic_and_invalid_params_are_rejected() { + let (client, mut server) = make_client(); + let secret = "db-password-in-error"; + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(FailingProvider { + fail_list: true, + secret, + })), + ) + .await; + let session_id = session.id().to_string(); + + server + .send_request(30, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "Skill provider listSkills failed" + ); + assert!(response["error"].get("data").is_none()); + assert!(!response.to_string().contains(secret)); + + let read_session = create_session( + &client, + &mut server, + SessionConfig::default() + .with_session_id("failing-read") + .with_skill_provider(Arc::new(FailingProvider { + fail_list: false, + secret, + })), + ) + .await; + server + .send_request( + 31, + "skillProvider.read", + json!({ "sessionId": read_session.id(), "name": "review" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "Skill provider readSkill failed" + ); + assert!(response["error"].get("data").is_none()); + assert!(!response.to_string().contains(secret)); + + server + .send_request( + 32, + "skillProvider.read", + json!({ "sessionId": read_session.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32602); +} + +#[tokio::test] +async fn cancel_request_drops_provider_future_and_reports_cancellation() { + let (client, mut server) = make_client(); + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(BlockingProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await; + + for (id, method, params) in [ + ( + 60, + "skillProvider.list", + json!({ "sessionId": session.id() }), + ), + ( + 61, + "skillProvider.read", + json!({ "sessionId": session.id(), "name": "review" }), + ), + ] { + server.send_request(id, method, params).await; + timeout(TIMEOUT, entered.notified()).await.unwrap(); + server + .send_notification("$/cancelRequest", json!({ "id": id })) + .await; + + let response = server.read_response().await; + assert_eq!(response["id"], id); + assert_eq!(response["error"]["code"], -32800); + timeout(TIMEOUT, dropped.notified()).await.unwrap(); + } +} + +#[tokio::test] +async fn connection_close_drops_provider_future() { + let (client, mut server) = make_client(); + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(BlockingProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await; + + server + .send_request( + 62, + "skillProvider.list", + json!({ "sessionId": session.id() }), + ) + .await; + timeout(TIMEOUT, entered.notified()).await.unwrap(); + drop(server); + + timeout(TIMEOUT, dropped.notified()).await.unwrap(); +} + +#[tokio::test] +async fn missing_provider_reports_generic_error() { + let (client, mut server) = make_client(); + let session = create_session(&client, &mut server, SessionConfig::default()).await; + let session_id = session.id().to_string(); + server + .send_request( + 41, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "review" }), + ) + .await; + let response = server.read_response().await; + assert_no_provider(&response, &session_id); +} + +#[tokio::test] +async fn teardown_and_failed_open_stop_serving_provider() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + disconnect_session(&session, &mut server).await; + + let failed_provider = Arc::new(RecordingProvider::new()); + let failed_calls = failed_provider.calls.clone(); + let failed_create = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("failed-open") + .with_skill_provider(failed_provider), + ) + .await + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server + .respond_error(&request, -32603, "create failed") + .await; + let error = match timeout(TIMEOUT, failed_create).await.unwrap().unwrap() { + Ok(_) => panic!("failed-open session unexpectedly succeeded"), + Err(error) => error, + }; + assert!(error.to_string().contains("create failed")); + server + .send_request(50, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 50); + assert_session_not_found(&response, &session_id); + server + .send_request( + 51, + "skillProvider.list", + json!({ "sessionId": "failed-open" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 51); + assert_session_not_found(&response, "failed-open"); + assert_eq!(calls.snapshot(), Vec::::new()); + assert_eq!(failed_calls.snapshot(), Vec::::new()); +} + +#[tokio::test] +async fn dropped_session_answers_provider_callbacks_with_an_error() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + drop(session); + + server + .send_request(60, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 60); + assert_session_not_found(&response, &session_id); + + server + .send_request(61, "skillProvider.read", json!({ "name": "review" })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 61); + assert_eq!(response["error"]["code"], -32602); + assert_eq!(calls.snapshot(), Vec::::new()); +} + +#[tokio::test] +async fn resume_rebinds_provider() { + let (client, mut server) = make_client(); + let original = Arc::new(RecordingProvider::new()); + let original_calls = original.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(original), + ) + .await; + let session_id = session.id().clone(); + disconnect_session(&session, &mut server).await; + + let replacement = Arc::new(RecordingProvider::new()); + let replacement_calls = replacement.calls.clone(); + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(session_id).with_skill_provider(replacement), + ) + .await + .unwrap() + } + }); + respond_to_resume(&mut server).await; + let resumed = timeout(TIMEOUT, resume).await.unwrap().unwrap(); + server + .send_request( + 70, + "skillProvider.list", + json!({ "sessionId": resumed.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"]["skills"][0]["name"], "review"); + assert_eq!(original_calls.snapshot(), Vec::::new()); + assert_eq!(replacement_calls.snapshot(), vec!["list"]); +} + +#[tokio::test] +async fn failed_resume_restores_the_resident_provider() { + let (client, mut server) = make_client(); + let resident = Arc::new(RecordingProvider::new()); + let resident_calls = resident.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(resident), + ) + .await; + let session_id = session.id().clone(); + + let replacement = Arc::new(RecordingProvider::new()); + let replacement_calls = replacement.calls.clone(); + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(session_id).with_skill_provider(replacement), + ) + .await + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.resume"); + server + .respond_error(&request, -32603, "resume failed") + .await; + let error = match timeout(TIMEOUT, resume).await.unwrap().unwrap() { + Ok(_) => panic!("resume unexpectedly succeeded"), + Err(error) => error, + }; + assert!(error.to_string().contains("resume failed")); + + // The runtime keeps the resident binding when a resume fails. + server + .send_request(80, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 80); + assert_eq!(response["result"]["skills"][0]["name"], "review"); + assert_eq!(resident_calls.snapshot(), vec!["list"]); + assert_eq!(replacement_calls.snapshot(), Vec::::new()); + drop(session); +} diff --git a/samples/runtime-host/README.md b/samples/runtime-host/README.md new file mode 100644 index 0000000000..1f4349d0b7 --- /dev/null +++ b/samples/runtime-host/README.md @@ -0,0 +1,210 @@ +# Runtime-supervised AHP host: source-build smoke + +This Linux-only smoke uses the Node SDK's `startAhpHost()` and the **standard** +`@microsoft/agent-host-protocol` TypeScript client. There is no application-side +AHP relay, custom protocol implementation, or prototype callback adapter. + +The runtime hosts the full AHP listener in-process and connects it as another +SDK participant to that same runtime. `/proc` verifies the runtime executable, +loaded provider, descendants, and ownership of the listening socket's inode. +There is no companion host or second runtime. Disposing the host closes its +listener and connected AHP client while the runtime and owner's SDK session survive. +`host.pid` is absent for in-process listeners; stop hosting with `dispose()`. There is no +host process-isolation boundary, and `onExit` reports hosting-task termination, +not process death. In-process hosts have no `exitCode`. + +Both source-build and assembled local-candidate modes run the same tests and +manual smoke; only artifact selection differs. + +## Prerequisites + +- Use the `copilot-agent-runtime` checkout. This SDK is + `copilot-agent-runtime/src/sdk`, at the same source commit as the runtime. +- Build the native `copilot-runtime` launcher and its provider shared library + from the local `copilot-agent-runtime` checkout. +- The provider links `copilotd-hosting` from the immutable `copilot-host` Git + revision in the runtime's `Cargo.lock`. No separate host build is staged. +- Install this checkout's `nodejs` and `test/harness` dependencies. +- Set the two absolute source-built artifact paths below. Missing paths fail + immediately; this smoke never falls back to a downloaded CLI/runtime package. + +The exact Node dev dependency is `@microsoft/agent-host-protocol@0.9.0`, matching +host's `rust/v0.9.0` pin (commit +`60706330f2f351b09f150d9a9c3c0eaedfc8e8b9`) and wire version `0.9.0`. +The old `ahp-adapter` prototype's `0.7.0` client is not compatible. + +## Manual smoke with live inference + +From the SDK root (`copilot-agent-runtime/src/sdk`), with a valid GitHub Copilot credential in +`GITHUB_TOKEN` or `GH_TOKEN`: + +```sh +export COPILOT_CLI_PATH=/absolute/path/to/local/runtime-executable +export COPILOT_RUNTIME_PROVIDER_LIB=/absolute/path/to/local/libcopilot_runtime.so +nodejs/node_modules/.bin/tsx samples/runtime-host/smoke.ts "$PWD" +``` + +This sends one ordinary arithmetic turn through AHP and one through the SDK, +checks streamed AHP deltas and runtime session coexistence, then verifies +disposal. It never prints the listener's bearer token. The smoke intentionally +uses the same small standard-client helper as the automated tests. +Before creating the AHP session, the client discovers the agent's advertised +GitHub protected resource and sends the standard `authenticate` command with +the GitHub credential. Listener authentication and GitHub authentication remain +separate; no protected-resource checks are bypassed. + +## Focused E2Es with the existing record/replay harness + +```sh +mkdir -p .runtime-host-test-work +export TMPDIR="$PWD/.runtime-host-test-work" +export COPILOT_RUNTIME_HOST_E2E=1 +export GITHUB_ACTIONS=true +cd nodejs +npm test -- test/e2e/runtime_host.e2e.test.ts +``` + +`TMPDIR` keeps the existing harness's isolated homes, workspaces, and proxy files +inside the checkout. The opt-in flag avoids running a source-build-only suite +against released artifacts. These tests explicitly use the local runtime's TCP +transport so dropping one owner connection does not kill the shared runtime. + +Non-model scenarios cover direct-RPC concurrent/repeated disposal, occupied-port +startup failure and recovery, listener endpoint/token settings and validation, and graceful runtime shutdown +with an attached AHP session. The shutdown case requires a successful RPC and clean exit notification, +not just eventual forced termination. To run only those: + +```sh +npm test -- test/e2e/runtime_host.e2e.test.ts -t 'disposes|startup failure|honors|disables|gracefully' +``` + +The three inference-backed scenarios prove rejection of a second same-home host, +owner-disconnect recovery with ordinary SDK sessions still usable, and +catalog/history recovery after repeated listener disposal and a full +runtime restart using an SDK `baseDirectory`. A second runtime using that directory can run ordinary SDK +sessions but cannot start another AHP writer. The catalog excludes ordinary +SDK sessions. These scenarios use the existing `CapiProxy`, snapshot matcher, and canonical +`session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml` +conversation: model `claude-sonnet-5`, prompt `What is 2+2?`. No recordings or +responses are fabricated. `GITHUB_ACTIONS=true` enforces replay-only matching; +an incompatible request fails rather than silently contacting live inference. +Do not overwrite that shared recording to accommodate a different request. If +the host changes its request shape, record a separate scenario using the existing +harness and a real credential, then review the resulting traffic before use. + +Exit assertions use the optional `onExit` callback, not a handle-owned promise. +The tests independently inspect listener closure and runtime survival, or runtime +reaping on owner shutdown. They do not claim process-crash isolation or injected +task-failure coverage. The +connection-token-disabled case still requires normal AHP resource authentication. +Listener tests also cover supplied/generated tokens, hostname resolution, explicit +non-loopback binding, IPv6 endpoints, and omitted/zero/fixed ports. + +These AHP tests remain opt-in until coordinated runtime publication and SDK +runtime pin updates land. Local unsigned/debug candidates do not verify macOS +notarization, the normal full Bazel build, or other platform release executions. + +## Assembled local candidate package + +After building the local runtime's `dist-cli` assets and its launcher/provider, +commit all source changes in the runtime checkout, then assemble +an unpublished local platform package: + +```sh +# Keep the two source artifact overrides from the prerequisites for staging. +nodejs/node_modules/.bin/tsx samples/runtime-host/stage-candidate.ts \ + /absolute/copilot-agent-runtime \ + "$PWD/.runtime-host-test-work/candidate" +export COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST="$PWD/.runtime-host-test-work/candidate/candidate.json" +unset COPILOT_CLI_PATH COPILOT_RUNTIME_PROVIDER_LIB +cd nodejs +npm test -- test/e2e/runtime_host.e2e.test.ts +``` + +The output directory must not already exist. Staging uses the SDK's actual +platform asset materializer over local `dist-cli`, the runtime's package metadata +and verification helpers. +It then runs `npm pack` and extracts that archive as the candidate package. +The version 3 manifest records the runtime repository root and full source +commit plus the immutable linked `copilotd-hosting` source from `Cargo.lock`. +The SDK source identifies the runtime repository and commit with +`path: "src/sdk"`, rather than claiming a third repository. The runtime argument +must be the repository root containing this SDK. +The manifest also records original paths, binary hashes, and archive hash. +No separate host checkout, host executable, or host-side SDK patch is required. +The hosting library uses its own runtime-free SDK dependency; staging does not +rewrite its Cargo graph. + +The private package's `0.0.0-canary.r1.g.unsigned` version satisfies the +runtime metadata helper's grammar; `r1` is a local placeholder, not release +provenance. Nothing is downloaded or published. Rebuild and restage after changing +sources; do not relabel old binaries as a new build. + +Set `COPILOT_RUNTIME_HOST_CANDIDATE_MANIFEST` to an absolute local manifest path +instead of setting development artifact overrides. Keep +`COPILOT_RUNTIME_HOST_E2E=1`, `GITHUB_ACTIONS=true`, and checkout-local `TMPDIR` +for E2Es; run the identical command above. + +The package must contain local build outputs at +`prebuilds//{copilot-runtime,runtime.node}` for in-process hosting. Its +`package.json` must identify `@github/copilot-` or +`@github/copilot-sdk-` and carry existing `copilotRuntime` metadata: +`sourceRepository: "github/copilot-agent-runtime"` and `sourceSha` equal to the +manifest's runtime source commit. + +The staging step writes this local-build attestation (replace all placeholders; +checksums are lowercase SHA-256 of the actual local build outputs): + +```json +{ + "schemaVersion": 3, + "kind": "local-runtime-host-candidate", + "platform": "linux-x64", + "packageRoot": "/absolute/local-candidate/node_modules/@github/copilot-linux-x64", + "sources": { + "runtime": { + "repository": "github/copilot-agent-runtime", + "checkout": "/absolute/copilot-agent-runtime", + "commit": "" + }, + "hosting": { + "repository": "github/copilot-host", + "commit": "" + }, + "sdk": { + "repository": "github/copilot-agent-runtime", + "checkout": "/absolute/copilot-agent-runtime", + "commit": "", + "path": "src/sdk" + } + }, + "artifacts": { + "runtime": { + "path": "prebuilds/linux-x64/copilot-runtime", + "sourcePath": "/absolute/copilot-agent-runtime/local-build/copilot-runtime", + "sha256": "" + }, + "provider": { + "path": "prebuilds/linux-x64/runtime.node", + "sourcePath": "/absolute/copilot-agent-runtime/local-build/runtime.node", + "sha256": "" + } + } +} +``` + +Source revisions must match the runtime checkout and its locked hosting library, +including this SDK's runtime commit. Legacy version 1/2 manifests must be restaged; editing an old attestation +does not validate the new sources. +Original build artifacts must live inside the runtime checkout, outside +`node_modules`; candidate packages may live inside `node_modules`. +Do not attest downloaded releases as local builds. + +The helper checks the original and packaged artifact hashes, then invokes the +SDK's existing `materializeRuntimeBundle` into `.runtime-host-materialized` +beside the manifest and rechecks the materialized hashes. +The runtime child environment explicitly omits the provider development +override—even if it is set in the invoking shell. `/proc` assertions verify +the actual loaded provider and socket-owning runtime are the validated package +copies, with no companion host or second runtime. This exercises bundled lookup rather than +another source-path override. diff --git a/samples/runtime-host/candidate-sources.ts b/samples/runtime-host/candidate-sources.ts new file mode 100644 index 0000000000..ab19f1233b --- /dev/null +++ b/samples/runtime-host/candidate-sources.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readFileSync, realpathSync } from "node:fs"; +import { isAbsolute, join } from "node:path"; + +interface CandidateSource { + repository: string; + checkout: string; + commit: string; +} + +export interface CandidateSources { + runtime: CandidateSource; + hosting: { repository: "github/copilot-host"; commit: string }; + sdk: CandidateSource & { path: "src/sdk" }; +} + +/** The linked library's immutable source is recorded by Cargo, not a companion build. */ +export function hostingSource(runtime: string): CandidateSources["hosting"] { + const lock = readFileSync(join(runtime, "Cargo.lock"), "utf8"); + const packages = lock.split(/^\[\[package\]\]\s*$/m).filter((entry) => /^name = "copilotd-hosting"$/m.test(entry)); + assert.equal(packages.length, 1, "Cargo.lock must identify one copilotd-hosting library"); + const source = /^source = "(git\+[^"]+)"$/m.exec(packages[0])?.[1]; + assert(source, "The hosting library must use an immutable Git source"); + const url = new URL(source.slice(4)); + assert.equal(url.origin + url.pathname.replace(/\.git$/, ""), "https://github.com/github/copilot-host"); + const commit = url.hash.slice(1); + assert.match(commit, /^[a-f0-9]{40}$/, "Hosting source needs a full resolved commit"); + assert.equal(url.searchParams.get("rev"), commit, "Hosting dependency must pin its resolved commit"); + return { repository: "github/copilot-host", commit }; +} + +function git(checkout: string, ...args: string[]): string { + return execFileSync("git", ["-C", checkout, ...args], { + encoding: "utf8", + }).trim(); +} + +export function validateCandidateSources(sources: CandidateSources, sdk: string): { runtime: string; sdk: string } { + const roots = {} as { runtime: string; sdk: string }; + for (const [name, repository] of [["runtime", "github/copilot-agent-runtime"]] as const) { + const source = sources[name]; + assert.equal(source.repository, repository); + assert(isAbsolute(source.checkout), `${name} checkout must be absolute`); + assert.match(source.commit, /^[a-f0-9]{40}$/, `${name} needs a full source commit`); + roots[name] = realpathSync(source.checkout); + assert.equal( + realpathSync(git(roots[name], "rev-parse", "--show-toplevel")), + roots[name], + `${name} checkout must be a repository root`, + ); + assert.equal( + git(roots[name], "rev-parse", "HEAD"), + source.commit, + `${name} candidate source must match the local checkout`, + ); + } + assert.deepEqual( + sources.hosting, + hostingSource(roots.runtime), + "Candidate hosting source must match the runtime's locked library", + ); + assert.equal(sources.sdk.repository, sources.runtime.repository); + assert(isAbsolute(sources.sdk.checkout), "sdk checkout must be absolute"); + assert.equal(realpathSync(sources.sdk.checkout), roots.runtime, "SDK must belong to the runtime checkout"); + assert.equal(sources.sdk.commit, sources.runtime.commit, "SDK must use the runtime commit"); + assert.equal(sources.sdk.path, "src/sdk", "SDK must identify the src/sdk subtree"); + roots.sdk = realpathSync(join(roots.runtime, sources.sdk.path)); + assert.equal(roots.sdk, realpathSync(sdk), "Candidate must use this local SDK checkout"); + assert.equal( + realpathSync(git(roots.sdk, "rev-parse", "--show-toplevel")), + roots.runtime, + "SDK must be part of the runtime repository", + ); + return roots; +} + +export function localCandidateSources(runtime: string, sdk: string): CandidateSources { + const sources: CandidateSources = { + runtime: { + repository: "github/copilot-agent-runtime", + checkout: realpathSync(runtime), + commit: git(runtime, "rev-parse", "HEAD"), + }, + hosting: hostingSource(runtime), + sdk: { + repository: "github/copilot-agent-runtime", + checkout: realpathSync(runtime), + commit: git(runtime, "rev-parse", "HEAD"), + path: "src/sdk", + }, + }; + validateCandidateSources(sources, sdk); + for (const name of ["runtime"] as const) { + assert.equal( + git(sources[name].checkout, "status", "--porcelain", "--untracked-files=no"), + "", + `Commit tracked changes in ${name} before attesting a source revision`, + ); + } + return sources; +} diff --git a/samples/runtime-host/package.json b/samples/runtime-host/package.json new file mode 100644 index 0000000000..f4a5308c55 --- /dev/null +++ b/samples/runtime-host/package.json @@ -0,0 +1,5 @@ +{ + "name": "runtime-host-source-smoke", + "private": true, + "type": "module" +} diff --git a/samples/runtime-host/smoke.ts b/samples/runtime-host/smoke.ts new file mode 100644 index 0000000000..2f785a56c1 --- /dev/null +++ b/samples/runtime-host/smoke.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import type { ChildProcess } from "node:child_process"; +import { resolve } from "node:path"; +import { approveAll, CopilotClient, RuntimeConnection } from "../../nodejs/src/index.js"; +import { + assertHostStopped, + assertRuntimeListener, + connectAhp, + createAhpSession, + localHostArtifacts, + streamedTurn, +} from "../../nodejs/test/e2e/harness/runtimeHost.js"; + +if (process.platform !== "linux") throw new Error("This source-build topology smoke requires Linux"); +const artifacts = localHostArtifacts(); +const workDir = resolve(process.argv[2] ?? "."); +const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; +assert(githubToken, "Set GITHUB_TOKEN or GH_TOKEN for the live smoke"); +const owner = new CopilotClient({ + connection: RuntimeConnection.forTcp({ path: artifacts.runtimePath }), + workingDirectory: workDir, + gitHubToken: githubToken, + env: { + ...process.env, + ...artifacts.env, + GITHUB_TOKEN: githubToken, + GH_TOKEN: githubToken, + }, +}); +try { + await using sdkSession = await owner.createSession({ + onPermissionRequest: approveAll, + 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); + try { + const session = await createAhpSession(ahp, workDir, githubToken); + const runtime = (owner as unknown as { cliProcess: ChildProcess }).cliProcess; + 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)`); + const [response, sdkResponse] = await Promise.all([ + streamedTurn(ahp.client, session.chatUri, session.subscription, "What is 2+2?"), + sdkSession.sendAndWait({ prompt: "What is 2+2?" }), + ]); + assert(response.deltas > 0, "AHP turn must include streaming deltas"); + assert(response.text.includes("4")); + assert(sdkResponse, "SDK turn must complete"); + assert(sdkResponse.data.content.includes("4")); + const ids = (await owner.listSessions()).map((item) => item.sessionId); + assert(ids.includes(session.sessionId), "AHP session must live in the owner's runtime"); + assert(ids.includes(sdkSession.sessionId)); + console.log(`Standard AHP response: ${response.text} (${response.deltas} streamed deltas)`); + console.log(`SDK response on the same runtime: ${sdkResponse.data.content}`); + await host.dispose(); + await assertHostStopped(host, ahp, runtime.pid); + await sdkSession.getEvents(); + console.log("Listener closed, AHP client disconnected; hosting runtime and SDK session survive."); + } finally { + await ahp.client.shutdown(); + } +} finally { + await owner.stop(); +} diff --git a/samples/runtime-host/stage-candidate.ts b/samples/runtime-host/stage-candidate.ts new file mode 100644 index 0000000000..bcb290c58b --- /dev/null +++ b/samples/runtime-host/stage-candidate.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { createReadStream } from "node:fs"; +import { copyFile, mkdir, realpath, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { getRuntimePlatform, materializeRuntimeBundle } from "../../nodejs/src/runtimeArtifacts.js"; +import { localCandidateSources } from "./candidate-sources.js"; + +const [runtimeArgument, outputArgument] = process.argv.slice(2); +assert( + runtimeArgument && outputArgument && process.argv.length === 4, + "Usage: tsx samples/runtime-host/stage-candidate.ts RUNTIME_CHECKOUT OUTPUT_DIRECTORY", +); +assert.equal(process.platform, "linux", "This integration candidate currently targets Linux"); +const sdk = await realpath(fileURLToPath(new URL("../../", import.meta.url))); +const runtime = await realpath(resolve(runtimeArgument)); +const output = resolve(outputArgument); +const platform = getRuntimePlatform(); +const require = createRequire(join(sdk, "nodejs/package.json")); +const { extract } = require("tar"); + +const sources = localCandidateSources(runtime, sdk); +await mkdir(output); + +async function digest(path: string) { + const hash = createHash("sha256"); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return hash.digest("hex"); +} + +const artifacts: Record = {}; +for (const [name, variable, checkout, filename] of [ + ["runtime", "COPILOT_CLI_PATH", runtime, "copilot-runtime"], + ["provider", "COPILOT_RUNTIME_PROVIDER_LIB", runtime, "runtime.node"], +]) { + const value = process.env[variable]; + assert(value && isAbsolute(value), `${variable} must select an absolute local build output`); + const sourcePath = await realpath(value); + const suffix = relative(checkout, sourcePath); + assert(suffix && !isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)); + assert(!sourcePath.includes(`${sep}node_modules${sep}`), `${variable} cannot select a release`); + artifacts[name] = { + path: join("prebuilds", platform, filename), + sourcePath, + sha256: await digest(sourcePath), + }; +} + +const assembly = join(output, "assembly"); +const packed = join(output, "packed"); +await mkdir(packed); +const { createRuntimePackageMetadata, verifyExtractedRuntimePackage } = await import( + pathToFileURL(join(runtime, "script/runtime-platform-package.mjs")).href +); +// Use the same asset selector as Node platform-package assembly. dist-cli must +// already have been built locally; no release acquisition runs in this script. +const wrapper = materializeRuntimeBundle({ packageRoot: join(runtime, "dist-cli"), platform }, assembly, platform); +const assemblyRoot = resolve(dirname(wrapper), "../.."); +for (const name of ["runtime", "provider"]) { + await copyFile(artifacts[name].sourcePath, join(assemblyRoot, artifacts[name].path)); +} +// The release metadata helper requires its normal version grammar. r1 here is +// a local-only placeholder, not a claim of an actual GitHub Actions release. +const version = `0.0.0-canary.r1.g${sources.runtime.commit.slice(0, 7)}.unsigned`; +await writeFile( + join(assemblyRoot, "package.json"), + JSON.stringify( + { + ...createRuntimePackageMetadata(platform, version, sources.runtime.commit), + name: `@github/copilot-sdk-${platform}`, + private: true, + }, + null, + 2, + ) + "\n", +); +await verifyExtractedRuntimePackage(assemblyRoot, platform); +const packResult = JSON.parse( + execFileSync("npm", ["pack", assemblyRoot, "--ignore-scripts", "--json", "--pack-destination", packed], { + cwd: sdk, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + }), +); +const tarball = join(packed, packResult[0].filename); +const packageRoot = join(output, "node_modules", "@github", `copilot-sdk-${platform}`); +await mkdir(packageRoot, { recursive: true }); +await extract({ file: tarball, cwd: packageRoot, strip: 1, strict: true }); +await verifyExtractedRuntimePackage(packageRoot, platform); +for (const artifact of Object.values(artifacts)) { + assert.equal(await digest(join(packageRoot, artifact.path)), artifact.sha256); +} +const manifestPath = join(output, "candidate.json"); +await writeFile( + manifestPath, + JSON.stringify( + { + schemaVersion: 3, + kind: "local-runtime-host-candidate", + packageRoot, + platform, + sources, + artifacts, + packageArchive: { path: tarball, sha256: await digest(tarball) }, + }, + null, + 2, + ) + "\n", +); +await rm(assembly, { recursive: true }); +console.log(manifestPath); 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..7c205a2255 100644 --- a/scripts/ci/run-tasks.test.mjs +++ b/scripts/ci/run-tasks.test.mjs @@ -3,7 +3,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from "node:assert/strict"; +import childProcess from "node:child_process"; +import fs from "node:fs"; +import { syncBuiltinESMExports } from "node:module"; +import path from "node:path"; import test from "node:test"; +import { pathToFileURL } from "node:url"; import { getTaskSteps } from "../run-tasks.mjs"; @@ -33,3 +38,117 @@ test("retains the explicit SDK Rust formatting command", () => { }, ]); }); + +test("default build selects the existing Node and Rust build steps in order", () => { + assert.deepEqual(getTaskSteps("build:default"), [ + ...getTaskSteps("build", "nodejs"), + ...getTaskSteps("build", "rust"), + ]); + assert.throws(() => getTaskSteps("build:default", "java"), /Unknown SDK task/); +}); + +test("checkout default build prepares both projections once without rebuilding the CLI", async (t) => { + const { runTasks, preparations, calls, sdkRoot, root } = await taskFixture(t, true); + + runTasks("build:default", undefined, { environment: {}, runtimeSource: "checkout" }); + + assert.deepEqual(preparations, [{ languages: ["nodejs", "rust"], runtimeRoot: root, sdkRoot }]); + assert.deepEqual(calls, [ + { executable: "npm", args: ["ci", "--ignore-scripts", "--include=dev"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "npm", args: ["run", "build"], cwd: path.join(sdkRoot, "nodejs") }, + { + executable: process.platform === "win32" ? "pnpm.cmd" : "pnpm", + args: ["bazel", "build", "--action_env=PATH", "//src/sdk/rust:github-copilot-sdk"], + cwd: root, + }, + ]); + + calls.length = 0; + runTasks("build:default", undefined, { environment: {}, runtimeSource: "checkout" }); + assert.equal(preparations.length, 2); + assert.equal(calls.length, 2); + assert.deepEqual(calls[0].args, ["run", "build"]); +}); + +test("standalone default build retains the all-features Cargo build", async (t) => { + const { runTasks, preparations, calls, sdkRoot } = await taskFixture(t, false); + + runTasks("build:default", undefined, { environment: { COPILOT_RUNTIME_SOURCE: "published" } }); + + assert.deepEqual(preparations, []); + assert.deepEqual(calls, [ + { executable: "npm", args: ["ci", "--ignore-scripts", "--include=dev"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "npm", args: ["run", "build"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "cargo", args: ["build", "--all-features"], cwd: path.join(sdkRoot, "rust") }, + ]); +}); + +for (const language of ["nodejs", "rust"]) { + test(`checkout ${language} build still prepares only its selected projection`, async (t) => { + const { runTasks, preparations, calls, sdkRoot, root } = await taskFixture(t, true); + + runTasks("build", language, { environment: {}, runtimeSource: "checkout" }); + + assert.deepEqual(preparations, [{ languages: [language], runtimeRoot: root, sdkRoot }]); + assert.equal(calls.length, language === "nodejs" ? 2 : 1); + assert.ok(calls.every(({ cwd }) => cwd === (language === "nodejs" ? path.join(sdkRoot, language) : root))); + }); +} + +for (const checkout of [true, false]) { + test(`Java docs install once through the Maven wrapper before validation (${checkout ? "checkout" : "standalone"})`, async (t) => { + const { runTasks, preparations, calls, sdkRoot } = await taskFixture(t, checkout); + + runTasks("docs", "java", { environment: {}, runtimeSource: checkout ? "checkout" : "published" }); + + assert.deepEqual(preparations, []); + assert.deepEqual(calls, [ + { executable: "npm", args: ["run", "extract"], cwd: path.join(sdkRoot, "scripts/docs-validation") }, + { + executable: process.platform === "win32" ? "mvnw.cmd" : "./mvnw", + args: ["install", "-Dmaven.test.skip=true", "-Dskip.test.harness=true", "-Dcopilot.native.skip.download=true"], + cwd: path.join(sdkRoot, "java"), + }, + { executable: "npm", args: ["run", "validate:java"], cwd: path.join(sdkRoot, "scripts/docs-validation") }, + ]); + }); +} + +async function taskFixture(t, checkout) { + const root = fs.mkdtempSync(path.join(import.meta.dirname, ".run-tasks-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const sdkRoot = path.join(root, checkout ? "src/sdk" : "sdk"); + const scripts = path.join(sdkRoot, "scripts"); + fs.mkdirSync(scripts, { recursive: true }); + for (const name of ["run-tasks.mjs", "runtime-layout.mjs", "install-dependencies.mjs"]) { + fs.copyFileSync(new URL(`../${name}`, import.meta.url), path.join(scripts, name)); + } + // Observe the prerequisite boundary without invoking Bazel or changing real generated files. + fs.writeFileSync( + path.join(scripts, "build-prerequisites.mjs"), + `export const preparations = []; +export function prepareSdkSources(options) { preparations.push(options); } +export function bazelActionEnvironmentArgument() { return "--action_env=PATH"; } +`, + ); + fs.mkdirSync(path.join(sdkRoot, "nodejs")); + fs.writeFileSync(path.join(sdkRoot, "nodejs/package.json"), "{}"); + fs.writeFileSync(path.join(sdkRoot, "nodejs/package-lock.json"), "{}"); + if (checkout) { + fs.mkdirSync(path.join(root, "script")); + fs.writeFileSync(path.join(root, "script/sea-build.ts"), ""); + } + const calls = []; + const spawn = t.mock.method(childProcess, "spawnSync", (executable, args, { cwd }) => { + calls.push({ executable, args, cwd }); + return { status: 0 }; + }); + syncBuiltinESMExports(); + t.after(() => { + spawn.mock.restore(); + syncBuiltinESMExports(); + }); + const { runTasks } = await import(pathToFileURL(path.join(scripts, "run-tasks.mjs"))); + const { preparations } = await import(pathToFileURL(path.join(scripts, "build-prerequisites.mjs"))); + return { runTasks, preparations, calls, sdkRoot, root }; +} diff --git a/scripts/ci/runtime-artifact.mjs b/scripts/ci/runtime-artifact.mjs index a6f41275bf..8205c196de 100644 --- a/scripts/ci/runtime-artifact.mjs +++ b/scripts/ci/runtime-artifact.mjs @@ -99,6 +99,8 @@ export function prepareRuntimeArtifact(options = {}) { `package/${executableName}`, "--exclude", "package/app.js", + "--exclude", + "package/cli-main.js", "-czf", path.basename(inProcessArchive), "package", diff --git a/scripts/ci/runtime-artifact.test.mjs b/scripts/ci/runtime-artifact.test.mjs index bee71a0c29..1823844ad3 100644 --- a/scripts/ci/runtime-artifact.test.mjs +++ b/scripts/ci/runtime-artifact.test.mjs @@ -14,6 +14,50 @@ import { fileURLToPath } from "node:url"; import { prepareRuntimeArtifact, restoreRuntimeArtifact, stageRuntimeSchemas } from "./runtime-artifact.mjs"; +await test("keeps test fixtures out of the caller's GitHub command files and runtime output", (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "sdk-runtime-artifact-isolation-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const environmentFile = path.join(root, "github-env"); + const outputFile = path.join(root, "github-output"); + const outputDirectory = path.join(root, "caller-output"); + const marker = path.join(outputDirectory, "marker"); + writeFixture(environmentFile, "EXISTING_ENV=unchanged\n"); + writeFixture(outputFile, "existing-output=unchanged\n"); + writeFixture(marker, "caller-owned output"); + const environment = { ...process.env }; + // Start a fresh runner rather than inheriting the parent's test-child protocol. + delete environment.NODE_TEST_CONTEXT; + + const result = spawnSync( + process.execPath, + [ + "--test", + "--test-reporter=tap", + "--test-name-pattern=^(preserves complete|selects an explicit|prepares artifacts)", + fileURLToPath(import.meta.url), + ], + { + encoding: "utf8", + env: { + ...environment, + GITHUB_ENV: environmentFile, + GITHUB_OUTPUT: outputFile, + COPILOT_RUNTIME_TARGET: `${process.platform}-${process.arch}`, + COPILOT_RUNTIME_OUTPUT_DIRECTORY: outputDirectory, + }, + }, + ); + + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.match(result.stdout, /^ok \d+ - preserves complete checked-in Rust release pins$/m); + assert.match(result.stdout, /^ok \d+ - selects an explicit musl target without falling back to the GNU artifact$/m); + assert.match(result.stdout, /^ok \d+ - prepares artifacts from an exported SDK layout$/m); + assert.equal(fs.readFileSync(environmentFile, "utf8"), "EXISTING_ENV=unchanged\n"); + assert.equal(fs.readFileSync(outputFile, "utf8"), "existing-output=unchanged\n"); + assert.equal(fs.readFileSync(marker, "utf8"), "caller-owned output"); + assert.deepEqual(fs.readdirSync(outputDirectory), ["marker"]); +}); + await test("stages all same-checkout runtime inputs", (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "sdk-runtime-artifact-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); @@ -28,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"); @@ -36,12 +81,13 @@ await test("stages all same-checkout runtime inputs", (t) => { writeFixture(path.join(runtimeRoot, "dist-cli", "prebuilds", target, wrapperName), "wrapper"); fs.mkdirSync(path.join(sdkRoot, "rust"), { recursive: true }); - const values = prepareRuntimeArtifact({ runtimeRoot, sdkRoot, outputDirectory, environmentFile }); + const values = prepareRuntimeArtifact({ runtimeRoot, sdkRoot, target, outputDirectory, environmentFile }); assert.equal(fs.readFileSync(values.COPILOT_RUNTIME_BINARY_PATH, "utf8"), "executable"); 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", @@ -67,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) => { @@ -249,12 +296,18 @@ await test("prepares artifacts from an exported SDK layout", (t) => { encoding: "utf8", env: { ...process.env, + COPILOT_RUNTIME_TARGET: `${process.platform}-${process.arch}`, COPILOT_RUNTIME_OUTPUT_DIRECTORY: path.join(root, "output"), GITHUB_ENV: path.join(root, "environment"), }, }); assert.equal(result.status, 0, result.stderr); + // The fixture paths must be the ones the script used; the isolation test relies on it. + assert.match( + fs.readFileSync(path.join(root, "environment"), "utf8"), + new RegExp(`^COPILOT_CLI_PATH=${escapeRegExp(path.join(root, "output", "package"))}`, "m"), + ); }); await test("restores executable bits only in directories present in the artifact", (t) => { diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 0b2620b093..d274af26d9 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -18,6 +18,7 @@ import { getApiSchemaPath, getRpcSchemaTypeName, getSessionEventsSchemaPath, + isCodegenEntrypoint, writeGeneratedFile, collectExternalSchemaRefNames, collectDefinitionCollections, @@ -57,6 +58,7 @@ import { type RpcMethod, type SessionEventEnvelopeProperty, } from "./utils.js"; +import { isOmittableRequest, readLegacyParameters, validateLegacyDefinitions } from "./legacy-parameters.js"; const execFileAsync = promisify(execFile); @@ -448,6 +450,9 @@ function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: if (format === "date-time") return "DateTimeOffset?"; return "string?"; } + if (nonNullTypes.length === 1 && nonNullTypes[0] === "boolean") { + return "bool?"; + } if (nonNullTypes.length === 1 && (nonNullTypes[0] === "number" || nonNullTypes[0] === "integer")) { if (format === "duration" && !isSecondsDurationPropertyName(propName)) { return "TimeSpan?"; @@ -582,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"]); @@ -632,6 +639,7 @@ interface EventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventDeprecated: boolean; eventExperimental: boolean; dataExperimental: boolean; } @@ -733,6 +741,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataClassName: `${baseName}Data`, dataSchema, dataDescription: dataSchema?.description, + eventDeprecated: isSchemaDeprecated(variant), eventExperimental: isSchemaExperimental(variant), dataExperimental: isSchemaExperimental(dataSchema), }; @@ -1502,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; `); @@ -1522,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.`; @@ -1540,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}";`, ""); @@ -1569,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))]`); @@ -1595,6 +1692,10 @@ export async function generateSessionEvents(schemaPath?: string): Promise // ══════════════════════════════════════════════════════════════════════════════ let emittedRpcClassSchemas = new Map(); +// Class names reachable from a public method's params or result. A type shared +// with an internal method must stay public regardless of which method emits it +// first; see the upgrade in `emitRpcClass`. +const publicReachableRpcClasses = new Set(); const nonSessionRequestTypeNames = new Set(); let emittedRpcEnumResultTypes = new Set(); let experimentalRpcTypes = new Set(); @@ -1670,6 +1771,78 @@ function resolveMethodParamsSchema(method: RpcMethod): JSONSchema7 | undefined { ); } +type RpcParameterEntry = [string, JSONSchema7 | boolean]; + +function legacyParameterEntries( + method: RpcMethod, + params: JSONSchema7 | undefined, + entries: RpcParameterEntry[], + session = false, +): RpcParameterEntry[] | undefined { + const resolved = method.params ? resolveSchema(method.params, rpcDefinitions) : undefined; + const legacy = readLegacyParameters(params, method.rpcMethod, { + implicit: session ? ["sessionId"] : [], + optional: isOmittableRequest(method.params), + nullable: !!((method.params && getNullableInner(method.params)) || (resolved && getNullableInner(resolved))), + }); + if (!legacy) return undefined; + const properties = new Map(entries); + let optionalSeen = false; + return legacy.legacy.map((name) => { + const property = properties.get(name); + if (!property || typeof property !== "object") { + throw new Error(`Invalid x-legacy-parameters for ${method.rpcMethod}: non-object property ${name}`); + } + // C# positional parameters with defaults must follow the required ones. + if (legacy.required.has(name)) { + if (optionalSeen) { + throw new Error(`Invalid x-legacy-parameters for ${method.rpcMethod}: required parameters must precede optional parameters`); + } + } else { + optionalSeen = true; + } + return [name, property]; + }); +} + +function emitRequestObjectMethod( + name: string, + method: RpcMethod, + requestClassName: string, + wireRequestClassName: string, + assignments: string[] | undefined, + resultClassName: string, + lines: string[], + indent: string, + groupExperimental: boolean, + groupDeprecated: boolean, + requiredMembers: string[], +): void { + const resultSchema = getMethodResultSchema(method); + const taskType = isVoidSchema(resultSchema) ? "Task" : `Task<${resultClassName}>`; + const visibility = method.visibility === "internal" ? "internal" : "public"; + const session = assignments !== undefined; + lines.push(""); + pushRpcMethodXmlDocs(lines, method, indent, [ + { name: "request", description: rpcParamsDescription(method, resolveMethodParamsSchema(method)) }, + { name: "cancellationToken", description: CANCELLATION_TOKEN_DESCRIPTION, escapeDescription: false }, + ], resultSchema); + if (method.stability === "experimental" && !groupExperimental) pushExperimentalAttribute(lines, indent); + if (method.deprecated && !groupDeprecated) pushObsoleteAttributes(lines, indent); + lines.push(`${indent}${visibility} async ${taskType} ${name}Async(${requestClassName} request, CancellationToken cancellationToken = default)`); + lines.push(`${indent}{`, `${indent} ArgumentNullException.ThrowIfNull(request);`); + for (const member of requiredMembers) { + lines.push(`${indent} ArgumentNullException.ThrowIfNull(request.${member});`); + } + if (session) { + lines.push(`${indent} _session.ThrowIfDisposed();`); + lines.push(`${indent} var wireRequest = new ${wireRequestClassName} { ${assignments.join(", ")} };`); + } + const invocation = `CopilotClient.InvokeRpcAsync${isVoidSchema(resultSchema) ? "" : `<${resultClassName}>`}`; + lines.push(`${indent} ${isVoidSchema(resultSchema) ? "" : "return "}await ${invocation}(${session ? "_session.Rpc" : "_rpc"}, "${method.rpcMethod}", [${session ? "wireRequest" : "request"}], cancellationToken);`); + lines.push(`${indent}}`); +} + function stableStringify(value: unknown): string { if (Array.isArray(value)) { return `[${value.map((item) => stableStringify(item)).join(",")}]`; @@ -1718,6 +1891,10 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam if (isOpaqueJson(schema)) { return isRequired ? "JsonElement" : "JsonElement?"; } + if (schema.type === "object" && schema.additionalProperties === true && + (!schema.properties || Object.keys(schema.properties).length === 0)) { + return isRequired ? "IDictionary" : "IDictionary?"; + } // Handle $ref by resolving against schema definitions and generating the referenced class if (schema.$ref) { const typeName = typeToClassName(refTypeName(schema.$ref, rpcDefinitions)); @@ -1846,7 +2023,8 @@ function emitRpcClass( visibility: "public" | "internal", extraClasses: string[], inlineTypeParentName: string = className, - preserveRequiredNulls = false + preserveRequiredNulls = false, + requiredMembers = false ): string { const effectiveSchema = resolveObjectSchema(schema, rpcDefinitions) ?? @@ -1861,6 +2039,13 @@ function emitRpcClass( (effectiveSchema as Record).visibility === "internal" ) { visibility = "internal"; + } else if (publicReachableRpcClasses.has(className)) { + // The caller's default follows the *method* being emitted, and only the + // first emission of a class name wins. When an internal method shares a + // type with a public one and is emitted first, that would freeze the + // type as internal and leave the public method referencing it + // (CS0050/CS0051). Reachability from a public method decides instead. + visibility = "public"; } const schemaKey = stableStringify(effectiveSchema); const existingSchema = emittedRpcClassSchemas.get(className); @@ -1908,7 +2093,12 @@ function emitRpcClass( let defaultVal = ""; let propAccessors = "{ get; set; }"; - if (isReq && !csharpType.endsWith("?")) { + let requiredModifier = ""; + if (requiredMembers && isReq && !csharpType.endsWith("?")) { + // Legacy-parameter request types require their mandatory inputs rather than + // defaulting them to empty values. + requiredModifier = "required "; + } else if (isReq && !csharpType.endsWith("?")) { if (csharpType === "string") defaultVal = " = string.Empty;"; else if (csharpType.startsWith("IList<")) { propAccessors = "{ get => field ??= []; set; }"; @@ -1921,7 +2111,7 @@ function emitRpcClass( defaultVal = " = null!;"; } } - lines.push(` ${propVisibility} ${csharpType} ${csharpName} ${propAccessors}${defaultVal}`); + lines.push(` ${propVisibility} ${requiredModifier}${csharpType} ${csharpName} ${propAccessors}${defaultVal}`); if (i < props.length - 1) lines.push(""); } lines.push(`}`); @@ -2072,6 +2262,7 @@ function emitServerInstanceMethod( const effectiveParams = resolveMethodParamsSchema(method); const paramEntries = effectiveParams?.properties ? Object.entries(effectiveParams.properties) : []; const requiredSet = new Set(effectiveParams?.required || []); + const legacyEntries = legacyParameterEntries(method, effectiveParams, paramEntries); // Sort so required params come before optional (C# requires defaults at end) paramEntries.sort((a, b) => { @@ -2086,16 +2277,17 @@ function emitServerInstanceMethod( if (method.stability === "experimental" && !nonExperimentalRpcTypes.has(requestClassName)) { experimentalRpcTypes.add(requestClassName); } - const reqClass = emitRpcClass(requestClassName, effectiveParams!, "internal", classes, requestClassName, true); + const reqClass = emitRpcClass(requestClassName, effectiveParams!, legacyEntries ? methodVisibility : "internal", classes, requestClassName, true, !!legacyEntries); if (reqClass) classes.push(reqClass); } const sigParams: string[] = []; const bodyAssignments: string[] = []; const argumentNullChecks: string[] = []; + const requiredMemberNames: string[] = []; const parameterDescriptions: Array<{ name: string; description?: string; escapeDescription?: boolean }> = []; - for (const [pName, pSchema] of paramEntries) { + for (const [pName, pSchema] of legacyEntries ?? paramEntries) { if (typeof pSchema !== "object") continue; const isReq = requiredSet.has(pName); const jsonSchema = pSchema as JSONSchema7; @@ -2138,6 +2330,9 @@ function emitServerInstanceMethod( if (opaqueRequired || opaqueListRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } + if (opaqueListRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { + requiredMemberNames.push(csharpName); + } parameterDescriptions.push({ name: pName, description: jsonSchema.description }); } sigParams.push("CancellationToken cancellationToken = default"); @@ -2178,6 +2373,9 @@ function emitServerInstanceMethod( } } lines.push(`${indent}}`); + if (legacyEntries && requestClassName) { + emitRequestObjectMethod(methodName, method, requestClassName, requestClassName, undefined, resultClassName, lines, indent, groupExperimental, groupDeprecated, requiredMemberNames); + } } function emitSessionRpcClasses(node: Record, classes: string[]): string[] { @@ -2252,6 +2450,7 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas const effectiveParams = resolveMethodParamsSchema(method); const paramEntries = (effectiveParams?.properties ? Object.entries(effectiveParams.properties) : []).filter(([k]) => k !== "sessionId"); const requiredSet = new Set(effectiveParams?.required || []); + const legacyEntries = legacyParameterEntries(method, effectiveParams, paramEntries, true); const useRequestParameter = paramEntries.length > 0 && !!getNullableInner(method.params) && @@ -2265,7 +2464,7 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas }); const requestClassName = paramsTypeName(method); - const wireRequestClassName = useRequestParameter || hasSessionRequestEnvelope(effectiveParams) + const wireRequestClassName = useRequestParameter || legacyEntries || hasSessionRequestEnvelope(effectiveParams) ? `${requestClassName}WithSession` : requestClassName; if (method.stability === "experimental" && !nonExperimentalRpcTypes.has(requestClassName)) { @@ -2275,13 +2474,13 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas } } if (effectiveParams?.properties && Object.keys(effectiveParams.properties).length > 0) { - if (useRequestParameter) { + if (useRequestParameter || legacyEntries) { const publicParams: JSONSchema7 = { ...effectiveParams, properties: Object.fromEntries(paramEntries), required: effectiveParams.required?.filter((name) => name !== "sessionId"), }; - const publicReqClass = emitRpcClass(requestClassName, publicParams, methodVisibility, classes, requestClassName, true); + const publicReqClass = emitRpcClass(requestClassName, publicParams, methodVisibility, classes, requestClassName, true, !!legacyEntries); if (publicReqClass) classes.push(publicReqClass); // The wire wrapper carries the same properties as the public request // type plus `sessionId`, so both must reuse the same inline types. @@ -2303,6 +2502,7 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas const sigParams: string[] = []; const bodyAssignments = [`SessionId = _session.SessionId`]; const argumentNullChecks: string[] = []; + const requiredMemberNames: string[] = []; const parameterDescriptions: Array<{ name: string; description?: string; escapeDescription?: boolean }> = []; if (useRequestParameter) { @@ -2314,7 +2514,7 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas bodyAssignments.push(`${csharpName} = request?.${csharpName}`); } } else { - for (const [pName, pSchema] of paramEntries) { + for (const [pName, pSchema] of legacyEntries ?? paramEntries) { if (typeof pSchema !== "object") continue; const isReq = requiredSet.has(pName); const jsonSchema = pSchema as JSONSchema7; @@ -2348,6 +2548,9 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas if (opaqueRequired || opaqueListRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } + if (opaqueListRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { + requiredMemberNames.push(csharpName); + } parameterDescriptions.push({ name: pName, description: jsonSchema.description }); } } @@ -2379,6 +2582,11 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas } else { lines.push(`${indent} await CopilotClient.InvokeRpcAsync(_session.Rpc, "${method.rpcMethod}", [${localRequestName}], cancellationToken);`, `${indent}}`); } + if (legacyEntries) { + const assignments = ["SessionId = _session.SessionId", ...paramEntries.flatMap(([name, schema]) => + typeof schema === "object" ? [`${toCSharpPropertyName(name, schema)} = request.${toCSharpPropertyName(name, schema)}`] : [])]; + emitRequestObjectMethod(methodName, method, requestClassName, wireRequestClassName, assignments, resultClassName, lines, indent, groupExperimental, groupDeprecated, requiredMemberNames); + } } function emitSessionApiClass(className: string, node: Record, classes: string[]): string[] { @@ -2709,6 +2917,7 @@ export function generateRpcCode( schema = cloneSchemaForCodegen(schema); omitUnrepresentableInternalProperties(schema); emittedRpcClassSchemas.clear(); + publicReachableRpcClasses.clear(); nonSessionRequestTypeNames.clear(); emittedRpcEnumResultTypes.clear(); experimentalRpcTypes.clear(); @@ -2719,6 +2928,8 @@ export function generateRpcCode( generatedEnums.clear(); // Clear shared enum deduplication map externalRpcValueTypes = new Set([...externalValueTypes].map(typeToClassName)); rpcDefinitions = collectDefinitionCollections(schema as Record); + // Response classes gain settable properties, so object initialisers are unchanged. + validateLegacyDefinitions(rpcDefinitions); for (const method of [ ...collectRpcMethods(schema.server || {}), ...collectRpcMethods(filterNodeByVisibility(schema.clientSession || {}, "public") || {}), @@ -2748,6 +2959,18 @@ export function generateRpcCode( } } } + for (const name of collectRpcMethodReferencedDefinitionNames( + allMethods.filter((method) => method.visibility !== "internal"), + rpcDefinitions + )) { + publicReachableRpcClasses.add(typeToClassName(name)); + } + // A method's own request wrapper is deliberately emitted `internal` at its + // call site; public methods take the individual parameters instead. Only + // types shared *inside* a signature need the upgrade, so drop the wrappers. + for (const method of allMethods) { + publicReachableRpcClasses.delete(paramsTypeName(method)); + } const classes: string[] = []; let serverRpcParts: string[] = []; @@ -2909,7 +3132,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro const __filename = fileURLToPath(import.meta.url); -if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { +if (isCodegenEntrypoint(process.argv[1], __filename)) { const sessionArg = process.argv[2] || undefined; const apiArg = process.argv[3] || undefined; generate(sessionArg, apiArg).catch((err) => { diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 7dfc4e444c..5e01ee7bf2 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -29,6 +29,7 @@ import { getNullableInner, getRpcSchemaTypeName, getSessionEventsSchemaPath, + isCodegenEntrypoint, getSessionEventVariantSchemas, getSharedSessionEventEnvelopeProperties, hasSchemaPayload, @@ -58,6 +59,7 @@ import { type RpcMethod, type SessionEventEnvelopeProperty, } from "./utils.js"; +import { validateLegacyRequests, validateLegacyDefinitions } from "./legacy-parameters.js"; const execFileAsync = promisify(execFile); @@ -409,8 +411,8 @@ async function formatGoFile(filePath: string): Promise { try { await execFileAsync("go", ["fmt", filePath]); console.log(` ✓ Formatted with go fmt`); - } catch { - // go fmt not available, skip + } catch (cause) { + throw new Error(`Failed to format ${filePath}. Go is required for SDK generation; install the version specified in go/go.mod and ensure go is on PATH.`, { cause }); } } @@ -490,6 +492,9 @@ function goParamsTypeName(method: RpcMethod): string { if (method.rpcMethod.startsWith("session.") && method.params?.$ref) { return fallback; } + if (method.params?.$ref) { + return toPascalCase(refTypeName(method.params.$ref, rpcDefinitions)); + } return getRpcSchemaTypeName(getMethodParamsSchema(method), fallback); } @@ -500,6 +505,7 @@ interface GoEventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventDeprecated: boolean; eventExperimental: boolean; dataExperimental: boolean; } @@ -593,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), }; @@ -3009,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)) { @@ -3212,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[] = []; @@ -3286,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(`)`); @@ -3529,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])] @@ -3555,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(`)`); @@ -3565,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(`)`); @@ -3822,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}`); @@ -3850,6 +3936,15 @@ async function generateRpc(schemaPath?: string): Promise { // Build a combined definition map, including shared API definitions plus // method-specific request/result wrapper types. rpcDefinitions = collectDefinitionCollections(schema as Record); + // Added inputs are optional pointer fields of the same request struct, so keyed literals are unchanged. + validateLegacyRequests( + schema, + (node) => collectRpcMethods(node), + getMethodParamsSchema, + (method) => !!method.params && !!getNullableInner(method.params) + ); + // Response structs gain optional pointer fields, so keyed literals are unchanged. + validateLegacyDefinitions(rpcDefinitions); const allDefinitions: Record = { ...Object.fromEntries( Object.entries(rpcDefinitions.$defs ?? {}).filter(([, value]) => typeof value === "object" && value !== null) @@ -4265,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})`); } @@ -4279,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(``); @@ -4419,7 +4520,7 @@ export function emitClientSessionApiRegistration(lines: string[], clientSchema: lines.push(``); } -function emitClientGlobalApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { +export function emitClientGlobalApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { const groups = collectClientGroups(clientSchema); for (const { groupName, groupNode, methods } of groups) { @@ -4569,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/legacy-parameters.ts b/scripts/codegen/legacy-parameters.ts new file mode 100644 index 0000000000..418aab2224 --- /dev/null +++ b/scripts/codegen/legacy-parameters.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Language-neutral `x-legacy-parameters` handling shared by every SDK generator. + * + * A request schema opts in by listing its original parameter set. Each generator keeps + * that published API with its own ordering and construction conventions, and exposes the + * full current schema through an extensible entry point where the language needs one. + * Future optional properties are added to the schema without changing the annotation. + * + * A response record can opt in too. Its legacy list names the properties the published + * record had, in schema property order, so languages with positional construction can keep + * that constructor while the record gains the new fields. + * + * This module is dependency-free so generators outside `src/sdk/scripts/codegen`, such + * as the Java generator, can import it directly. + */ + +export const LEGACY_PARAMETERS_KEY = "x-legacy-parameters"; + +interface LegacyParameterSchema { + properties?: Record; + required?: readonly string[]; + [LEGACY_PARAMETERS_KEY]?: unknown; +} + +export interface LegacyParameters { + /** The original parameter set, in declared order. */ + readonly legacy: readonly string[]; + /** Properties added after the legacy API, in schema property order. All are optional. */ + readonly additions: readonly string[]; + /** Schema-required properties, excluding implicit ones. */ + readonly required: ReadonlySet; +} + +export interface LegacyParameterOptions { + /** Properties the SDK supplies itself, such as a session-scoped `sessionId`. */ + readonly implicit?: readonly string[]; + /** Whether callers may omit the whole request. Optional requests cannot opt in. */ + readonly optional?: boolean; + /** Whether the request schema accepts null. Nullable requests cannot opt in. */ + readonly nullable?: boolean; + /** + * Whether the legacy list must follow schema property order, as a positional record + * constructor requires. + */ + readonly ordered?: boolean; +} + +/** True for the `anyOf: [{ not: {} }, …]` shape of a request callers may omit entirely. */ +export function isOmittableRequest(schema: unknown): boolean { + const anyOf = (schema as { anyOf?: unknown } | null | undefined)?.anyOf; + return ( + Array.isArray(anyOf) && + anyOf.some((variant) => { + const not = (variant as { not?: unknown } | null)?.not; + return typeof not === "object" && not !== null && Object.keys(not).length === 0; + }) + ); +} + +export function hasLegacyParameters(schema: unknown): boolean { + return typeof schema === "object" && schema !== null && LEGACY_PARAMETERS_KEY in schema; +} + +/** + * Reads and validates a request schema's legacy parameter set. + * + * Returns `undefined` when the schema has not opted in, so unmarked APIs keep their + * existing generation. Throws when the metadata would omit a required input, name an + * unknown property, or otherwise fail to preserve the original API. + */ +export function readLegacyParameters( + schema: unknown, + owner: string, + options: LegacyParameterOptions = {} +): LegacyParameters | undefined { + if (!hasLegacyParameters(schema)) return undefined; + const request = schema as LegacyParameterSchema; + const invalid = (reason: string): never => { + throw new Error(`Invalid ${LEGACY_PARAMETERS_KEY} for ${owner}: ${reason}`); + }; + const names = request[LEGACY_PARAMETERS_KEY]; + if (!Array.isArray(names) || !names.every((name): name is string => typeof name === "string")) { + return invalid("expected an array of property names"); + } + if (options.optional) return invalid("optional requests cannot declare legacy parameters"); + if (options.nullable) return invalid("nullable requests cannot declare legacy parameters"); + if (new Set(names).size !== names.length) return invalid("duplicate property names"); + + const implicit = new Set(options.implicit ?? []); + const properties = Object.keys(request.properties ?? {}).filter((name) => !implicit.has(name)); + const known = new Set(properties); + for (const name of names) { + if (implicit.has(name)) return invalid(`implicit property ${name} cannot be a legacy parameter`); + if (!known.has(name)) return invalid(`unknown property ${name}`); + } + + const required = new Set((request.required ?? []).filter((name) => !implicit.has(name))); + const legacy = new Set(names); + for (const name of required) { + if (!legacy.has(name)) return invalid(`required property ${name} must be a legacy parameter`); + } + const additions = properties.filter((name) => !legacy.has(name)); + if (additions.length === 0) return invalid("expected at least one property added after the legacy API"); + if (options.ordered && properties.filter((name) => legacy.has(name)).some((name, index) => name !== names[index])) { + return invalid("legacy parameters must follow schema property order"); + } + + return { legacy: names, additions, required }; +} + +/** Only requests the SDK sends can preserve a legacy API; handler payloads cannot opt in. */ +export function rejectLegacyParameters(schema: unknown, owner: string): void { + if (hasLegacyParameters(schema)) { + throw new Error(`Invalid ${LEGACY_PARAMETERS_KEY} for ${owner}: only server and session requests are supported`); + } +} + +/** The schema sections that can carry requests, keyed as in `api.schema.json`. */ +export interface LegacyRequestSections { + server?: Node; + session?: Node; + clientSession?: Node; + clientGlobal?: Node; +} + +/** + * Validates every request's `x-legacy-parameters` for generators whose projection needs no + * extra API: added inputs are optional fields of the same request type. + */ +export function validateLegacyRequests( + sections: LegacyRequestSections, + collect: (node: Node) => Method[], + params: (method: Method) => unknown, + nullable: (method: Method) => boolean +): void { + for (const [section, implicit] of [["server", []], ["session", ["sessionId"]]] as const) { + const node = sections[section]; + if (!node) continue; + for (const method of collect(node)) { + readLegacyParameters(params(method), method.rpcMethod, { + implicit, + optional: isOmittableRequest(method.params), + nullable: nullable(method), + }); + } + } + for (const section of ["clientSession", "clientGlobal"] as const) { + const node = sections[section]; + if (!node) continue; + for (const method of collect(node)) rejectLegacyParameters(params(method), method.rpcMethod); + } +} + +/** + * Validates `x-legacy-parameters` on every shared definition, including response records + * that no request path reads. Generators whose projection of an added field needs no extra + * API call this so malformed metadata fails identically in every language. + */ +export function validateLegacyDefinitions(collections: { + definitions?: Record; + $defs?: Record; +}): void { + for (const definitions of [collections.definitions, collections.$defs]) { + for (const [name, schema] of Object.entries(definitions ?? {})) { + readLegacyParameters(schema, name, { implicit: ["sessionId"] }); + } + } +} diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 74bee76eb6..1c66958585 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -7,7 +7,6 @@ */ import fs from "fs/promises"; -import path from "path"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { fileURLToPath } from "url"; import { @@ -18,6 +17,7 @@ import { getApiSchemaPath, getRpcSchemaTypeName, getSessionEventsSchemaPath, + isCodegenEntrypoint, isObjectSchema, isOpaqueJson, isVoidSchema, @@ -59,6 +59,7 @@ import { type RpcMethod, type SessionEventEnvelopeProperty, } from "./utils.js"; +import { readLegacyParameters } from "./legacy-parameters.js"; // ── Utilities ─────────────────────────────────────────────────────────────── @@ -113,16 +114,7 @@ function rewriteExternalRefsForPython(schema: JSONSchema7 & { definitions?: Reco if (!schema.definitions) schema.definitions = {}; for (const placeholder of placeholderNames.keys()) { if (!schema.definitions[placeholder]) { - const markerProperty = `__externalRefMarker_${placeholder}`; - schema.definitions[placeholder] = { - type: "object", - additionalProperties: false, - title: placeholder, - properties: { - [markerProperty]: { type: "string" }, - }, - required: [markerProperty], - }; + schema.definitions[placeholder] = pythonReferencePlaceholder(placeholder); } } } @@ -130,6 +122,17 @@ function rewriteExternalRefsForPython(schema: JSONSchema7 & { definitions?: Reco return { placeholderNames, imports }; } +function pythonReferencePlaceholder(placeholder: string): JSONSchema7 { + const markerProperty = `__externalRefMarker_${placeholder}`; + return { + type: "object", + additionalProperties: false, + title: placeholder, + properties: { [markerProperty]: { type: "string" } }, + required: [markerProperty], + }; +} + function placeholderToQuicktypeIdentifier(placeholder: string): string { return placeholder .replace(/^_+/, "") @@ -267,9 +270,74 @@ function preservePythonSessionEventConstructorOrder(schema: JSONSchema7): void { (resolvedPath as Record)["x-copilot-sdk-append-last"] = true; } } + + // Added after these constructors were published; sorting it with the + // other optional fields would shift `tool_call_id` and `warning` for + // positional callers. + for (const name of [ + "PermissionPromptRequestCommands", + "PermissionRequestRead", + "PermissionRequestShell", + "PermissionRequestWrite", + ]) { + const definition = definitions[name]; + const sandboxPathGrant = + definition && typeof definition === "object" + ? (definition as JSONSchema7).properties?.sandboxPathGrant + : undefined; + if (sandboxPathGrant && typeof sandboxPathGrant === "object") { + (sandboxPathGrant as Record)["x-copilot-sdk-append-last"] = true; + } + } } } +/** + * Optional RPC fields added after a type was first published. Python dataclasses expose + * their field order as a positional constructor contract, so these must stay last instead + * of being sorted in among the pre-existing optional fields. + */ +const PY_RPC_APPEND_LAST_FIELDS: ReadonlyArray = [ + ["ConnectorReconcileRequest", "forceConnectorName"], +]; + +/** + * Append-last entries whose property exists in this schema. An older or narrower schema + * selected for generation may predate the field, which is not an error; a field the + * schema declares but the generated dataclass lacks still fails generation. + */ +export function pythonAppendLastFieldsPresentIn( + definitions: Record, + fields: ReadonlyArray = PY_RPC_APPEND_LAST_FIELDS +): ReadonlyArray { + const result = fields.filter(([className, propertyName]) => { + const definition = definitions[className]; + return typeof definition === "object" && Object.hasOwn(definition.properties ?? {}, propertyName); + }); + const seen = new Set(result.map(([className, propertyName]) => `${className}.${propertyName}`)); + + for (const [className, definition] of Object.entries(definitions)) { + if (typeof definition !== "object") continue; + // Quicktype merges inline union variants into the definition's dataclass. + const shapes = [definition, ...(definition.anyOf ?? definition.oneOf ?? [])]; + for (const [propertyName, property] of shapes.flatMap((shape) => + typeof shape === "object" ? Object.entries(shape.properties ?? {}) : [] + )) { + if ( + typeof property !== "object" || + property["x-copilot-sdk-append-last"] !== true || + seen.has(`${className}.${propertyName}`) + ) { + continue; + } + result.push([className, propertyName]); + seen.add(`${className}.${propertyName}`); + } + } + + return result; +} + function preservePythonRpcStringDateFields(definitions: Record): void { const quotaSnapshot = definitions.AccountQuotaSnapshot; const resetDate = quotaSnapshot?.properties?.resetDate as JSONSchema7 | undefined; @@ -458,30 +526,8 @@ function postProcessRefBasedDiscriminatedUnionsForPython( for (const match of code.matchAll(/^class (\w+)[:\(]/gm)) { emittedClassNames.add(match[1]); } - const acronymCandidates = (name: string): string[] => { - const substitutions: Array<[RegExp, string]> = [ - [/Api/g, "API"], - [/Mcp/g, "MCP"], - [/Url/g, "URL"], - [/Json/g, "JSON"], - [/Http/g, "HTTP"], - [/Hmac/g, "HMAC"], - [/Tcp/g, "TCP"], - [/Sql/g, "SQL"], - [/Id\b/g, "ID"], - [/Llm/g, "LLM"], - [/Cli/g, "CLI"], - ]; - const results = new Set([name]); - for (const [pattern, replacement] of substitutions) { - for (const existing of [...results]) { - results.add(existing.replace(pattern, replacement)); - } - } - return [...results]; - }; const resolveActualName = (expected: string): string | undefined => { - for (const candidate of acronymCandidates(expected)) { + for (const candidate of pythonAcronymCandidates(expected)) { if (emittedClassNames.has(candidate)) return candidate; } return undefined; @@ -1345,7 +1391,8 @@ function removeRequiredAnyDefaultsForPython( } const requiredFields = resolved.required.map(toSnakeCase); - for (const className of new Set([definitionName, toPascalCase(definitionName)])) { + // Quicktype capitalizes acronyms, for example McpPromptMessage becomes MCPPromptMessage. + for (const className of new Set([definitionName.toLowerCase(), toPascalCase(definitionName).toLowerCase()])) { const fields = requiredFieldsByClass.get(className) ?? new Set(); for (const field of requiredFields) { fields.add(field); @@ -1356,7 +1403,7 @@ function removeRequiredAnyDefaultsForPython( const classBlockRe = /(@dataclass\r?\nclass\s+(\w+):[\s\S]*?)(?=^@dataclass|^class\s+\w|^def\s+\w|\Z)/gm; return code.replace(classBlockRe, (block: string, _classPrefix: string, className: string) => { - const requiredFields = requiredFieldsByClass.get(className); + const requiredFields = requiredFieldsByClass.get(className.toLowerCase()); if (!requiredFields) { return block; } @@ -1423,6 +1470,55 @@ function removeShadowedSessionEventEnumsForPython( .replace(/\n{3,}/g, "\n\n"); } +/** Quicktype applies acronym casing to class names; list every spelling it may emit. */ +function pythonAcronymCandidates(name: string): string[] { + const substitutions: Array<[RegExp, string]> = [ + [/Api/g, "API"], + [/Mcp/g, "MCP"], + [/Url/g, "URL"], + [/Json/g, "JSON"], + [/Http/g, "HTTP"], + [/Hmac/g, "HMAC"], + [/Tcp/g, "TCP"], + [/Sql/g, "SQL"], + [/Id\b/g, "ID"], + [/Llm/g, "LLM"], + [/Cli/g, "CLI"], + ]; + const results = new Set([name]); + for (const [pattern, replacement] of substitutions) { + for (const existing of [...results]) { + results.add(existing.replace(pattern, replacement)); + } + } + return [...results]; +} + +/** + * Applies `x-legacy-parameters` to generated request dataclasses: properties added after + * the legacy API become keyword-only with defaults, so existing positional construction + * keeps binding the original parameters. Required fields stay constructor arguments. + */ +export function applyPythonLegacyParameters( + code: string, + definitions: Record +): string { + const emitted = new Set([...code.matchAll(/^class (\w+)[:(]/gm)].map((match) => match[1])); + const handled = new Set(); + for (const [definitionName, schema] of Object.entries(definitions)) { + const legacy = readLegacyParameters(schema, definitionName, { implicit: ["sessionId"] }); + if (!legacy) continue; + const className = pythonAcronymCandidates(definitionName).find((candidate) => emitted.has(candidate)); + if (!className) throw new Error(`Missing dataclass for ${definitionName}`); + if (handled.has(className)) continue; + handled.add(className); + for (const addition of legacy.additions) { + code = makePythonDataclassFieldKeywordOnly(code, className, toSnakeCase(addition)); + } + } + return code; +} + function makePythonDataclassFieldKeywordOnly( code: string, className: string, @@ -1474,7 +1570,97 @@ function makePythonDataclassFieldKeywordOnly( return updated; } -function reorderPythonDataclassFields(code: string): string { +/** + * Move optional fields listed in {@link PY_RPC_APPEND_LAST_FIELDS} to the end of their + * dataclass so a field added after publication cannot shift the positional constructor + * contract of the fields that were already there. + */ +export function appendLastPythonRpcConstructorFields( + code: string, + fields: ReadonlyArray = PY_RPC_APPEND_LAST_FIELDS +): string { + const fieldRe = /^ (\w+): .* = .*$/; + const methodRe = /^ (?:@(?:staticmethod|classmethod|property)|(?:async\s+)?def\s+)/; + + let updated = code; + for (const [className, propertyName] of fields) { + const targetField = toSnakeCase(propertyName); + const classBlockRe = new RegExp( + `(@dataclass\\r?\\nclass\\s+${escapeRegExp(className)}:[\\s\\S]*?)(?=^@dataclass|^class\\s+\\w|^def\\s+\\w|(?![\\s\\S]))`, + "m" + ); + let foundClass = false; + updated = updated.replace(classBlockRe, (block: string) => { + foundClass = true; + const lines = block.split("\n"); + const memberStart = lines.findIndex((line, index) => index >= 2 && methodRe.test(line)); + if (memberStart < 0) { + throw new Error(`Missing from_dict constructor for ${className}`); + } + + const groups: string[][] = []; + const preamble: string[] = []; + let current: string[] | undefined; + for (const line of lines.slice(2, memberStart)) { + if (/^ \w+:/.test(line)) { + current = [line]; + groups.push(current); + } else if (current) { + current.push(line); + } else { + preamble.push(line); + } + } + + const targetIndex = groups.findIndex((group) => fieldRe.exec(group[0])?.[1] === targetField); + if (targetIndex < 0) { + throw new Error(`Missing dataclass field ${className}.${targetField}`); + } + if (targetIndex === groups.length - 1) return block; + + const reordered = [ + ...groups.slice(0, targetIndex), + ...groups.slice(targetIndex + 1), + groups[targetIndex], + ].map((group) => { + const trimmed = [...group]; + while (trimmed.length > 1 && trimmed[trimmed.length - 1].trim() === "") trimmed.pop(); + return trimmed; + }); + const fieldOrder = reordered + .map((group) => group[0].match(/^ (\w+):/)?.[1]) + .filter((name): name is string => Boolean(name)); + let foundConstructor = false; + const members = lines + .slice(memberStart) + .join("\n") + .replace( + new RegExp(`return ${escapeRegExp(className)}\\(([^()\\n]*)\\)`), + (_call: string, args: string) => { + const parsed = args.split(",").map((arg) => arg.trim()); + if (parsed.length !== fieldOrder.length || !parsed.every((arg) => fieldOrder.includes(arg))) { + throw new Error(`Unexpected from_dict constructor arguments for ${className}`); + } + foundConstructor = true; + return `return ${className}(${fieldOrder.join(", ")})`; + } + ); + // Reordering fields without rewriting the positional constructor call would + // silently bind every later argument to the wrong field. + if (!foundConstructor) { + throw new Error(`Missing from_dict constructor for ${className}`); + } + + return [...lines.slice(0, 2), ...preamble, ...reordered.flat(), "", members].join("\n"); + }); + if (!foundClass) { + throw new Error(`Missing dataclass ${className}`); + } + } + return updated; +} + +function reorderPythonDataclassFields(code: string, keywordOnlyLastInClass?: string): string { const fieldRe = /^ \w+: (?:Any|bool|int|float|str|dict|list|ClassVar|[A-Z_]\w*|['"][A-Z_]\w*)(?:[^=]*)?(?: = .*)?$/; const methodRe = /^ (?:@(?:staticmethod|classmethod|property)|(?:async\s+)?def\s+)/; @@ -1515,7 +1701,13 @@ function reorderPythonDataclassFields(code: string): string { const required = groups.filter((group) => !group[0].includes(" = ")); const optional = groups.filter((group) => group[0].includes(" = ")); - const reorderedGroups = [...required, ...optional]; + const reorderedGroups = block.startsWith(`@dataclass\nclass ${keywordOnlyLastInClass}:`) + ? [ + ...required, + ...optional.filter((group) => !group[0].includes("kw_only=True")), + ...optional.filter((group) => group[0].includes("kw_only=True")), + ] + : [...required, ...optional]; const changed = reorderedGroups.some((group, index) => group !== groups[index]); if (!changed) { return block; @@ -1580,8 +1772,12 @@ function getMethodResultSchema(method: RpcMethod): JSONSchema7 | undefined { return resolveSchema(method.result, rpcDefinitions) ?? method.result ?? undefined; } -function isPythonObjectResultSchema(schema: JSONSchema7 | undefined): boolean { +export function isPythonObjectResultSchema( + schema: JSONSchema7 | undefined, + definitions: DefinitionCollections = rpcDefinitions, +): boolean { if (!schema) return false; + schema = resolveSchema(schema, definitions) ?? schema; if (isObjectSchema(schema)) return true; const variants = schema.anyOf ?? schema.oneOf; @@ -1589,7 +1785,7 @@ function isPythonObjectResultSchema(schema: JSONSchema7 | undefined): boolean { const nonNullVariants = variants .filter((variant): variant is JSONSchema7 => typeof variant === "object" && variant !== null) - .map((variant) => resolveObjectSchema(variant, rpcDefinitions) ?? resolveSchema(variant, rpcDefinitions) ?? variant) + .map((variant) => resolveObjectSchema(variant, definitions) ?? resolveSchema(variant, definitions) ?? variant) .filter( (variant) => variant.type !== "null" && @@ -1601,7 +1797,7 @@ function isPythonObjectResultSchema(schema: JSONSchema7 | undefined): boolean { ); if (nonNullVariants.length === 1) { - return isPythonObjectResultSchema(nonNullVariants[0]); + return isPythonObjectResultSchema(nonNullVariants[0], definitions); } return nonNullVariants.length > 1 && findPyDiscriminator(nonNullVariants) !== null; @@ -1617,6 +1813,10 @@ function getMethodParamsSchema(method: RpcMethod): JSONSchema7 | undefined { } function pythonResultTypeName(method: RpcMethod, schemaOverride?: JSONSchema7): string { + // Session-scoped methods retain their existing method-specific result names. + if (!schemaOverride && !method.rpcMethod.startsWith("session.") && method.result?.$ref) { + return toPascalCase(refTypeName(method.result.$ref, rpcDefinitions)); + } const schema = schemaOverride ?? getMethodResultSchema(method); // If schema is a $ref, derive the type name from the ref path if (schema?.$ref) { @@ -1644,6 +1844,9 @@ function pythonParamsTypeName(method: RpcMethod): string { if (method.rpcMethod.startsWith("session.") && method.params?.$ref) { return fallback; } + if (method.params?.$ref) { + return toPascalCase(refTypeName(method.params.$ref, rpcDefinitions)); + } const schema = getMethodParamsSchema(method); if (schema?.$ref) return toPascalCase(refTypeName(schema.$ref, rpcDefinitions)); return getRpcSchemaTypeName(schema, fallback); @@ -2081,10 +2284,17 @@ function resolvePyNamedUnion( }; } +/** + * Enums whose contract defines `unknown` as "outside the supported vocabulary". The runtime + * contract decodes later values as `unknown` (`#[serde(other)]`); Python does the same instead + * of failing the whole event. + */ +const COLLAPSE_UNKNOWN_PYTHON_ENUMS = new Set(["PermissionApprovalEvaluationReasonCode"]); + function getOrCreatePyEnum( enumName: string, values: string[], - ctx: PyCodegenCtx, + ctx: Pick, description?: string, enumValueDescriptions?: EnumValueDescriptions, deprecated?: boolean, @@ -2117,6 +2327,23 @@ function getOrCreatePyEnum( } lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`); } + if (enumName === "ToolExecutionCompleteFileEditKind") { + lines.push(``); + lines.push(` @classmethod`); + lines.push(` def _missing_(cls, value: object) -> "ToolExecutionCompleteFileEditKind | None":`); + lines.push(` if not isinstance(value, str):`); + lines.push(` return None`); + lines.push(` member = object.__new__(cls)`); + lines.push(` member._name_ = "UNKNOWN"`); + lines.push(` member._value_ = value`); + lines.push(` return cls._value2member_map_.setdefault(value, member)`); + } else if (COLLAPSE_UNKNOWN_PYTHON_ENUMS.has(enumName)) { + if (!values.includes("unknown")) throw new Error(`${enumName} has no "unknown" value to collapse into`); + lines.push(``); + lines.push(` @classmethod`); + lines.push(` def _missing_(cls, value: object) -> "${enumName} | None":`); + lines.push(` return cls.UNKNOWN if isinstance(value, str) else None`); + } ctx.enumsByName.set(enumName, enumName); ctx.enums.push(lines.join("\n")); return enumName; @@ -3102,6 +3329,24 @@ async function generateSessionEvents(schemaPath?: string): Promise { // ── RPC Types ─────────────────────────────────────────────────────────────── +function preserveUnknownPythonEnumValues(code: string, name: string): string { + const declaration = new RegExp(`^class ${name}\\(Enum\\):\\n(?:[ \\t].*\\n|\\n)*`, "m"); + const match = code.match(declaration); + if (!match) throw new Error(`Missing generated ${name} enum`); + const method = ` + @classmethod + def _missing_(cls, value: object) -> ${name} | None: + if not isinstance(value, str): + return None + member = object.__new__(cls) + member._name_ = "UNKNOWN" + member._value_ = value + return cls._value2member_map_.setdefault(value, member) + +`; + return code.replace(declaration, `${match[0].trimEnd()}\n${method}`); +} + async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema7): Promise { console.log("Python: generating RPC types..."); const { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } = await import("quicktype-core"); @@ -3196,6 +3441,30 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema }; const externalRefs = rewriteExternalRefsForPython(singleSchema as JSONSchema7 & { definitions?: Record }); const externalEnumNames = collectPythonExternalEnumNames(sessionEventsSchema, externalRefs.placeholderNames); + // Discovery and session availability are independently versioned despite identical values. + // Hide the discovery enum from quicktype's enum merging, then restore it with our enum emitter. + const distinctEnums: Pick = { + enumsByName: new Map(), + enums: [], + }; + for (const name of ["ConnectorDiscoveryAvailability"]) { + const definition = allDefinitions[name]; + if (!definition) continue; + if (!definition.enum?.every((value): value is string => typeof value === "string")) { + throw new Error(`Expected a string enum for ${name}`); + } + getOrCreatePyEnum( + name, + definition.enum, + distinctEnums, + definition.description, + getEnumValueDescriptions(definition) + ); + const placeholder = `__ExternalRef_${name}`; + (singleSchema.definitions as Record)[name] = pythonReferencePlaceholder(placeholder); + externalRefs.placeholderNames.set(placeholder, name); + externalEnumNames.add(name); + } const externalDiscriminatedUnionNames = collectPythonExternalDiscriminatedUnionNames( sessionEventsSchema, externalRefs.placeholderNames @@ -3242,6 +3511,10 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema ); typesCode = removeRequiredAnyDefaultsForPython(typesCode, allDefinitions, allDefinitionCollections); typesCode = reorderPythonDataclassFields(typesCode); + typesCode = appendLastPythonRpcConstructorFields( + typesCode, + pythonAppendLastFieldsPresentIn(allDefinitions) + ); // Fix bare except: to use Exception (required by ruff/pylint) typesCode = typesCode.replace(/except:/g, "except Exception:"); // Remove unnecessary pass when class has methods (quicktype generates pass for empty schemas) @@ -3257,6 +3530,7 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema externalEnumNames, externalDiscriminatedUnionNames ); + typesCode += `\n\n${distinctEnums.enums.join("\n\n")}\n`; typesCode = removeShadowedSessionEventEnumsForPython( typesCode, externalRefs.imports.get(".session_events") ?? new Set(), @@ -3295,11 +3569,20 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema // Reorder class/enum definitions to resolve forward references. // Quicktype may emit classes before their dependencies are defined. typesCode = reorderPythonForwardRefs(typesCode); + for (const name of ["CustomizationReloadStatus", "CustomizationReloadSubsystem"]) { + if (Object.hasOwn(allDefinitions, name)) { + typesCode = preserveUnknownPythonEnumValues(typesCode, name); + } + } typesCode = makePythonDataclassFieldKeywordOnly( typesCode, "MCPServerConfigHTTP", "oauth_scopes" ); + typesCode = applyPythonLegacyParameters(typesCode, allDefinitions); + // Preserve the published positional SQLite capability when adding binary reads. + typesCode = makePythonDataclassFieldKeywordOnly(typesCode, "SessionFSSetProviderCapabilities", "binary"); + typesCode = reorderPythonDataclassFields(typesCode, "SessionFSSetProviderCapabilities"); // Strip quicktype's import block and preamble — we provide our own unified header. // The preamble ends just before the first helper function (e.g. "def from_str") @@ -3377,12 +3660,25 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema rootFieldTypes.set(match[1], match[2]); } } + const nullableAliasTargets = new Map(); + for (const [defName, definition] of Object.entries(allDefinitions)) { + if (!definition || typeof definition !== "object" || !Array.isArray(definition.anyOf)) continue; + const branches = definition.anyOf.filter((branch): branch is JSONSchema7 => typeof branch === "object"); + // Require a real `type: "null"` branch; the `{ "not": {} }` omission sentinel is not a wire null. + if (!branches.some((branch) => branch.type === "null")) continue; + const refBranches = branches.filter((branch) => typeof branch.$ref === "string"); + if (refBranches.length !== 1) continue; + nullableAliasTargets.set(defName, refBranches[0].$ref!.split("/").pop()!); + } for (const defName of Object.keys(allDefinitions)) { const actualName = rootFieldTypes.get(toSnakeCase(defName)); if (actualName) { definitionAliases.set(defName.toLowerCase(), actualName); if (actualName !== defName && !actualTypeNames.has(defName.toLowerCase()) && /^[A-Za-z_]\w*$/.test(defName)) { - publicTypeAliases.set(defName, actualName); + publicTypeAliases.set( + defName, + nullableAliasTargets.get(defName) === actualName ? `${actualName} | None` : actualName + ); } } } @@ -3580,6 +3876,7 @@ def _patch_model_capabilities(data: dict) -> dict: } } + finalCode = appendPythonCompatibilityAliases(finalCode); finalCode = appendPythonRpcAllList(finalCode, rpcDefinitions); const outPath = await writeGeneratedFile("python/copilot/generated/rpc.py", finalCode); @@ -3676,6 +3973,21 @@ function appendPythonRpcAllList(code: string, _definitions: { definitions: Recor return code.replace(/\s*$/, "") + "\n\n" + renderPythonAllList([...exported].sort()) + "\n"; } +function appendPythonCompatibilityAliases(code: string): string { + const aliases: string[] = []; + if (/\bclass CatalogInstallability\b/.test(code) && !/\bclass CatalogCandidateInstallability\b/.test(code) && !/^CatalogCandidateInstallability\s*=/m.test(code)) { + aliases.push("CatalogCandidateInstallability = CatalogInstallability"); + } + if (/\bclass CatalogMCPServerInstallability\b/.test(code) && !/\bclass CatalogMCPServerInstallabilityEnum\b/.test(code) && !/^CatalogMCPServerInstallabilityEnum\s*=/m.test(code)) { + aliases.push("CatalogMCPServerInstallabilityEnum = CatalogMCPServerInstallability"); + } + if (/\bclass CatalogMCPServerInstallability\b/.test(code) && !/\bclass CatalogMcpServerInstallability\b/.test(code) && !/^CatalogMcpServerInstallability\s*=/m.test(code)) { + aliases.push("CatalogMcpServerInstallability = CatalogMCPServerInstallability"); + } + if (aliases.length === 0) return code; + return `${code.replace(/\s*$/, "")}\n\n# Backward-compatible public aliases retained across typed-review generation.\n${aliases.join("\n")}\n`; +} + function renderPythonAllList(names: string[]): string { const lines: string[] = ["__all__ = ["]; for (const name of names) { @@ -3804,7 +4116,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); } -function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, groupExperimental = false, groupDeprecated = false): void { +export function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, groupExperimental = false, groupDeprecated = false): void { const isInternal = method.visibility === "internal"; const methodName = (isInternal ? "_" : "") + toSnakeCase(name); const resultSchema = getMethodResultSchema(method); @@ -3862,7 +4174,7 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: ? `${innerTypeName}.from_dict(${expr}) if ${expr} is not None else None` : `${innerTypeName}(${expr}) if ${expr} is not None else None`; } - return resultIsObject ? `${innerTypeName}.from_dict(${expr})` : `${innerTypeName}(${expr})`; + return resultIsObject && innerTypeName !== "dict" ? `${innerTypeName}.from_dict(${expr})` : `${innerTypeName}(${expr})`; }; // Build request body with proper serialization/deserialization @@ -4045,7 +4357,7 @@ function emitClientSessionRegistrationMethod( lines.push(` client.set_request_handler("${method.rpcMethod}", ${handlerVariableName})`); } -function emitClientGlobalApiRegistration( +export function emitClientGlobalApiRegistration( lines: string[], node: Record, resolveType: (name: string) => string @@ -4126,7 +4438,7 @@ function emitClientGlobalRegistrationMethod( // notification path (an `id`-less message never reaches a request // handler), so register on the method-specific notification registry. lines.push(` async def ${handlerVariableName}(params: dict) -> None:`); - lines.push(` request = ${paramsType}.from_dict(params)`); + lines.push(` request = ${paramsType === "dict" ? "dict(params)" : `${paramsType}.from_dict(params)`}`); lines.push(` handler = handlers.${handlerField}`); lines.push(` if handler is None: return None`); lines.push(` await handler.${handlerMethod}(request)`); @@ -4136,12 +4448,12 @@ function emitClientGlobalRegistrationMethod( } lines.push(` async def ${handlerVariableName}(params: dict) -> dict | None:`); - lines.push(` request = ${paramsType}.from_dict(params)`); + lines.push(` request = ${paramsType === "dict" ? "dict(params)" : `${paramsType}.from_dict(params)`}`); lines.push(` handler = handlers.${handlerField}`); lines.push(` if handler is None: raise RuntimeError("No ${handlerField} client-global handler registered")`); if (hasResult) { lines.push(` result = await handler.${handlerMethod}(request)`); - if (isObjectSchema(resultSchema)) { + if (isObjectSchema(resultSchema) && resolveType(pythonResultTypeName(method)) !== "dict") { lines.push(` return result.to_dict()`); } else { lines.push(` return result.value if hasattr(result, 'value') else result`); @@ -4178,7 +4490,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro const __filename = fileURLToPath(import.meta.url); -if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { +if (isCodegenEntrypoint(process.argv[1], __filename)) { const sessionArg = process.argv[2] || undefined; const apiArg = process.argv[3] || undefined; generate(sessionArg, apiArg).catch((err) => { diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 875082e893..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, @@ -55,6 +55,13 @@ import { stripBooleanLiterals, type EnumValueDescriptions, } from "./utils.js"; +import { + isOmittableRequest, + LEGACY_PARAMETERS_KEY, + readLegacyParameters, + rejectLegacyParameters, + type LegacyParameters, +} from "./legacy-parameters.js"; const execFileAsync = promisify(execFile); @@ -208,6 +215,8 @@ interface RustCodegenCtx { enums: string[]; /** Track generated type names to avoid duplicates. */ generatedNames: Set; + /** Value set and origin of each emitted string enum, so two literals never share one name. */ + stringEnums: Map; /** * Generated type names that do not (and cannot trivially) implement * `Default` — currently `#[serde(untagged)]` enums of distinct payload @@ -220,7 +229,7 @@ interface RustCodegenCtx { experimentalTypeNames: Set; /** Schema definitions for $ref resolution. */ definitions?: DefinitionCollections; - /** When set, only these const-valued properties are accepted as union discriminators. */ + /** Discriminator names accepted in addition to required, enum-referenced string constants. */ unionDiscriminatorProperties?: Set; /** Whether unions without a const-valued discriminator should be emitted. */ allowUntaggedUnions: boolean; @@ -299,11 +308,36 @@ function findRustDiscriminator(variants: RustUnionVariant[]): string | null { return null; } +function hasRequiredReferencedStringDiscriminator( + variants: RustUnionVariant[], + discriminator: string, + ctx: RustCodegenCtx, +): boolean { + return variants.every(({ schema }) => { + const property = schema.properties?.[discriminator]; + if ( + !schema.required?.includes(discriminator) || + typeof property !== "object" || + !property.$ref || + typeof property.const !== "string" + ) { + return false; + } + const referenced = resolveRef(property.$ref, ctx.definitions); + return referenced?.type === "string" && referenced.enum?.includes(property.const) === true; + }); +} + +/** Discriminators recognised before the MCP installation payloads were typed. */ +const RELEASED_UNION_DISCRIMINATORS: ReadonlySet = new Set(["kind", "resource"]); + function tryEmitRustUnion( schema: JSONSchema7, parentTypeName: string, jsonPropName: string, ctx: RustCodegenCtx, + isRequired = true, + standalone = false, ): string | null { const variants = getUnionVariants(schema); if (!variants) return null; @@ -313,7 +347,7 @@ function tryEmitRustUnion( const enumName = (typeof schema.title === "string" && schema.title) || - parentTypeName + toPascalCase(jsonPropName); + parentTypeName + (jsonPropName ? toPascalCase(jsonPropName) : ""); const isAllowedUnionType = ctx.allowedUnionTypeNames.has(enumName); const resolvedVariants: RustUnionVariant[] = []; @@ -364,18 +398,24 @@ function tryEmitRustUnion( const discriminator = findRustDiscriminator(resolvedVariants); if (discriminator) { + // A definition emitted on its own, not reached through a field, keeps the released + // discriminator rule, so newer rules add no standalone types. + const discriminators = standalone ? RELEASED_UNION_DISCRIMINATORS : ctx.unionDiscriminatorProperties; if ( - ctx.unionDiscriminatorProperties && - !ctx.unionDiscriminatorProperties.has(discriminator) && + discriminators && + !discriminators.has(discriminator) && + (standalone || !isRequired || !hasRequiredReferencedStringDiscriminator(resolvedVariants, discriminator, ctx)) && !isAllowedUnionType ) { + // Newly recognised unions must not narrow existing optional raw metadata: + // consumers need malformed/future payloads intact for bounded degradation. return null; } } else if (!ctx.allowUntaggedUnions && !isAllowedUnionType) { return null; } - if (ctx.generatedNames.has(enumName)) { + if (hasGeneratedRustType(enumName, ctx)) { return enumName; } ctx.generatedNames.add(enumName); @@ -453,13 +493,14 @@ function makeCtx( typeAliases: [], enums: [], generatedNames: new Set(), + stringEnums: new Map(), nonDefaultableTypes: new Set(options.nonDefaultableTypes ?? []), experimentalTypeNames: new Set(options.experimentalTypeNames ?? []), definitions, unionDiscriminatorProperties: options.unionDiscriminatorProperties === null ? undefined - : (options.unionDiscriminatorProperties ?? new Set(["kind"])), + : (options.unionDiscriminatorProperties ?? new Set(["kind", "action", "phase", "resource"])), allowUntaggedUnions: options.allowUntaggedUnions ?? false, allowedUnionTypeNames: new Set(options.allowedUnionTypeNames ?? []), strictBooleanConstFields: new Map(), @@ -562,6 +603,10 @@ function rustMapType( return `HashMap`; } +function hasGeneratedRustType(typeName: string, ctx: RustCodegenCtx): boolean { + return ctx.generatedNames.has(typeName); +} + function emitRustTypeAlias( typeName: string, schema: JSONSchema7, @@ -569,7 +614,7 @@ function emitRustTypeAlias( ctx: RustCodegenCtx, description?: string, ): void { - if (ctx.generatedNames.has(typeName)) return; + if (hasGeneratedRustType(typeName, ctx)) return; ctx.generatedNames.add(typeName); const lines: string[] = []; @@ -592,7 +637,7 @@ function emitRustArrayAlias( ctx: RustCodegenCtx, description?: string, ): void { - if (ctx.generatedNames.has(typeName)) return; + if (hasGeneratedRustType(typeName, ctx)) return; emitRustTypeAlias( typeName, schema, @@ -608,7 +653,7 @@ function emitRustMapAlias( ctx: RustCodegenCtx, description?: string, ): void { - if (ctx.generatedNames.has(typeName)) return; + if (hasGeneratedRustType(typeName, ctx)) return; emitRustTypeAlias( typeName, schema, @@ -651,7 +696,7 @@ function emitRustScalarAlias( ctx: RustCodegenCtx, description?: string, ): void { - if (ctx.generatedNames.has(typeName)) return; + if (hasGeneratedRustType(typeName, ctx)) return; const scalarType = rustScalarType(schema); if (!scalarType) return; emitRustTypeAlias(typeName, schema, scalarType, ctx, description); @@ -705,6 +750,24 @@ function rustRefTypeName(ref: string, definitions?: DefinitionCollections): stri return toPascalCase(externalRef?.definitionName ?? refTypeName(ref, definitions)); } +function isRustNullableSchema( + schema: JSONSchema7, + definitions: DefinitionCollections | undefined, + seen = new Set(), +): boolean { + const resolved = resolveSchema(schema, definitions); + if (!resolved || seen.has(resolved)) return false; + seen.add(resolved); + return resolved.type === "null" || + (Array.isArray(resolved.type) && resolved.type.includes("null")) || + (getUnionVariants(resolved)?.some( + (variant) => typeof variant === "object" && isRustNullableSchema(variant, definitions, seen), + ) ?? false) || + (resolved.type === undefined && (resolved.allOf?.every( + (variant) => typeof variant === "object" && isRustNullableSchema(variant, definitions, new Set(seen)), + ) ?? false)); +} + /** * Map a JSON Schema to a Rust type string. Emits nested type definitions as * side effects into ctx. @@ -722,6 +785,7 @@ function resolveRustType( return wrapOption("bool", isRequired); } + // $ref — resolve and recurse if (propSchema.$ref && typeof propSchema.$ref === "string") { recordExternalRustTypeRef(propSchema.$ref, ctx); @@ -744,7 +808,7 @@ function resolveRustType( return wrapOption(typeName, isRequired); } return resolveRustType( - resolved, + getUnionVariants(resolved) ? { ...resolved, title: resolved.title ?? typeName } : resolved, parentTypeName, jsonPropName, isRequired, @@ -761,9 +825,10 @@ function resolveRustType( parentTypeName, jsonPropName, ctx, + isRequired, ); if (unionType) { - return wrapOption(unionType, isRequired); + return wrapOption(unionType, isRequired && !isRustNullableSchema(propSchema, ctx.definitions)); } const nonNull = (propSchema.anyOf as JSONSchema7[]).filter( @@ -798,9 +863,10 @@ function resolveRustType( parentTypeName, jsonPropName, ctx, + isRequired, ); if (unionType) { - return wrapOption(unionType, isRequired); + return wrapOption(unionType, isRequired && !isRustNullableSchema(propSchema, ctx.definitions)); } const nonNull = (propSchema.oneOf as JSONSchema7[]).filter( @@ -814,7 +880,7 @@ function resolveRustType( true, ctx, ); - return wrapOption(innerType, isRequired); + return wrapOption(innerType, isRequired && !isRustNullableSchema(propSchema, ctx.definitions)); } return wrapOption("serde_json::Value", isRequired); } @@ -846,7 +912,9 @@ function resolveRustType( // const — just a string if (propSchema.const !== undefined) { if (typeof propSchema.const === "string") { - const enumName = (propSchema.title as string) || nestedName; + // A title on a single literal names the union enum other generators infer; each + // Rust literal is its own type, so name it from its owner as untitled literals are. + const enumName = nestedName; emitRustConstStringEnum( enumName, propSchema.const, @@ -950,7 +1018,7 @@ function emitRustStruct( ctx: RustCodegenCtx, description?: string, ): void { - if (ctx.generatedNames.has(typeName)) return; + if (hasGeneratedRustType(typeName, ctx)) return; ctx.generatedNames.add(typeName); const required = new Set(schema.required || []); @@ -1158,6 +1226,35 @@ 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 + * name matches an existing value enum reuses it, as released output does. + */ +function claimRustStringEnum( + enumName: string, + values: readonly string[], + fromConst: boolean, + ctx: RustCodegenCtx, +): boolean { + const key = JSON.stringify([...values].sort()); + const previous = ctx.stringEnums.get(enumName); + if (previous !== undefined) { + if (previous.values !== key && (fromConst ? previous.fromConst : true)) { + throw new Error(`Rust string enum ${enumName} is requested for different values ${previous.values} and ${key}`); + } + return false; + } + if (hasGeneratedRustType(enumName, ctx)) return false; + ctx.stringEnums.set(enumName, { values: key, fromConst }); + ctx.generatedNames.add(enumName); + return true; +} + function emitRustStringEnum( enumName: string, values: string[], @@ -1166,10 +1263,11 @@ function emitRustStringEnum( enumValueDescriptions?: EnumValueDescriptions, experimental = false, ): void { - if (ctx.generatedNames.has(enumName)) return; - ctx.generatedNames.add(enumName); + 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}`); @@ -1177,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. @@ -1193,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")); } @@ -1218,8 +1357,7 @@ function emitRustConstStringEnum( ctx: RustCodegenCtx, description?: string, ): void { - if (ctx.generatedNames.has(enumName)) return; - ctx.generatedNames.add(enumName); + if (!claimRustStringEnum(enumName, [value], true, ctx)) return; const lines: string[] = []; if (description) { @@ -1254,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. */ @@ -1302,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), }; @@ -1320,6 +1460,7 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { { allowUntaggedUnions: true, allowedUnionTypeNames: [ + "HumanResponseRecordedResponse", "ToolExecutionCompleteContent", "ToolExecutionCompleteContentResourceDetails", ], @@ -1366,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, @@ -1393,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, @@ -1491,6 +1638,25 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { out.push(""); } + if (ctx.generatedNames.has("InstallationReview")) { + out.push("/// Compatibility alias for the confirmation review wrapper."); + out.push("pub type InstallationConfirmationRequestReview = InstallationReview;"); + out.push(""); + out.push("/// Resource discriminator for installation confirmation reviews."); + out.push("#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]"); + out.push("pub enum InstallationReviewResource {"); + out.push(" #[serde(rename = \"mcp\")]"); + out.push(" #[default]"); + out.push(" Mcp,"); + out.push(" #[serde(rename = \"skill\")]"); + out.push(" Skill,"); + out.push("}"); + out.push(""); + out.push("/// Compatibility alias for the confirmation review resource discriminator."); + out.push("pub type InstallationConfirmationRequestReviewResource = InstallationReviewResource;"); + out.push(""); + } + return out.join("\n"); } @@ -1552,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`, @@ -1618,6 +1792,147 @@ function isNullableParamsSchema( return !!resolved && !!getNullableInner(resolved); } +// ── x-legacy-parameters ───────────────────────────────────────────────────── + +/** + * A request that declares `x-legacy-parameters`. Rust keeps the published struct and + * method with only the legacy fields, and adds a private-field options type with a + * `new(required...)` constructor, fluent setters and one `*_with_options` method. + */ +interface RustLegacyRequest { + legacy: LegacyParameters; + /** The full current request schema, excluding the session-scoped `sessionId`. */ + schema: JSONSchema7; + optionsName: string; +} + +function rustLegacyRequest( + method: RpcMethod, + defCollections: DefinitionCollections, + isSession: boolean, +): RustLegacyRequest | undefined { + const schema = getMethodParamsObjectSchema(method, defCollections, isSession); + const legacy = readLegacyParameters(schema, method.rpcMethod, { + optional: isOmittableRequest(method.params), + nullable: !!method.params && isNullableParamsSchema(method.params, defCollections), + }); + if (!legacy || !schema) return undefined; + const paramsName = rustParamsTypeName(method, defCollections); + return { + legacy, + schema, + optionsName: `${paramsName.replace(/(Request|Params)$/, "")}Options`, + }; +} + +/** The published request schema: the current one without properties added after it. */ +function rustLegacyStructSchema( + schema: JSONSchema7, + request: RustLegacyRequest, + owner: string, +): JSONSchema7 { + const additions = new Set(request.legacy.additions); + if (Object.hasOwn(schema.properties ?? {}, "sessionId")) { + throw new Error(`Invalid ${LEGACY_PARAMETERS_KEY} for ${owner}: the request struct must not carry sessionId`); + } + return { + ...schema, + properties: Object.fromEntries( + Object.entries(schema.properties ?? {}).filter(([name]) => !additions.has(name)), + ), + }; +} + +function emitRustLegacyOptions( + legacyName: string, + request: RustLegacyRequest, + ctx: RustCodegenCtx, +): void { + const { optionsName, schema, legacy } = request; + if ( + ctx.generatedNames.has(optionsName) || + Object.hasOwn(ctx.definitions?.definitions ?? {}, optionsName) || + Object.hasOwn(ctx.definitions?.$defs ?? {}, optionsName) + ) { + throw new Error(`Conflicting Rust type ${optionsName} for the ${legacyName} options`); + } + ctx.generatedNames.add(optionsName); + const properties = schema.properties ?? {}; + const field = (propName: string, isReq: boolean) => { + const prop = properties[propName] as JSONSchema7; + const rustType = resolveRustType(prop, legacyName, propName, isReq, ctx); + if (!isReq && !rustType.startsWith("Option<")) { + throw new Error(`Invalid ${LEGACY_PARAMETERS_KEY} for ${legacyName}: optional ${propName} must be an Option`); + } + return { propName, prop, rustField: safeRustFieldName(propName), rustType }; + }; + const required = legacy.legacy.filter((name) => legacy.required.has(name)).map((name) => field(name, true)); + const legacyOptional = legacy.legacy.filter((name) => !legacy.required.has(name)).map((name) => field(name, false)); + const additions = legacy.additions.map((name) => field(name, false)); + const experimental = isSchemaExperimental(schema) || ctx.experimentalTypeNames.has(legacyName); + + const lines: string[] = []; + lines.push(`/// Extensible [\`${legacyName}\`], including inputs added after it was published.`); + lines.push("///"); + lines.push(`/// Required inputs are [\`${optionsName}::new\`] arguments; optional inputs have fluent setters.`); + lines.push("/// Input-only: it serialises to the flat wire request and is not deserialisable."); + pushRustExperimentalDocs(lines, experimental); + lines.push("#[derive(Debug, Clone, Serialize)]"); + lines.push(`#[serde(rename_all = "camelCase")]`); + lines.push(`pub struct ${optionsName} {`); + lines.push(" #[serde(flatten)]"); + lines.push(` legacy: ${legacyName},`); + for (const { propName, rustField, rustType } of additions) { + const rename = snakeToCamelCase(toRustFieldName(propName)) !== propName ? `rename = "${propName}", ` : ""; + lines.push(` #[serde(${rename}skip_serializing_if = "Option::is_none")]`); + lines.push(` ${rustField}: ${rustType},`); + } + lines.push("}"); + lines.push(""); + lines.push(`impl ${optionsName} {`); + lines.push(" /// Creates options with the required inputs."); + const stringInput = (rustType: string) => rustType === "String"; + const parameters = required.map(({ rustField, rustType }) => + `${rustField}: ${stringInput(rustType) ? "impl Into" : rustType}`, + ); + lines.push(` pub fn new(${parameters.join(", ")}) -> Self {`); + lines.push(" Self {"); + const legacyFields = [ + ...required.map(({ rustField, rustType }) => + stringInput(rustType) ? `${rustField}: ${rustField}.into()` : rustField, + ), + ...legacyOptional.map(({ rustField }) => `${rustField}: None`), + ]; + lines.push(` legacy: ${legacyName} { ${legacyFields.join(", ")} },`); + for (const { rustField } of additions) { + lines.push(` ${rustField}: None,`); + } + lines.push(" }"); + lines.push(" }"); + for (const [target, fields] of [["self.legacy", legacyOptional], ["self", additions]] as const) { + for (const { prop, rustField, rustType } of fields) { + const inner = stripOption(rustType); + const [argType, value] = inner === "String" ? ["impl Into", "value.into()"] : [inner, "value"]; + lines.push(""); + pushRustDoc(lines, prop.description ?? `Sets \`${rustField}\`.`, " "); + lines.push(` pub fn ${rustField}(mut self, value: ${argType}) -> Self {`); + lines.push(` ${target}.${rustField} = Some(${value});`); + lines.push(" self"); + lines.push(" }"); + } + } + lines.push("}"); + if (required.length === 0) { + lines.push(""); + lines.push(`impl Default for ${optionsName} {`); + lines.push(" fn default() -> Self {"); + lines.push(" Self::new()"); + lines.push(" }"); + lines.push("}"); + } + ctx.structs.push(lines.join("\n")); +} + export function generateApiTypesCode( apiSchema: ApiSchema, nonDefaultableTypes: Iterable = [], @@ -1726,12 +2041,43 @@ export function generateApiTypesCode( tryEmitRustUnion(definition as JSONSchema7, name, "", ctx); } + const legacyRequests = new Map(); + const handlerMethods = new Set( + collectRpcMethods((apiSchema.clientSession ?? {}) as Record), + ); + for (const method of handlerMethods) { + rejectLegacyParameters(method.params && resolveSchema(method.params, defCollections), method.rpcMethod); + } + for (const { method, isSession } of methodEntries) { + if (handlerMethods.has(method)) continue; + const request = rustLegacyRequest(method, defCollections, isSession); + if (request) legacyRequests.set(rustParamsTypeName(method, defCollections), request); + } + const emitLegacyOptions = (name: string): void => { + const request = legacyRequests.get(name); + if (request && ctx.generatedNames.has(name) && !ctx.generatedNames.has(request.optionsName)) { + emitRustLegacyOptions(name, request, ctx); + } + }; + // Generate shared definitions (structs & enums) for (const [name, def] of Object.entries(definitions)) { if (typeof def !== "object" || def === null) continue; - const schema = inlineMethodParamSchemas.get(name) ?? (def as JSONSchema7); - - if (schema.enum && Array.isArray(schema.enum)) { + 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 (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[], @@ -1759,11 +2105,27 @@ export function generateApiTypesCode( if (nullableInner && isObjectSchema(nullableInner)) { emitRustStruct(name, nullableInner, ctx, nullableInner.description ?? schema.description); } else { - tryEmitRustUnion(schema, name, "", ctx); + tryEmitRustUnion(schema, name, "", ctx, true, true); } } else { emitRustScalarAlias(name, schema, ctx, schema.description); } + emitLegacyOptions(name); + } + + // A response record must keep every field it deserialises, so Rust cannot freeze it. + // Existing literals stay source-compatible only through `..Default::default()`. + for (const [name, def] of Object.entries(definitions)) { + if (legacyRequests.has(name)) continue; + if (!readLegacyParameters(def, name, { implicit: ["sessionId"] })) continue; + const defaultStruct = new RegExp( + `#\\[derive\\([^)]*\\bDefault\\b[^)]*\\)\\]\\n#\\[serde\\(rename_all = "camelCase"\\)\\]\\n(?:pub|pub\\(crate\\)) struct ${name} \\{`, + ); + if (!ctx.structs.some((block) => defaultStruct.test(block))) { + throw new Error( + `Invalid ${LEGACY_PARAMETERS_KEY} for ${name}: a response record must be a struct that derives Default`, + ); + } } // RPC method name constants @@ -1793,12 +2155,17 @@ export function generateApiTypesCode( const generatedParamsSchema = paramsSchema ?? sessionWireParamsSchema; if (generatedParamsSchema) { const paramsName = rustParamsTypeName(method, ctx); + const legacyRequest = legacyRequests.get(paramsName); + const structSchema = legacyRequest + ? rustLegacyStructSchema(generatedParamsSchema, legacyRequest, paramsName) + : generatedParamsSchema; emitRustStruct( paramsName, - generatedParamsSchema, + structSchema, ctx, - generatedParamsSchema.description, + structSchema.description, ); + emitLegacyOptions(paramsName); } if (method.result && !isVoidSchema(method.result)) { const resultName = rustResultTypeName(method, ctx); @@ -1889,6 +2256,25 @@ export function generateApiTypesCode( out.push(""); } + if (ctx.generatedNames.has("InstallationReview")) { + out.push("/// Compatibility alias for the confirmation review wrapper."); + out.push("pub type InstallationConfirmationRequestReview = InstallationReview;"); + out.push(""); + out.push("/// Resource discriminator for installation confirmation reviews."); + out.push("#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]"); + out.push("pub enum InstallationReviewResource {"); + out.push(" #[serde(rename = \"mcp\")]"); + out.push(" #[default]"); + out.push(" Mcp,"); + out.push(" #[serde(rename = \"skill\")]"); + out.push(" Skill,"); + out.push("}"); + out.push(""); + out.push("/// Compatibility alias for the confirmation review resource discriminator."); + out.push("pub type InstallationConfirmationRequestReviewResource = InstallationReviewResource;"); + out.push(""); + } + return out.join("\n"); } @@ -2062,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, @@ -2188,6 +2591,8 @@ function emitNamespaceMethod( ? "pub(crate)" : "pub"; + const legacyRequest = hasParams ? rustLegacyRequest(method, defCollections, isSession) : undefined; + if (hasParams && paramsInfo.optional) { out.push(...buildDocs(false)); out.push( @@ -2210,9 +2615,20 @@ function emitNamespaceMethod( ); pushNamespaceMethodBody(out, constName, isSession, hasParams, resultIsVoid); out.push(""); + + if (legacyRequest) { + out.push(...buildDocs(true)); + out.push(" ///"); + out.push(` /// Accepts [\`${legacyRequest.optionsName}\`], including inputs added after [\`${paramsTypeName}\`].`); + out.push( + ` ${fnVis} async fn ${fnName}_with_options(&self, params: ${legacyRequest.optionsName}) -> Result<${returnType}, Error> {`, + ); + pushNamespaceMethodBody(out, constName, isSession, true, resultIsVoid); + out.push(""); + } } -function generateRpcCode(apiSchema: ApiSchema): string { +export function generateRpcCode(apiSchema: ApiSchema): string { const defCollections = collectDefinitionCollections( apiSchema as unknown as Record, ); @@ -2473,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 a4680803e0..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, @@ -51,6 +50,7 @@ import { type DefinitionCollections, type RpcMethod, } from "./utils.js"; +import { validateLegacyDefinitions, validateLegacyRequests } from "./legacy-parameters.js"; const TS_EXPERIMENTAL_JSDOC = "/** @experimental */"; const EXTERNAL_SCHEMA_TS_IMPORT: Record = { @@ -385,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)) @@ -432,15 +435,26 @@ export function normalizeSchemaForTypeScript( delete rewritten["x-opaque-json"]; delete rewritten["x-opaque-in-process"]; + // A title names an enum that other generators infer from a discriminator; a + // TypeScript literal needs no alias, so keep it inline. + if (typeof rewritten.title === "string" && "const" in rewritten && typeof rewritten.const === "string") { + delete rewritten.title; + } + 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"]; @@ -664,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`; } @@ -742,6 +765,15 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; // Build a single combined schema with shared definitions and all method types. // This ensures $ref-referenced types are generated exactly once. rpcDefinitions = collectDefinitionCollections(schema as Record); + // Added inputs are optional properties of the same request interface. + validateLegacyRequests( + schema, + (node) => collectRpcMethods(node), + getMethodParamsSchema, + (method) => !!(method.params && getNullableInner(method.params)) + ); + // Response records gain optional properties of the same interface. + validateLegacyDefinitions(rpcDefinitions); const combinedSchema = withSharedDefinitions( { $schema: "http://json-schema.org/draft-07/schema#", @@ -996,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 }"; } @@ -1178,7 +1210,7 @@ export function emitClientSessionApiRegistration(clientSchema: Record): string[] { +export function emitClientGlobalApiRegistration(clientSchema: Record): string[] { const lines: string[] = []; const groups = collectClientGroups(clientSchema); @@ -1311,29 +1343,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro const __filename = fileURLToPath(import.meta.url); -export function isTypeScriptCodegenEntrypoint( - entryPath: string | undefined, - modulePath = __filename, - platform = process.platform, -): boolean { - if (!entryPath) { - return false; - } - const canonicalize = (filePath: string) => { - try { - return realpathSync.native(filePath); - } catch { - return path.resolve(filePath); - } - }; - const canonicalEntryPath = canonicalize(entryPath); - const canonicalModulePath = canonicalize(modulePath); - return platform === "win32" - ? canonicalEntryPath.toLowerCase() === canonicalModulePath.toLowerCase() - : canonicalEntryPath === canonicalModulePath; -} - -if (isTypeScriptCodegenEntrypoint(process.argv[1])) { +if (isCodegenEntrypoint(process.argv[1], __filename)) { const sessionArg = process.argv[2] || undefined; const apiArg = process.argv[3] || undefined; generate(sessionArg, apiArg).catch((err) => { diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 078e45c4b9..5bb3c9f995 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -8,6 +8,7 @@ import { execFile } from "child_process"; import fs from "fs/promises"; +import { realpathSync } from "fs"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; @@ -24,6 +25,32 @@ const __dirname = path.dirname(__filename); /** Root of the copilot-sdk repo */ export const REPO_ROOT = path.resolve(__dirname, "../.."); +/** Recognizes entrypoints reached through Bazel links or Windows path casing. */ +export function isCodegenEntrypoint( + entryPath: string | undefined, + modulePath: string, + platform = process.platform, +): boolean { + if (!entryPath) { + return false; + } + const canonicalize = (filePath: string) => { + try { + return realpathSync.native(filePath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + return path.resolve(filePath); + } + }; + const canonicalEntryPath = canonicalize(entryPath); + const canonicalModulePath = canonicalize(modulePath); + return platform === "win32" + ? canonicalEntryPath.toLowerCase() === canonicalModulePath.toLowerCase() + : canonicalEntryPath === canonicalModulePath; +} + /** Event types to exclude from generation (internal/legacy types) */ export const EXCLUDED_EVENT_TYPES = new Set(["session.import_legacy"]); diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index 7ddd2c136c..d869196ba7 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -457,6 +457,7 @@ async function main() { cwd: DOCS_DIR, ignore: [".validation/**", "node_modules/**", "IMPROVEMENT_PLAN.md"], }); + mdFiles.push("../java/README.md"); console.log(`Found ${mdFiles.length} markdown files\n`); @@ -472,7 +473,15 @@ async function main() { for (const mdFile of mdFiles) { const fullPath = path.join(DOCS_DIR, mdFile); - const content = fs.readFileSync(fullPath, "utf-8"); + let content = fs.readFileSync(fullPath, "utf-8"); + if (mdFile === "../java/README.md") { + // Replace the legacy README smoke with validation of its exact Quick Start. + const quickStart = /^## Quick Start\r?\n[\s\S]*?^```java\r?\n[\s\S]*?^```/m.exec(content); + if (!quickStart) { + throw new Error(`Could not find the Java Quick Start in ${mdFile}`); + } + content = "\n".repeat(content.slice(0, quickStart.index).split("\n").length - 1) + quickStart[0]; + } const blocks = parseMarkdownCodeBlocks(content, mdFile); for (const block of blocks) { diff --git a/scripts/docs-validation/validate.ts b/scripts/docs-validation/validate.ts index cb7b2ae2f6..cfa14b9251 100644 --- a/scripts/docs-validation/validate.ts +++ b/scripts/docs-validation/validate.ts @@ -385,12 +385,10 @@ async function validateJava(): Promise { fs.copyFileSync(path.join(javaDir, file), path.join(srcDir, file)); } - // Read the inherited SDK version from java/sdk/pom.xml - const sdkPomPath = path.join(ROOT_DIR, "java", "sdk", "pom.xml"); + // The required SDK install writes a flattened POM with ${revision} resolved. + const sdkPomPath = path.join(ROOT_DIR, "java", "sdk", ".flattened-pom.xml"); const sdkPomContent = fs.readFileSync(sdkPomPath, "utf-8"); - const versionMatch = sdkPomContent.match( - /[\s\S]*?([^<]+)<\/version>[\s\S]*?<\/parent>/, - ); + const versionMatch = sdkPomContent.match(/([^<]+)<\/version>/); if (!versionMatch) { throw new Error(`Could not read the Java SDK version from ${sdkPomPath}`); } @@ -421,37 +419,13 @@ async function validateJava(): Promise { fs.writeFileSync(path.join(javaDir, "pom.xml"), pomXml); - // First, install the local SDK into the local Maven repo - const pomPath = path.join(ROOT_DIR, "java", "pom.xml"); - try { - execSync(`mvn install -f "${pomPath}" -Dmaven.test.skip=true -q`, { - encoding: "utf-8", - cwd: path.join(ROOT_DIR, "java"), - }); - } catch (err: any) { - // If SDK install fails, all Java snippets fail - const errorMsg = `SDK install failed: ${(err.stderr || err.message || "").slice(0, 200)}`; - for (const file of files) { - const block = manifest.blocks.find( - (b) => b.outputFile === `java/${file}`, - ); - results.push({ - file: `java/${file}`, - sourceFile: block?.sourceFile || "unknown", - sourceLine: block?.sourceLine || 0, - success: false, - errors: [errorMsg], - }); - } - return results; - } - - // Compile the validation project + // The docs task or CI installs the SDK before validation. try { const validationPom = path.join(javaDir, "pom.xml"); - execSync(`mvn compile -f "${validationPom}" -q`, { + const maven = process.platform === "win32" ? "mvnw.cmd" : "./mvnw"; + execSync(`${maven} compile -f "${validationPom}" -q`, { encoding: "utf-8", - cwd: javaDir, + cwd: path.join(ROOT_DIR, "java"), }); // All files passed @@ -487,6 +461,10 @@ async function validateJava(): Promise { } } + if (fileErrors.size === 0) { + throw new Error(`Java documentation compilation failed:\n${output}`); + } + for (const file of files) { const block = manifest.blocks.find( (b) => b.outputFile === `java/${file}`, diff --git a/scripts/install-dependencies.mjs b/scripts/install-dependencies.mjs new file mode 100644 index 0000000000..123e822c08 --- /dev/null +++ b/scripts/install-dependencies.mjs @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +export function installNpmDependencies(directory, runNpm) { + const nodeModules = join(directory, "node_modules"); + const stampFile = join(nodeModules, ".copilot-sdk-install-stamp"); + const args = ["ci", "--ignore-scripts", "--include=dev"]; + const hash = createHash("sha256").update(JSON.stringify(args)); + for (const file of ["package-lock.json", "package.json"]) { + hash.update(readFileSync(join(directory, file))); + } + const fingerprint = hash.digest("hex"); + let stamp; + try { + stamp = readFileSync(stampFile, "utf8").trim(); + } catch (error) { + if (error.code !== "ENOENT") { + throw error; + } + } + if (existsSync(nodeModules) && stamp === fingerprint) { + return false; + } + + // Failed installs must not leave a stamp claiming the dependency tree is current. + rmSync(stampFile, { force: true }); + runNpm(args, directory); + mkdirSync(nodeModules, { recursive: true }); + writeFileSync(stampFile, `${fingerprint}\n`); + return true; +} diff --git a/scripts/run-tasks.mjs b/scripts/run-tasks.mjs index a74fafbf14..8b0e4592b7 100644 --- a/scripts/run-tasks.mjs +++ b/scripts/run-tasks.mjs @@ -10,6 +10,7 @@ import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { bazelActionEnvironmentArgument, prepareSdkSources } from "./build-prerequisites.mjs"; import { findRuntimeRoot, getRuntimeCliPaths } from "./runtime-layout.mjs"; +import { installNpmDependencies } from "./install-dependencies.mjs"; const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const languages = ["nodejs", "python", "go", "dotnet", "java", "rust"]; @@ -35,7 +36,7 @@ const tasks = { "generate:schemas": {}, build: { nodejs: [ - command("nodejs", "npm", ["ci", "--ignore-scripts"]), + command("nodejs", "npm", ["ci", "--ignore-scripts", "--include=dev"]), command("nodejs", "npm", ["run", "build"]), ], python: [ @@ -142,7 +143,15 @@ const tasks = { python: [command("scripts/docs-validation", "npm", ["run", "validate:py"])], go: [command("scripts/docs-validation", "npm", ["run", "validate:go"])], dotnet: [command("scripts/docs-validation", "npm", ["run", "validate:cs"])], - java: [command("scripts/docs-validation", "npm", ["run", "validate:java"])], + java: [ + command("java", maven, [ + "install", + "-Dmaven.test.skip=true", + "-Dskip.test.harness=true", + "-Dcopilot.native.skip.download=true", + ]), + command("scripts/docs-validation", "npm", ["run", "validate:java"]), + ], }, }; @@ -151,6 +160,11 @@ tasks["test:default"] = { rust: tasks.test.rust, }; +tasks["build:default"] = { + nodejs: tasks.build.nodejs, + rust: tasks.build.rust, +}; + tasks.check.rust = [...tasks["format:check"].rust, ...tasks.lint.rust]; function collectGoFiles(directory) { @@ -304,7 +318,7 @@ export function runTasks(verb, language, options = {}) { if ( runtimeSource === "checkout" && runtimeRoot && - ["build", "generate", "generate:schemas", "test", "test:default"].includes(verb) + ["build", "build:default", "generate", "generate:schemas", "test", "test:default"].includes(verb) ) { const selectedLanguages = language ? [language] : Object.keys(tasks[verb]); prepareSdkSources({ languages: selectedLanguages, runtimeRoot, sdkRoot }); @@ -341,13 +355,21 @@ export function runTasks(verb, language, options = {}) { if (step.kind === "gofmt") { runGoFormat(step.write); } else { - if (resolvedRuntimeSource === "checkout" && verb === "build" && step.language === "rust") { + if ( + resolvedRuntimeSource === "checkout" && + (verb === "build" || verb === "build:default") && + step.language === "rust" + ) { runRootCommand( runtimeRoot, "pnpm", ["bazel", "build", bazelActionEnvironmentArgument(environment), "//src/sdk/rust:github-copilot-sdk"], environment, ); + } else if (step.executable === "npm" && step.args[0] === "ci") { + installNpmDependencies(resolve(sdkRoot, step.cwd), (args) => + runCommand({ ...step, args }, environment), + ); } else { runCommand(step, environment); } diff --git a/scripts/runtime-release.mjs b/scripts/runtime-release.mjs new file mode 100644 index 0000000000..fcd3585ca3 --- /dev/null +++ b/scripts/runtime-release.mjs @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Resolve a pinned runtime's public release, preserving the caller's mirror override. + * @param {string} version + * @param {string | null | undefined} [baseUrl] Pass null to ignore ambient mirror settings. + */ +export function runtimeReleaseUrl(version, baseUrl = process.env.COPILOT_CLI_DOWNLOAD_BASE_URL) { + const unstable = + /^(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*)){2}(?:-unstable|-(?:0|[1-9][0-9]*)\.unstable)\.r[1-9][0-9]*\.g[0-9a-f]{7}$/.test( + version, + ); + const repository = unstable ? "copilot-sdk" : "copilot-cli"; + const tag = unstable ? `runtime-${version}` : `v${version}`; + const base = baseUrl ?? `https://github.com/github/${repository}/releases/download`; + return `${base.replace(/\/+$/, "")}/${tag}`; +} diff --git a/test/harness/capturingHttpProxy.ts b/test/harness/capturingHttpProxy.ts index fdc1fc46c1..91fe1d061a 100644 --- a/test/harness/capturingHttpProxy.ts +++ b/test/harness/capturingHttpProxy.ts @@ -119,6 +119,9 @@ export class CapturingHttpProxy { }); }); }); + // Tests pause between control requests; idle expiry can race a pooled POST. + // Fixture teardown already closes every connection explicitly. + this.server.keepAliveTimeout = 0; return new Promise((resolve, reject) => { this.server!.on("error", (err) => { diff --git a/test/harness/certUtils.ts b/test/harness/certUtils.ts index ed1754547a..56be7d41b0 100644 --- a/test/harness/certUtils.ts +++ b/test/harness/certUtils.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +import net from "net"; import tls from "tls"; import forge from "node-forge"; @@ -35,6 +36,7 @@ export function generateCA(): CaData { cert.setExtensions([ { name: "basicConstraints", cA: true, critical: true }, { name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true }, + { name: "subjectKeyIdentifier" }, ]); cert.sign(keys.privateKey, forge.md.sha256.create()); @@ -47,10 +49,34 @@ export function generateCA(): CaData { }; } +export interface IdentityData { + certPem: string; + keyPem: string; +} + export function createSecureContextForHost( hostname: string, ca: CaData, ): tls.SecureContext { + const identity = createIdentityForHost(hostname, ca); + return tls.createSecureContext({ + key: identity.keyPem, + cert: identity.certPem, + ca: ca.certPem, + }); +} + +/** + * Issues a CA-signed server identity for `hostname`. IP literals get an IP + * subjectAltName rather than a DNS one, because verifiers reject a DNS name + * when the client connected to an address; `serverAuth` and modern key-usage + * extensions keep strict platform verifiers (macOS SecTrust) from rejecting + * the chain outright. + */ +export function createIdentityForHost( + hostname: string, + ca: CaData, +): IdentityData { const keys = forge.pki.rsa.generateKeyPair(2048); const cert = forge.pki.createCertificate(); cert.publicKey = keys.publicKey; @@ -65,17 +91,31 @@ export function createSecureContextForHost( cert.setSubject([{ name: "commonName", value: hostname }]); cert.setIssuer(ca.caCert.subject.attributes); cert.setExtensions([ + { name: "basicConstraints", cA: false, critical: true }, + { + name: "keyUsage", + digitalSignature: true, + keyEncipherment: true, + critical: true, + }, + { name: "extKeyUsage", serverAuth: true }, { name: "subjectAltName", - altNames: [{ type: 2, value: hostname }], + altNames: net.isIP(hostname) + ? [{ type: 7, ip: hostname }] + : [{ type: 2, value: hostname }], + }, + { + name: "authorityKeyIdentifier", + keyIdentifier: ca.caCert.generateSubjectKeyIdentifier().getBytes(), }, + { name: "subjectKeyIdentifier" }, ]); cert.sign(ca.caKey, forge.md.sha256.create()); - return tls.createSecureContext({ - key: forge.pki.privateKeyToPem(keys.privateKey), - cert: forge.pki.certificateToPem(cert), - ca: ca.certPem, - }); + return { + keyPem: forge.pki.privateKeyToPem(keys.privateKey), + certPem: forge.pki.certificateToPem(cert), + }; } diff --git a/test/harness/mcp-prompt-fixtures.json b/test/harness/mcp-prompt-fixtures.json new file mode 100644 index 0000000000..41fe307adc --- /dev/null +++ b/test/harness/mcp-prompt-fixtures.json @@ -0,0 +1,129 @@ +{ + "$comment": "Copyright (c) Microsoft Corporation. All rights reserved.", + "firstPage": { + "prompts": [ + { + "name": "rich", + "title": "Rich prompt", + "description": "Ordered opaque content", + "icons": [ + { + "src": "data:image/png;base64,aW1hZ2U=", + "mimeType": "image/png", + "sizes": ["16x16", "32x32"], + "theme": "dark", + "additionalProperties": { + "x-icon": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + } + ], + "_meta": { "descriptor": { "version": 1 } }, + "additionalProperties": { + "x-prompt": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + }, + "arguments": [ + { + "name": "topic", + "description": "Required topic", + "required": true, + "_meta": { "argument": { "label": "Topic" } }, + "additionalProperties": { + "x-argument": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + }, + { "name": "style", "description": "Optional style", "required": false }, + { "name": "extra" } + ] + } + ], + "nextCursor": "page:2/opaque+cursor=", + "_meta": { "page": { "number": 1 } }, + "additionalProperties": { + "x-list": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + }, + "secondPage": { + "prompts": [ + { "name": "echo", "arguments": [] }, + { "name": "refresh", "description": "Publish a list change" } + ], + "_meta": { "page": { "number": 2 } } + }, + "richPrompt": { + "description": "A prompt is data, not a model invocation", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Explain opaque content", + "annotations": { "audience": ["user"], "priority": 0.75 }, + "_meta": { "nested": { "values": [true, null, 3] } }, + "futureField": { "preserve": false } + }, + "_meta": { "message": { "index": 0 } }, + "additionalProperties": { + "x-message": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + }, + { + "role": "assistant", + "content": { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/png" } + }, + { + "role": "user", + "content": { "type": "audio", "data": "YXVkaW8=", "mimeType": "audio/wav" } + }, + { + "role": "assistant", + "content": { + "type": "resource", + "resource": { + "uri": "test://prompt/text", + "mimeType": "text/plain", + "text": "Embedded text", + "_meta": { "origin": { "embedded": true } } + } + } + }, + { + "role": "user", + "content": { + "type": "resource", + "resource": { "uri": "test://prompt/blob", "mimeType": "application/octet-stream", "blob": "AAE=" } + } + }, + { + "role": "assistant", + "content": { + "type": "resource_link", + "uri": "test://prompt/not-fetched", + "name": "Reference", + "title": "Resource link", + "description": "Must not be fetched", + "mimeType": "text/plain", + "size": 42, + "annotations": { "audience": ["assistant"] } + } + }, + { + "role": "user", + "content": { + "type": "future-content", + "payload": { "items": [1, false, null, { "key": "value" }] }, + "_meta": { "extension": ["kept"] } + } + } + ], + "_meta": { "result": { "source": "fixture", "flags": [false, true] } }, + "additionalProperties": { + "x-get": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + } +} diff --git a/test/harness/mockHandlers.ts b/test/harness/mockHandlers.ts index 9f75d6819e..8c520a6ff2 100644 --- a/test/harness/mockHandlers.ts +++ b/test/harness/mockHandlers.ts @@ -19,6 +19,9 @@ export function createE2eRequestHandler(options: { } if (targetHost === "github.com") { + if (req.method === "POST" && req.url === "/login/oauth/access_token") { + return forwardToCapiProxy(req, res, options.capiProxyUrl); + } respondJson(res, 404, { message: "Not Found (e2e mock)" }); return true; } diff --git a/test/harness/modelProtocolAdapters.test.ts b/test/harness/modelProtocolAdapters.test.ts index 6773edca80..a7b6ab4ce2 100644 --- a/test/harness/modelProtocolAdapters.test.ts +++ b/test/harness/modelProtocolAdapters.test.ts @@ -303,6 +303,40 @@ describe("Anthropic Messages adapter", () => { }); describe("OpenAI Responses adapter", () => { + test("normalizes image-bearing function output without changing ordinary JSON output", () => { + const imageOutput = JSON.stringify([ + { type: "input_text", text: "Viewed image file successfully." }, + { + type: "input_image", + detail: "auto", + image_url: "data:image/png;base64,AQID", + }, + ]); + const result = JSON.parse( + responsesApiRequestToChatCompletion( + JSON.stringify({ + model: "test-model", + input: [ + { + type: "function_call_output", + call_id: "view-1", + output: imageOutput, + }, + { + type: "function_call_output", + call_id: "other-1", + output: '[{"ok":true}]', + }, + ], + }), + ), + ) as { messages: Array<{ content: string }> }; + expect(result.messages.map((message) => message.content)).toEqual([ + "Viewed image file successfully.", + '[{"ok":true}]', + ]); + }); + test("normalizes messages, binary content, and tools", () => { const result = JSON.parse( responsesApiRequestToChatCompletion( diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 833a941119..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,71 @@ 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 }); + proxy.setCopilotUserByToken("other-token", { login: "other", id: 99 }); + proxy.setCopilotUserByToken("unresolved-token", { login: "unresolved" }); + const address = await proxy.start(); + try { + for (const [token, id, login] of [ + ["owner-token", 42, "owner"], + ["other-token", 99, "other"], + ] as const) { + const response = await fetch(`${address}/user`, { + headers: { authorization: `Bearer ${token}` }, + }); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ id, login, type: "User" }); + } + for (const token of ["unknown-token", "unresolved-token"]) { + const response = await fetch(`${address}/user`, { + headers: { authorization: `Bearer ${token}` }, + }); + expect(response.status).toBe(401); + } + } finally { + await proxy.stop(); + } + }); + test("does not write file when no chat completion exchanges", async () => { const outputPath = path.join(tempDir, "output.yaml"); const proxy = new ReplayingCapiProxy( @@ -93,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: [ @@ -117,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" }], @@ -332,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: [ @@ -664,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: [ @@ -676,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, + }), }, }, ], @@ -684,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), }, ], }); @@ -698,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 () => { @@ -804,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) => { @@ -855,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"], @@ -1052,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" @@ -1632,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({ @@ -1734,6 +2886,55 @@ Always include PINEAPPLE_COCONUT_42. } }); + test("streaming preserves tool choices from recorded multi-client traffic", async () => { + const proxy = new ReplayingCapiProxy("http://localhost:9999"); + await proxy.updateConfig({ + filePath: path.resolve( + __dirname, + "../snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml", + ), + workDir, + backend: "capi", + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "claude-sonnet-5", + messages: [ + { role: "system", content: "Application prompt" }, + { + role: "user", + content: + "Use the magic_number tool with seed 'hello' and tell me the result", + }, + ], + stream: true, + }, + }); + expect(response.status).toBe(200); + const chunks = response.body + .split("\n") + .filter((line) => line.startsWith("data: {")) + .map((line) => JSON.parse(line.slice(6)) as ChatCompletionChunk); + const calls = chunks.flatMap( + (chunk) => chunk.choices[0].delta.tool_calls ?? [], + ); + expect(calls.map((call) => call.function?.name)).toEqual([ + "report_intent", + "magic_number", + ]); + expect(calls.map((call) => call.index)).toEqual([0, 1]); + 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(); + } + }); + test("returns streaming response when stream: true", async () => { const cachePath = path.join(tempDir, "cache.yaml"); const cacheContent = yaml.stringify({ diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 2511ca9255..669cd65a6e 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -123,6 +123,8 @@ const defaultModel = "claude-sonnet-5"; */ export class ReplayingCapiProxy extends CapturingHttpProxy { private state: ReplayingCapiProxyState | null = null; + private memoryApiStub: MemoryApiStub | undefined; + private entraLogin: { subjectToken: string; githubToken: string } | undefined; private startPromise: Promise | null = null; private defaultToolResultNormalizers: ToolResultNormalizer[] = [ { toolName: "*", normalizer: normalizeLargeOutputFilepaths }, @@ -201,6 +203,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { filePath: config.filePath, workDir: config.workDir, testInfo: config.testInfo, + modelNames: config.modelNames, backend: parseReplayBackend(config.backend), replayOnly: config.replayOnly === true, autoResponseIndex: 0, @@ -292,6 +295,75 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { return; } + if ( + options.requestOptions.path === "/memory-api-config" && + options.requestOptions.method === "POST" + ) { + this.memoryApiStub = JSON.parse(options.body!) as MemoryApiStub; + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + + if ( + options.requestOptions.path === "/entra-login-config" && + options.requestOptions.method === "POST" + ) { + this.entraLogin = JSON.parse(options.body!) as { + subjectToken: string; + githubToken: string; + }; + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + if ( + this.entraLogin && + options.requestOptions.path === "/entra-broker" + ) { + options.onResponseStart(200, { "content-type": "application/json" }); + options.onData( + Buffer.from( + JSON.stringify({ + accessToken: this.entraLogin.subjectToken, + expiresOnTimestamp: Date.now() + 3600000, + username: "entra-sdk@example.test", + accountId: "entra-sdk-account", + }), + ), + ); + options.onResponseEnd(); + return; + } + if ( + this.entraLogin && + options.requestOptions.path === "/login/oauth/access_token" + ) { + const form = new URLSearchParams(options.body ?? ""); + const valid = + form.get("grant_type") === + "urn:ietf:params:oauth:grant-type:token-exchange" && + form.get("subject_token") === this.entraLogin.subjectToken; + options.onResponseStart(valid ? 200 : 400, { + "content-type": "application/json", + }); + options.onData( + Buffer.from( + JSON.stringify( + valid + ? { + access_token: this.entraLogin.githubToken, + expires_in: 3600, + token_type: "bearer", + } + : { error: "invalid_request" }, + ), + ), + ); + options.onResponseEnd(); + return; + } + // Handle /config endpoint for updating proxy configuration if ( options.requestOptions.path === "/config" && @@ -324,20 +396,22 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { options.requestOptions.method === "GET" ) { const protocol = replayProtocols[this.state?.backend ?? "capi"]; + const modelExchanges = this.exchanges.filter( + (exchange) => exchange.request.url === protocol.endpoint, + ); const parsedExchanges = await Promise.all( - this.exchanges - .filter((exchange) => exchange.request.url === protocol.endpoint) - .map((exchange) => - parseHttpExchange( - protocol.normalizeRequest?.(exchange.request.body) ?? - exchange.request.body, - protocol.canonicalResponse - ? exchange.response?.body - : undefined, - exchange.request.headers, - ), + modelExchanges.map((exchange) => + parseHttpExchange( + protocol.normalizeRequest?.(exchange.request.body) ?? + exchange.request.body, + protocol.canonicalResponse + ? exchange.response?.body + : undefined, + exchange.request.headers, ), + ), ); + addCompactionProviderUsage(parsedExchanges, modelExchanges); options.onResponseStart(200, {}); options.onData(Buffer.from(JSON.stringify(parsedExchanges))); options.onResponseEnd(); @@ -358,13 +432,17 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { return; } - // Handle /copilot_internal/user endpoint for per-session auth. + // Handle Copilot auth and GitHub identity validation from the same + // registered credentials. // This must run before the state guard below: the CLI authenticates and // calls /copilot_internal/user at startup, which can race ahead of the // per-test POST /config (e.g. the Go harness spawns the CLI before the // first ConfigureForTest). The response only depends on the token map, // which is populated independently of `state`. - if (options.requestOptions.path === "/copilot_internal/user") { + if ( + options.requestOptions.path === "/copilot_internal/user" || + options.requestOptions.path === "/user" + ) { const headers = options.requestOptions.headers; const headerMap = headers as | Record @@ -384,9 +462,17 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { // The CLI gates third-party MCP servers behind the copilot user's // `is_mcp_enabled` flag (a null/missing value disables them). Default // it to true so e2e MCP servers are enabled unless a test opts out. - const userResponse = registered - ? ({ is_mcp_enabled: true, ...registered } as CopilotUserResponse) - : undefined; + const userResponse = + options.requestOptions.path === "/user" + ? registered?.id !== undefined + ? { id: registered.id, login: registered.login, type: "User" } + : undefined + : registered + ? ({ + is_mcp_enabled: true, + ...registered, + } as CopilotUserResponse) + : undefined; if (userResponse) { const headers = { "content-type": "application/json", @@ -420,7 +506,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { state.storedData?.models && state.storedData.models.length > 0 ? state.storedData.models : [defaultModel]; - const modelsResponse = createGetModelsResponse(models); + const modelsResponse = createGetModelsResponse(models, state.modelNames); const body = JSON.stringify(modelsResponse); const headers = { "content-type": "application/json", @@ -504,8 +590,14 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { // Matches: /agents/*/memory/*/enabled, /agents/*/memory/*/recent, etc. if (options.requestOptions.path?.match(/\/agents\/.*\/memory\//)) { let body: string; + let statusCode = 200; if (options.requestOptions.path.includes("/enabled")) { - body = JSON.stringify({ enabled: false }); + body = JSON.stringify(this.memoryApiStub?.enabled ?? { enabled: false }); + } else if (options.requestOptions.path.includes("/memories")) { + statusCode = this.memoryApiStub?.memoriesStatusCode ?? 200; + body = JSON.stringify( + statusCode === 404 ? {} : (this.memoryApiStub?.memories ?? {}), + ); } else if (options.requestOptions.path.includes("/recent")) { body = JSON.stringify({ memories: [] }); } else { @@ -515,7 +607,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { "content-type": "application/json", ...commonResponseHeaders, }; - options.onResponseStart(200, headers); + options.onResponseStart(statusCode, headers); options.onData(Buffer.from(body)); options.onResponseEnd(); return; @@ -1029,6 +1121,14 @@ function coalesceAdjacentUserMessages(requestBody: string): string { const messages: NonNullable = []; for (const message of request.messages) { + if ( + message.role === "user" && + typeof message.content === "string" && + containsModeChangedNotice(message.content) && + normalizeUserMessage(message.content) === "" + ) { + continue; + } const previous = messages.at(-1); if ( previous?.role === "user" && @@ -1060,6 +1160,70 @@ function openAIErrorBody(code: string | undefined, message: string): unknown { return { error: { message, type, code: type } }; } +function rewriteToolArgumentPaths( + toolName: string, + argumentsJson: string, + rewritePath: (value: string) => string, + rewriteTextWorkDir: (value: string) => string, +): string { + const rewritePatchPaths = (patch: string) => + patch.replace( + /^(\*\*\* (?:Add File|Update File|Delete File|Move to):[ \t]*)([^\r\n]*)/gm, + (_match, header: string, filename: string) => + header + rewritePath(filename), + ); + if ( + toolName === "apply_patch" && + argumentsJson.trimStart().startsWith("*** Begin Patch") + ) { + return rewritePatchPaths(argumentsJson); + } + + let argumentsValue: unknown; + try { + argumentsValue = JSON.parse(argumentsJson, (key, value: unknown) => { + const isPath = /^(?:path|paths|file_path|filePath|cwd|directory)$/.test( + key, + ); + if (typeof value === "string") { + if (toolName === "apply_patch" && (key === "" || key === "input")) { + return rewritePatchPaths(value); + } + if (isPath) { + return rewritePath(value); + } + if (toolName === "${shell}" && key === "command") { + return rewriteTextWorkDir(value); + } + } else if (isPath && Array.isArray(value)) { + return value.map((item: unknown) => + typeof item === "string" ? rewritePath(item) : item, + ); + } + return value; + }); + } catch (error) { + if (error instanceof SyntaxError) { + return argumentsJson; + } + throw error; + } + if ( + toolName === "task" && + typeof argumentsValue === "object" && + argumentsValue !== null && + "prompt" in argumentsValue && + typeof argumentsValue.prompt === "string" + ) { + argumentsValue.prompt = rewriteTextWorkDir(argumentsValue.prompt); + } + const rewritten = JSON.stringify(argumentsValue); + if (rewritten === undefined) { + throw new Error("Unable to encode rewritten tool arguments"); + } + return rewritten; +} + function normalizeFilenames( conversations: NormalizedConversation[], workDir: string, @@ -1083,6 +1247,10 @@ function normalizeFilenames( /(? path.replace(/\\/g, "/"); + const normalizePath = (value: string) => + value.replace(workDirPattern, workDirReplacer).replace(/\\/g, "/"); + const normalizeTextWorkDir = (value: string) => + value.replace(workDirPattern, workDirReplacer); for (const conv of conversations) { for (const msg of conv.messages) { @@ -1092,13 +1260,11 @@ function normalizeFilenames( } for (const tc of msg.tool_calls ?? []) { if (tc.function?.arguments) { - tc.function.arguments = tc.function.arguments.replace( - workDirPattern, - workDirReplacer, - ); - tc.function.arguments = tc.function.arguments.replace( - windowsFnPattern, - windowsFnReplacer, + tc.function.arguments = rewriteToolArgumentPaths( + tc.function.name, + tc.function.arguments, + normalizePath, + normalizeTextWorkDir, ); } } @@ -1123,6 +1289,7 @@ function normalizeToolCalls( const precedingMessages: NormalizedMessage[] = []; let counter = 0; let unnamedBackgroundAgentCounter = 0; + let unnamedAgentCounter = 0; for (const msg of conv.messages) { for (const tc of msg.tool_calls ?? []) { // Normalize ID in tool calls @@ -1151,6 +1318,16 @@ function normalizeToolCalls( tc.function?.name === "read_agent" || tc.function?.name === "write_agent" ) { + for (const runtimeId of getUnnamedRuntimeAgentIds( + tc.function.name, + tc.function.arguments, + (id) => backgroundAgentNamesByRuntimeId.has(id), + )) { + backgroundAgentNamesByRuntimeId.set( + runtimeId, + unnamedAgentName(unnamedAgentCounter++), + ); + } tc.function.arguments = normalizeBackgroundAgentArguments( tc.function.arguments, backgroundAgentNamesByRuntimeId, @@ -1291,6 +1468,77 @@ async function parseHttpExchange( return { request, response, requestHeaders }; } +function addCompactionProviderUsage( + exchanges: ParsedHttpExchange[], + capturedExchanges: readonly CapturedExchange[], +): void { + const chains = new Map< + string, + { + usage: CompactionProviderUsage; + previous: ParsedHttpExchange; + } + >(); + for (const [index, exchange] of exchanges.entries()) { + const status = capturedExchanges[index].response?.statusCode; + const headers = exchange.requestHeaders; + if ( + status === undefined || + status < 200 || + status >= 300 || + headers?.["x-interaction-type"] !== "conversation-compaction" || + !exchange.response?.choices + ) { + continue; + } + const interactionId = headers["x-interaction-id"]; + if (typeof interactionId !== "string" || !interactionId) { + throw new Error( + "Compaction provider response has no interaction identity", + ); + } + const lastMessage = exchange.request.messages.at(-1); + const lastContent = + typeof lastMessage?.content === "string" + ? lastMessage.content + : lastMessage?.role === "user" && Array.isArray(lastMessage.content) + ? lastMessage.content + .map((part) => (part.type === "text" ? part.text : "")) + .join("") + : ""; + // The runtime appends this nudge for both length and reasoning-only continuations. + const continuation = + lastMessage?.role === "user" && + lastContent.endsWith("Please continue from where you left off."); + let chain = continuation ? chains.get(interactionId) : undefined; + if (continuation && !chain) { + throw new Error( + "Compaction continuation has no captured preceding provider response", + ); + } + if (!chain) { + chain = { + usage: { interactionId, summary: "", responseCount: 0, inputTokens: 0 }, + previous: exchange, + }; + chains.set(interactionId, chain); + } else { + delete chain.previous.compactionUsage; + } + const inputTokens = exchange.response.usage?.prompt_tokens; + chain.usage.inputTokens = + typeof inputTokens === "number" && chain.usage.inputTokens !== undefined + ? chain.usage.inputTokens + inputTokens + : undefined; + chain.usage.summary += exchange.response.choices + .map((choice) => choice.message.content ?? "") + .join(""); + chain.usage.responseCount++; + exchange.compactionUsage = { ...chain.usage }; + chain.previous = exchange; + } +} + // Converts a single HTTP exchange (request + response) into a normalized conversation async function transformHttpExchange( requestBody: string, @@ -1300,7 +1548,9 @@ async function transformHttpExchange( requestBody, responseBody, ); - const messages = request.messages.map(transformOpenAIRequestMessage); + const messages = request.messages + .map(transformOpenAIRequestMessage) + .filter((message): message is NormalizedMessage => message !== undefined); if (response?.choices?.length) { messages.push(...transformOpenAIResponseChoice(response.choices)); @@ -1313,29 +1563,41 @@ async function transformHttpExchange( // We use this to look up whether we already have a cached response for it function transformOpenAIRequestMessage( m: ChatCompletionMessageParam, -): NormalizedMessage { +): NormalizedMessage | undefined { let content: string | undefined; + let strippedModeNoticeOnly = false; if (m.role === "system") { // System message changes too often to include in snapshots - just store placeholder content = "${system}"; } else if (m.role === "user" && typeof m.content === "string") { content = normalizeUserMessage(m.content); + strippedModeNoticeOnly = + content === "" && containsModeChangedNotice(m.content); } else if (m.role === "user" && Array.isArray(m.content)) { // Multimodal user messages have array content with text and image_url parts. // Extract and normalize text parts; represent image_url parts as a stable marker. const parts: string[] = []; + let sawModeNotice = false; + let sawVisibleContent = false; for (const part of m.content) { if ( typeof part === "object" && part.type === "text" && typeof part.text === "string" ) { - parts.push(normalizeUserMessage(part.text)); + sawModeNotice ||= containsModeChangedNotice(part.text); + const normalized = normalizeUserMessage(part.text); + if (normalized) { + parts.push(normalized); + sawVisibleContent = true; + } } else if (typeof part === "object" && part.type === "image_url") { parts.push("[image]"); + sawVisibleContent = true; } } content = parts.join("\n") || undefined; + strippedModeNoticeOnly = sawModeNotice && !sawVisibleContent; } else if (m.role === "tool" && typeof m.content === "string") { // If it's a JSON tool call result, normalize the whitespace and property ordering. // For successful tool results wrapped in {resultType, textResultForLlm}, unwrap to @@ -1362,6 +1624,9 @@ function transformOpenAIRequestMessage( content = m.content; } + if (strippedModeNoticeOnly) { + return undefined; + } const msg: NormalizedMessage = { role: m.role }; if ("tool_call_id" in m && m.tool_call_id) { msg.tool_call_id = m.tool_call_id; @@ -1374,7 +1639,7 @@ function transformOpenAIRequestMessage( } function normalizeUserMessage(content: string): string { - return normalizeSkillContextFrontmatter(content) + return stripModeChangedNotice(normalizeSkillContext(content)) .replace( taskCompletionNotificationPattern, taskCompletionNotificationReplacement, @@ -1396,16 +1661,54 @@ const taskCompletionNotificationPattern = const taskCompletionNotificationReplacement = 'Agent "$1" ($2) has completed successfully. Use read_agent with agent_id "$1" to retrieve the full results.'; +const modeChangedNoticePattern = + /(\s*)[\s\S]*?<\/mode_changed_notice>(\s*)/g; + +function containsModeChangedNotice(content: string): boolean { + return /[\s\S]*?<\/mode_changed_notice>/.test(content); +} + +function stripModeChangedNotice(content: string): string { + let removed = false; + const stripped = content.replace( + modeChangedNoticePattern, + ( + match, + leading: string, + trailing: string, + offset: number, + source: string, + ) => { + removed = true; + const before = source.slice(0, offset); + const after = source.slice(offset + match.length); + if (!before.trim() || !after.trim()) { + return ""; + } + return /[\r\n]/.test(leading + trailing) ? "\n\n" : " "; + }, + ); + return removed ? stripped.trim() : content; +} + function normalizeStoredUserMessages(conversations: NormalizedConversation[]) { for (const conversation of conversations) { for (const message of conversation.messages) { if (message.role === "user" && typeof message.content === "string") { - message.content = message.content.replace( - taskCompletionNotificationPattern, - taskCompletionNotificationReplacement, + message.content = stripModeChangedNotice( + normalizeSkillContext(message.content).replace( + taskCompletionNotificationPattern, + taskCompletionNotificationReplacement, + ), ); } } + conversation.messages = conversation.messages.filter( + (message) => + message.role !== "user" || + message.content !== "" || + Object.keys(message).length !== 2, + ); } } @@ -1468,12 +1771,19 @@ function normalizeStoredToolMessages(conversations: NormalizedConversation[]) { } } -function normalizeSkillContextFrontmatter(content: string): string { +function normalizeSkillContext(content: string): string { // Runtime versions may include or omit SKILL.md metadata in the prompt context. - return content.replace( - /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+)---\r?\n(?:(?!<\/skill-context>)[\s\S])*?\r?\n---(?:\r?\n)+/g, - "$1", - ); + return content + .replace( + /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+)---\r?\n(?:(?!<\/skill-context>)[\s\S])*?\r?\n---(?:\r?\n)+/g, + "$1", + ) + .replace( + // Filesystem enumeration order is not part of the OTel skill contract. + /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+Related files \(use view tool to read\):\r?\n)((?: - [^\r\n]+\r?\n)+)/g, + (_match, prefix: string, files: string) => + prefix + files.split(/\r?\n/).slice(0, -1).sort().join("\n") + "\n", + ); } function normalizeLargeOutputFilepaths(result: string): string { @@ -1559,6 +1869,7 @@ function normalizeReadAgentResult(result: string): string { /^Agent completed\. (.*), status: idle,/, "Agent completed. $1, status: completed,", ) + .replace(/(, total_turns: \d+), model: [^,\r\n]+/, "$1") .replace(/, total_turns: \d+(?=\r?\n|$)/, ", total_turns: 0, duration: 0s") .replace(/\r?\n\r?\n\[Turn \d+\]\r?\n/, "\n\n"); @@ -1654,6 +1965,51 @@ function replaceBackgroundAgentIds( return normalized; } +const runtimeAgentIdPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Runtime-generated agent IDs a `read_agent` or `write_agent` call names that + * no `task` result introduced, such as the synthetic read the runtime adds when + * an SDK-started agent goes idle. `isKnown` reports IDs that already have a + * stable name. + */ +function getUnnamedRuntimeAgentIds( + toolName: string | undefined, + argumentsJson: string | undefined, + isKnown: (runtimeId: string) => boolean, +): string[] { + if ( + (toolName !== "read_agent" && toolName !== "write_agent") || + !argumentsJson + ) { + return []; + } + try { + const args = JSON.parse(argumentsJson) as { + agent_id?: unknown; + agent_ids?: unknown; + }; + const agentIds = new Set([ + args.agent_id, + ...(Array.isArray(args.agent_ids) ? args.agent_ids : []), + ]); + return [...agentIds].filter( + (agentId): agentId is string => + typeof agentId === "string" && + runtimeAgentIdPattern.test(agentId) && + !isKnown(agentId), + ); + } catch { + return []; + } +} + +/** Stable name for the Nth agent a conversation reads but did not start. */ +function unnamedAgentName(index: number): string { + return index === 0 ? "api-agent" : `api-agent-${index}`; +} + function rewriteBackgroundAgentArguments( argumentsJson: string, replacements: Map, @@ -1716,8 +2072,16 @@ function extractBackgroundAgentIds( }; const backgroundAgentNamesByToolCallId = new Map(); let unnamedBackgroundAgentCounter = 0; + let unnamedAgentCounter = 0; for (const message of request.messages ?? []) { for (const toolCall of message.tool_calls ?? []) { + for (const runtimeId of getUnnamedRuntimeAgentIds( + toolCall.function?.name, + toolCall.function?.arguments, + (id) => [...result.values()].includes(id), + )) { + result.set(unnamedAgentName(unnamedAgentCounter++), runtimeId); + } if ( toolCall.id && toolCall.function?.name === "task" && @@ -1878,8 +2242,33 @@ function findAssistantIndexAfterPrefix( // A continuation can start after an assistant message. Never coalesce // across this candidate request/response boundary. + const savedPrefix = savedMessages.slice(0, nextIndex); + const requestHasRelocatedImage = requestMessages.some( + (message, index) => + message.role === "user" && + /^Image file at path [^\n]+\n\[image\]$/.test(message.content ?? "") && + requestMessages[index - 1]?.role === "tool", + ); const prefix = normalizeMessagesForBackend( - savedMessages.slice(0, nextIndex), + backend === "capi" || requestHasRelocatedImage + ? savedPrefix + : savedPrefix.filter((message, index) => { + const toolResult = savedPrefix[index - 1]; + const toolCall = savedPrefix[index - 2]; + // CAPI sends viewed images as a separate user turn; BYOK includes + // them in (or, for text-only models, omits them from) the tool result. + return !( + message.role === "user" && + /^Image file at path [^\n]+\n\[image\]$/.test( + message.content ?? "", + ) && + toolResult?.role === "tool" && + toolResult.content === "Viewed image file successfully." && + toolCall?.tool_calls?.some( + (call) => call.function?.name === "view", + ) + ); + }), backend, ); if (prefix.length > requestMessages.length) break; @@ -1903,21 +2292,6 @@ function findAssistantIndexAfterPrefix( return undefined; } -function expandWorkDir( - content: string | undefined, - workDir: string, - jsonEscape: boolean, -): string | undefined { - if (!content) { - return content; - } - - const workDirValue = jsonEscape - ? JSON.stringify(workDir).replaceAll('"', "") - : workDir; - return content.replace(/\$\{workdir\}/g, workDirValue); -} - function expandToolName(name: string): string { for (const [fullName, normalized] of Object.entries(normalizedToolNames)) { if (name === normalized) { @@ -1936,6 +2310,8 @@ function createOpenAIResponse( workDir: string, backgroundAgentIds: Map, ): ChatCompletion { + const expandPath = (value: string) => + value.replace(/\$\{workdir\}/g, () => workDir); // Here we recreate the strange CAPI/productcode behavior of using multiple choices to represent // multiple assistant messages in a row. This is the inverse of the logic in transformOpenAIResponseChoice(). // So, find all successive assistant messages starting from responseStartIndex. @@ -1952,7 +2328,12 @@ function createOpenAIResponse( function: { name: expandToolName(tc.function?.name ?? ""), arguments: expandBackgroundAgentArguments( - expandWorkDir(tc.function?.arguments, workDir, true) ?? "{}", + rewriteToolArgumentPaths( + tc.function?.name ?? "", + tc.function?.arguments ?? "{}", + expandPath, + expandPath, + ), backgroundAgentIds, ), }, @@ -1962,8 +2343,9 @@ function createOpenAIResponse( index, message: { role: "assistant", - content: - expandWorkDir(assistantMessage.content, workDir, false) ?? null, + content: assistantMessage.content + ? expandPath(assistantMessage.content) + : null, refusal: assistantMessage.refusal ?? null, tool_calls: toolCalls, }, @@ -1991,11 +2373,17 @@ const STREAM_CHUNK_SIZE = 200; function convertToStreamingResponseChunks( completion: ChatCompletion, ): ChatCompletionChunk[] { - const choice = completion.choices[0]; - const content = choice.message.content ?? ""; - const toolCalls = choice.message.tool_calls?.filter( - (tc): tc is ChatCompletionMessageFunctionToolCall => tc.type === "function", - ); + // Recorded CAPI responses can use successive choices for one assistant turn. + // Preserve every choice, including tools following the opening text. + const content = completion.choices + .map((choice) => choice.message.content ?? "") + .join(""); + const toolCalls = completion.choices + .flatMap((choice) => choice.message.tool_calls ?? []) + .filter( + (tc): tc is ChatCompletionMessageFunctionToolCall => + tc.type === "function", + ); const makeChunk = ( delta: ChatCompletionChunk.Choice.Delta, @@ -2046,19 +2434,24 @@ function convertToStreamingResponseChunks( if (chunks.length === 0) { chunks.push(makeChunk({ role: "assistant" })); } - chunks[chunks.length - 1].choices[0].finish_reason = choice.finish_reason; + chunks[chunks.length - 1].choices[0].finish_reason = toolCalls.length + ? "tool_calls" + : (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 }, @@ -2101,6 +2494,8 @@ export type ToolResultNormalizer = { */ export type CopilotUserResponse = { login: string; + /** Stable GitHub user ID returned by the identity-validation `/user` endpoint. */ + id?: number; copilot_plan?: string; token_based_billing?: boolean; is_mcp_enabled?: boolean; @@ -2122,10 +2517,36 @@ export type CopilotUserResponse = { >; }; +export type MemoryApiStub = { + enabled?: { enabled: boolean }; + memories?: { + repositoryMemories?: Array<{ + subject: string; + fact: string; + citations: string[]; + }>; + userMemories?: Array<{ + subject: string; + fact: string; + citations: string[]; + }>; + }; + memoriesStatusCode?: number; +}; + export type ParsedHttpExchange = { request: ChatCompletionCreateParamsBase; response: ChatCompletion | undefined; requestHeaders?: Record; + /** Aggregated provider usage on the last successful response of a compaction chain. */ + compactionUsage?: CompactionProviderUsage; +}; + +export type CompactionProviderUsage = { + interactionId: string; + summary: string; + responseCount: number; + inputTokens?: number; }; // We want to be able to reuse the proxy across multiple tests, so it needs to be reconfigurable @@ -2134,6 +2555,7 @@ type ReplayingCapiProxyState = { filePath: string; workDir: string; testInfo?: { file: string; line?: number }; + modelNames?: Record; backend: ReplayBackend; replayOnly: boolean; storedData?: NormalizedData | undefined; diff --git a/test/harness/responsesApiAdapter.ts b/test/harness/responsesApiAdapter.ts index 16568f6e03..68eceb6df6 100644 --- a/test/harness/responsesApiAdapter.ts +++ b/test/harness/responsesApiAdapter.ts @@ -102,10 +102,7 @@ function responseInputItemToCanonicalMessages( { role: "tool", tool_call_id: typeof item.call_id === "string" ? item.call_id : "", - content: - typeof item.output === "string" - ? item.output - : JSON.stringify(item.output ?? ""), + content: normalizeFunctionCallOutput(item.output), }, ]; } @@ -180,6 +177,35 @@ function responseInputItemToCanonicalMessages( ]; } +function normalizeFunctionCallOutput(output: unknown): string { + const text = + typeof output === "string" ? output : JSON.stringify(output ?? ""); + if (!text.startsWith("[{") || !text.includes('"input_image"')) return text; + + let parts: unknown; + try { + parts = JSON.parse(text); + } catch { + return text; + } + if ( + !Array.isArray(parts) || + !parts.some((part) => isObject(part) && part.type === "input_image") || + !parts.every( + (part) => + isObject(part) && + ((part.type === "input_text" && typeof part.text === "string") || + (part.type === "input_image" && typeof part.image_url === "string")), + ) + ) { + return text; + } + return parts + .filter((part) => part.type === "input_text") + .map((part) => part.text) + .join("\n"); +} + function coalesceAssistantMessages( messages: CanonicalMessage[], ): CanonicalMessage[] { diff --git a/test/harness/stdio-shutdown-runtime.cjs b/test/harness/stdio-shutdown-runtime.cjs new file mode 100644 index 0000000000..fe7dc1cc09 --- /dev/null +++ b/test/harness/stdio-shutdown-runtime.cjs @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// Shared SDK shutdown fixture: node